useReducer
먼저, useReducer는 어떤 역할을 하는지 간단히 정리하고 넘어가려 한다.
useReducer는 useState를 온전히 대체할 수 있는 메서드로,
useState로 관리되는 상태들은 모두 컴포넌트 내부에서만 관리되어야 하며, 관련 로직도 모두 컴포넌트 내부에서만 작성되어야 한다.
때문에 UI 렌더링만 담당하면 되는 App 컴포넌트에서 굳이 노출될 필요 없는 로직들이 useState에 의해 모두 담겨있어야 하고, 이는 가독성과 유지보수성을 해친다.
이렇게 useState로 인해 컴포넌트 내부 코드가 쓸데없이 복잡해질 경우, useReducer를 사용해서 상태 관리 함수들을 모두 외부로 분리하여 컴포넌트를 정리할 수 있다.
단, 그렇다고 해서 모든 state를 useReducer로 사용하면 오히려 코드가 더 복잡해질 수도 있다.
상황에 따라 간단하게 상태 처리를 할 수 있으면 useState를 쓰고,
투두리스트처럼 상태 처리 로직이 복잡하고 가짓수가 많으면 useReducer를 쓰는 것이 좋다.
useState vs useReducer
| 조건 | 추천 |
| 상태가 단순하고 UI 요소 몇 개에만 영향을 줄 때 | ✅ useState |
| 상태가 복잡하고 관련 액션이 많을 때 | ✅ useReducer |
| 여러 개의 상태가 서로 연관되어 있을 때 | ✅ useReducer |
| 상태 변경 로직을 명확하게 분리하고 싶을 때 | ✅ useReducer |
useReducer 적용 전
- 하나의 App 컴포넌트에서 투두 리스트의 create, update, delete 상태가 모두 useState로 관리되고 있으며,
- 기능 로직도 모두 App 컴포넌트에서 쓰이고 있어 App 컴포넌트가 다소 복잡해진 상황이다.
// App.jsx
function App() {
const [todos, setTodos] = useState(mockData);
const idRef = useRef(3);
const onCreate = (content) => {
const newTodo = {
id: idRef.current++,
isDone: false,
content,
date: new Date().toLocaleString("ko-KR"),
};
setTodos((prevTodos) => [newTodo, ...prevTodos]);
};
const onUpdate = (targetId) => {
setTodos(
todos.map((todo) => {
return todo.id === targetId ? { ...todo, isDone: !todo.isDone } : todo;
})
);
};
const onDelete = (targetId) => {
setTodos(
todos.filter((todo) => {
return todo.id !== targetId;
})
);
};
return (
<>
<Header />
<Editor onCreate={onCreate} />
<List items={todos} onUpdate={onUpdate} onDelete={onDelete} />
</>
);
}
export default App;
App 컴포넌트에서는 UI들을 잘 렌더링 해주고, 자식 컴포넌트들에게 필요한 데이터들을 잘 전달해주기만 하면 되기 때문에 굳이 어떠한 기능의 로직까지 담고 있을 필요는 없다. 이런 경우에는 useReducer와 같은 방법을 사용해서 useState와 연계된 기능 로직들을 모두 외부로 분리시켜주는 것이 좋다.
useReducer 적용 후
// App.jsx
import { useRef, useReducer } from "react";
import todoReducer from "./reducer/TodoReducer";
// 포스팅과 관련없는 import들은 생략...
function App() {
const [todos, dispatch] = useReducer(todoReducer, mockData);
const idRef = useRef(3);
const onCreate = (content) => {
dispatch({
type: "CREATE",
data: {
id: idRef.current++,
content,
date: new Date().toLocaleString("ko-KR"),
},
});
};
const onUpdate = (targetId) => {
dispatch({
type: "UPDATE",
targetId,
});
};
const onDelete = (targetId) => {
dispatch({
type: "DELETE",
targetId,
});
};
return (
<>
<Header />
<Editor onCreate={onCreate} />
<List items={todos} onUpdate={onUpdate} onDelete={onDelete} />
</>
);
}
export default App;
기능 구현 로직들을 모두 외부 reducer 함수로 분리해서 useReducer로 갖다쓰니까 확실히 App 컴포넌트가 깔끔해졌고,
각 기능별 코드도 더 직관적으로 바뀌었다. (어떤 기능을 하고, 어떤 인자들이 전달되는지 쉽게 파악 가능)
[외부 reducer 함수]
// todoReducer.js
export default function todoReducer(todos, action) {
switch (action.type) {
case "CREATE":
return [action.data, ...todos];
case "UPDATE":
return todos.map((todo) =>
todo.id === action.targetId ? { ...todo, isDone: !todo.isDone } : todo
);
case "DELETE":
return todos.filter((todo) => todo.id !== action.targetId);
default:
throw new Error("Unknown action type: " + action.type);
}
}
'React.js' 카테고리의 다른 글
| [리액트] 같은 파일 재등록 안될 때 (등록 -> 삭제 -> 재등록) (0) | 2025.05.27 |
|---|---|
| Navigate vs useNavigate (0) | 2025.05.15 |
| 함수형 업데이트와 클로저 이해하기 (심화) (0) | 2025.05.12 |
| 브라우저가 리액트를 실행하는 과정 (간단 요약) (0) | 2025.05.08 |
| 배열 state 변경할 때, Spread 문법을 써야 하는 이유 (중요) (0) | 2025.05.05 |