useReducer 를 공부하면서 간략히 정리하는 글이다.
✓ useReducer 언제 사용할까
컴포넌트 내에서 하나의 state를 여기저기서 setState를 호출하여 다른 값으로 상태를 바꿔주고 있다면 추후 state를 업데이트중 장애/오류가 있을 시 유지/보수 및 관리하기에 어려움이 있다. 이런 경우에는 컴포넌트 밖으로 따로 빼서 한 곳에서 관리하는 것이 가독성도 좋고 유지/보수 측면에서도 용이할 것이다. useReducer 는 위와 같은 상황에서 state별로 관리할 때 유용하게 사용할 수 있는 React Hook이다.
✓ useReducer 사용방법
useReducer를 적용하기 전 아래의 예시코드를 확인해보자.
import "./App.css";
import DiaryEdtior from "./DiaryEditor";
import DiaryList from "./DiaryList";
import { useState, useRef, useEffect, useMemo, useCallback } from "react";
function App() {
const [data, setData] = useState([]);
const dataId = useRef(0);
const getData = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/comments").then((res) => {
return res.json();
});
const initData = res.slice(0, 20).map((obj) => {
return {
author: obj.email,
content: obj.body,
emotion: Math.floor(Math.random() * 5) + 1,
created_date: new Date().getTime(),
id: dataId.current++,
};
});
setData(initData);
};
useEffect(() => {
getData();
}, []);
const onCreate = useCallback((author, content, emotion) => {
const created_date = new Date().getTime();
const newItem = {
author,
content,
emotion,
created_date,
id: dataId.current,
};
dataId.current += 1;
setData((data) => [newItem, ...data]);
}, []);
const onRemove = useCallback((targetId) => {
setData((data) => data.filter((obj) => obj.id !== targetId));
}, []);
const onEdit = useCallback((targetId, newContent) => {
setData((data) => data.map((obj) => (obj.id === targetId ? { ...obj, content: newContent } : obj)));
}, []);
const getDiaryAnalysis = useMemo(() => {
const goodCount = data.filter((obj) => obj.emotion >= 3).length;
const badCount = data.length - goodCount;
const goodRatio = Math.floor((goodCount / data.length) * 100);
return { goodCount, badCount, goodRatio };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data.length]);
const { goodCount, badCount, goodRatio } = getDiaryAnalysis;
return (
<div className="App">
<DiaryEdtior onCreate={onCreate} />
<div>전체 게시글 수 : {data.length}</div>
<div>기분 좋은 게시글 수 : {goodCount}</div>
<div>기분 나쁜 게시글 수: {badCount}</div>
<div>기분 좋은 게시글 비율 : {goodRatio}</div>
<DiaryList onRemove={onRemove} onEdit={onEdit} diaryList={data} />
</div>
);
}
export default App;
위 코드를 보면 App 컴포넌트 내에서 관리하는 data 라는 state를 getData, onCreate, onRemove, onEdit 에서 setData 를 호출하여 state를 바꿔주고 있다. 위 코드는 하나의state를 5군데에서 바꿔주고 있지만 관리하는 state가 더 늘어는다면 코드가 더 복잡해질 것이다. 위 코드를 useReducer를 사용하여 정리해보자.
기본적인 사용법을 설명하자면
import { useReducer } from "react";
우선 useReducer를 사용하기 위해 위와 같이 import를 한다.
const Counter = () => {
const [state, dispatch] = useReducer(reducer, 1);// 첫번째 인자 : state를 받아 상태변화를 일으킬 reducer 두번째 인자: state의 초기값
return (
<div>
{state}
<button onClick={() => dispatch({type : 1})}> add 1</button>
<button onClick={() => dispatch({type : 2})}> add 10</button>
<button onClick={() => dispatch({type : 3})}> add 100</button>
<button onClick={() => dispatch({type : 4})}> add 1000</button>
</div>
);
}
useReducer를 사용할 변수를 선언해주는데 useState와 비슷하게 state는 상태는 관리할 변수명이고 dispatch는 상태를 변화시킬 함수이다. useReducer에 들어가는 첫번째 인자인 reducer는 dispatch의 type 별로 어떤 상태변화 작업을 수행할지 작성하는 함수이고 두번째 인자는 state의 초기값이다.
const reducer = (state, action) => {
switch(action.type) {
case 1:
return state + 1;
case 2:
return state + 10;
case 3:
return state + 100;
case 4:
return state + 1000;
default:
return state;
}
};
useReducer에 첫번째 인자로 들어가는 reducer에 type 별 상태변화 작업수행 로직을 switch 문으로 작성한다.
state는 마지막으로 변화된 현재 state 값, dispatch에서 객체로 넣어준 데이터는 action으로 받아서 사용한다. type 이외에도 다른 property 를 넣어서 action에서 점표기법으로 사용할 수 있다.
처음 예시코드를 위와 같이 useReducer를 사용하여 정리하면 아래와 같다.
import "./App.css";
import DiaryEdtior from "./DiaryEditor";
import DiaryList from "./DiaryList";
import { useRef, useEffect, useMemo, useCallback, useReducer } from "react";
const reducer = (state, action) => {
switch (action.type) {
case "INIT": {
return action.data;
}
case "CREATE": {
const created_date = new Date().getTime();
const newItem = {
...action.data,
created_date,
};
return [newItem, ...state];
}
case "REMOVE": {
return state.filter((obj) => obj.id !== action.targetId);
}
case "EDIT": {
return state.map((obj) => (obj.id === action.targetId ? { ...obj, content: action.newContent } : obj));
}
default:
return state;
}
};
const App = () => {
const [data, dispatch] = useReducer(reducer, []);
const dataId = useRef(0);
const getData = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/comments").then((res) => {
return res.json();
});
const initData = res.slice(0, 20).map((obj) => {
return {
author: obj.email,
content: obj.body,
emotion: Math.floor(Math.random() * 5) + 1,
created_date: new Date().getTime(),
id: dataId.current++,
};
});
dispatch({ type: "INIT", data: initData });
};
useEffect(() => {
getData();
}, []);
const onCreate = useCallback((author, content, emotion) => {
dispatch({ type: "CREATE", data: { author, content, emotion, id: dataId.current } });
dataId.current += 1;
}, []);
const onRemove = useCallback((targetId) => {
dispatch({ type: "REMOVE", targetId });
}, []);
const onEdit = useCallback((targetId, newContent) => {
dispatch({ type: "EDIT", targetId, newContent });
}, []);
const getDiaryAnalysis = useMemo(() => {
const goodCount = data.filter((obj) => obj.emotion >= 3).length;
const badCount = data.length - goodCount;
const goodRatio = Math.floor((goodCount / data.length) * 100);
return { goodCount, badCount, goodRatio };
}, [data.length]);
const { goodCount, badCount, goodRatio } = getDiaryAnalysis;
return (
<div className="App">
<DiaryEdtior onCreate={onCreate} />
<div>전체 게시글 수 : {data.length}</div>
<div>기분 좋은 게시글 수 : {goodCount}</div>
<div>기분 나쁜 게시글 수: {badCount}</div>
<div>기분 좋은 게시글 비율 : {goodRatio}</div>
<DiaryList onRemove={onRemove} onEdit={onEdit} diaryList={data} />
</div>
);
};
export default App;
기존에 setState를 해주는 함수를 각각 수행하는 상태변화 작업에 맞게 type을 지정해주고 useReducer에서 상태변화 작업을 수행하기 위해 필요한 데이터를 인자로 넘겨주었다. reducer 함수를 보면 action 에서 점표기법을 이용하여 type별 필요한 데이터를 넘겨받아 작업을 수행할 수 있다. state의 상태변화 로직을 밖으로 빼서 한 곳에서 관리하니 확실히 이전 코드에 비해 직관적이고 깔끔해진 것 같다.