-
Notifications
You must be signed in to change notification settings - Fork 2
Description
해당 문서는 노션에 정리했던 것을 옮긴 것으로, 약간의 깨짐이 발생할 수 있어 아래 링크에서 확인 가능.
https://oesiu24.notion.site/useReducer-2c9ec73d8f6a809481b0e1a6034fc70e?source=copy_link
State 로직을 reducer로 작성
💡https://ko.react.dev/learn/extracting-state-logic-into-a-reducer
✅한 컴포넌트에서 state 업데이트가 여러 이벤트 핸들러로 분산 되는 경우, state를 업데이트 하는 모든 로직을 reducer를 사용해 컴포넌트 외부의 단일 함수로 통합해 관리
useReducer(reducer, initialArg, init?)
매개변수
reducer: state가 어떻게 업데이트 되는지 지정하는 Reducer 함수- Reducer 함수는 반드시 순수 함수여야 하며, State와 Action을 인수로 받아야 하고, 다음 State를 반환해야 한다.
- State와 Action에는 모든 데이터 타입이 할당될 수 있다.
initialArg: 초기 State가 계산되는 값- 모든 데이터 타입이 할당될 수 있다.
- 초기 State가 어떻게 계산되는지는 다음
init인수에 따라 달라진다.
- 선택사항
init: 초기 State를 반환하는 초기화 함수- 이 함수가 인수에 할당되지 않으면 초기 State는
initialArg로 설정된다. - 할당되었다면 초기 State는
init(initialArg)를 호출한 결과가 할당된다.
- 이 함수가 인수에 할당되지 않으면 초기 State는
반환값
- 현재 state : 첫번째 렌더링에서의 state는
init(initialArg)또는initialArg로 설정 (init이 없을 경우initialArg로 설정됩니다). - **
dispatch함수 :**dispatch는 state를 새로운 값으로 업데이트하고 리렌더링을 일으킴.
주의사항
dispatch함수는 오직 다음 렌더링에 사용할 state 변수만 업데이트- 만약
dispatch함수를 호출한 직후에 state 변수를 읽는다면 호출 이전의 [최신화되지 않은 값을 참조](https://ko.react.dev/reference/react/useReducer#ive-dispatched-an-action-but-logging-gives-me-the-old-state-value)
- 만약
[Object.is](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)비교를 통해 새롭게 제공된 값과 현재state를 비교한 값이 같을 경우, React는 컴포넌트와 해당 컴포넌트의 자식 요소들의 리렌더링을 건너뜀.- 최적화에 관련된 동작으로써 결과를 무시하기 전에 컴포넌트가 호출되지만, 호출된 결과가 코드에 영향을 미치지는 않음.
- React는 [state의 업데이트를 batch](https://ko.react.dev/learn/queueing-a-series-of-state-updates)
- 이벤트 핸들러의 모든 코드가 수행되고
set함수가 모두 호출된 후에 화면을 업데이트한다. 이는 하나의 이벤트에 리렌더링이 여러번 일어나는 것을 방지한다. - DOM 접근 등 이른 화면 업데이트를 강제해야할 특수한 상황이 있을 경우
[flushSync](https://ko.react.dev/reference/react-dom/flushSync)를 사용할 수 있다.
- 이벤트 핸들러의 모든 코드가 수행되고
reducer를 사용하여 state로 로직 통합하기
예시 코드에서는 state가 배열을 보유하며 세가지 이벤트 핸들러를 통해 state를 업데이트 하고 있다.
따라서, 컴포넌트가 커질수록 그 안에서 state를 다루는 로직의 양이 늘어나게 된다. 복잡성은 줄이고 접근성을 높이기 위해 컴포넌트 내부의 state 로직을 컴포넌트 외부의 reducer라고 불리는 단일 함수로 옮긴다.
useState를 useReducer로 바꾸기
- state를 설정하는 것에서 action을 dispatch 함수로 전달하는 것으로 바꾸기.
- reducer 함수 작성하기.
- 컴포넌트에서 reducer 사용하기.
1단계.state를 설정하는 것에서 action을 dispatch 함수로 전달하는 것으로 바꾸기
직접 state로 관리
function handleAddTask(text) {
setTasks([...tasks, {
id: nextId++,
text: text,
done: false
}]);
}
function handleChangeTask(task) {
setTasks(tasks.map(t => {
if (t.id === task.id) {
return task;
} else {
return t;
}
}));
}
function handleDeleteTask(taskId) {
setTasks(
tasks.filter(t => t.id !== taskId)
);
}현재 이벤트 핸들러는 state를 설정해 무엇을 할 것인지 명시중이다.
먼저, state 관련 로직을 지워 세가지 이벤트의 핸들러만 남기면 다음과 같다.
- 사용자가 “Add”를 눌렀을 때 호출되는
handleAddTask(text). - 사용자가 task를 토글하거나 “Save”를 누르면 호출되는
handleChangeTask(task). - 사용자가 “Delete”를 누르면 호출되는
handleDeleteTask(taskId).
Reducer의 state 관리 vs 직접 state 설정
- 직접 state 설정
- 무엇을 할 지 지시
- reducer의 state 관리
- 이벤트 핸들러에서 action을 전달해 사용자가 방금 한 일을 지정
- state 업데이트 로직은 다른 곳에 있음
- 이벤트 핸들러를 통해
task를 설정하는 대신 task를 추가/변경/삭제하는 action을 전달
직접 state로 관리
function handleAddTask(text) {
setTasks([...tasks, {
id: nextId++,
text: text,
done: false
}]);
}
function handleChangeTask(task) {
setTasks(tasks.map(t => {
if (t.id === task.id) {
return task;
} else {
return t;
}
}));
}
function handleDeleteTask(taskId) {
setTasks(
tasks.filter(t => t.id !== taskId)
);
}Reducer의 state 관리
function handleAddTask(text) {
dispatch({
//action 객체
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId
});
}- dispatch에 넣어준 객체를 action이라고 한다.
- 이 안에 어떤 것이든 자유롭게 넣을 수 있지만, 일반적으로 어떤 상황이 발생하는지에 대한 최소한의 정보를 담고 있어야 한다.
action 객체는 어떤 형태든 될 수 있다.
그렇지만 발생한 일을 설명하는 문자열 type 을 넘겨주고 이외의 정보는 다른 필드에 담아서 전달하도록 하는 것이 일반적. type은 컴포넌트에 따라 값이 다르다. type에는 무슨 일이 일어나는지를 설명할 수 있는 이름을 넣어주면 됨.
dispatch({
// 컴포넌트마다 다른 값
type: 'what_happened',
// 다른 필드는 이곳에
});2단계: reducer 함수 작성하기
reducer 함수는 state에 대한 로직을 넣는 곳.
이 함수는 현재의 state 값과 action 객체, 이렇게 두 인자를 받고 다음 state 값을 반환.
function yourReducer(state, action) {
// React가 설정하게될 다음 state 값을 반환합니다.
}react는 reducer에서 반환한 값을 state에 설정한다.
이 예시에서 이벤트 핸들러에 구현 되어있는 state 설정과 관련 로직을 reducer 함수로 옮기기 위해서 다음과 같이 하면 된다.
- 첫 번째 인자에 현재 state (
tasks) 선언하기. - 두 번째 인자에
action객체 선언하기. - reducer에서 다음 state 반환하기 (React가 state에 설정하게 될 값).
function tasksReducer(tasks, action) {
if (action.type === 'added') {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
} else if (action.type === 'changed') {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
} else if (action.type === 'deleted') {
return tasks.filter(t => t.id !== action.id);
} else {
throw Error('Unknown action: ' + action.type);
}
}개인적으론 switch 문으로 쓰는게 가독성이 더 좋은거 같다..
reducer 함수는 state(tasks)를 인자로 받고 있기 때문에, 이를 컴포넌트 외부에서 선언할 수 있다.
위 코드에서 if/else 문을 사용하고 있지만 reducer 함수 안에서는 switch 문을 사용하는 게 규칙이다. 물론 결과는 같지만, switch 문으로 작성하는 것이 한눈에 읽기 더 쉬울 수 있다. 이제부터 이 문서에서 다룰 예시는 아래처럼 switch 문을 사용하자.
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
}
case 'changed': {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}case속에서 선언된 변수들이 서로 충돌하지 않도록case블록을 중괄호인{와}로 감싸는 걸 추천case는 일반적인 경우라면return으로 끝나야한다.return을 잊게 되면 코드가 다음case로 떨어질 수 있다.
3단계: 컴포넌트에서 reducer 사용하기
마지막으로 컴포넌트에 tasksReducer를 연결할 차례이다.
-
React에서
useReducerhook을 불러온다.import { useReducer } from 'react';
-
useState를 아래 처럼useReducer로 바꾼다.// const [tasks, setTasks] = useState(initialTasks); const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
useReducerhook은 초기 state 값을 입력받아 유상태(stateful) 값을 반환한다는 점과 state를 설정하는 함수 (useReducer에서는dispatch함수)의 원리를 보면useState와 비슷하다. 그러나 다음과 같은 점은 다르다.useReducerhook은 두 개의 인자를 넘겨받는다.- reducer 함수
- 초기 state 값
- 그리고 아래와 같이 반환한다.
- state를 담을 수 있는 값
- dispatch 함수 (사용자의 action을 reducer 함수에게 “전달하게 될”)
완성된 코드
export default function TaskApp() {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId
});
}
return( ... 중략 )
}
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
}
case 'changed': {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
let nextId = 3;
const initialTasks = [
{ id: 0, text: 'Visit Kafka Museum', done: true },
{ id: 1, text: 'Watch a puppet show', done: false },
{ id: 2, text: 'Lennon Wall pic', done: false }
];useState 와 useReducer 비교
reducer가 좋은 점만 있는 것은 아니다!!
1. 코드 크기
- useState
- 초기 코드가 짧고 간단함
- 단순한 상태 관리에 적합
- useReducer
- reducer와 action 정의가 필요해 초기 코드가 많음
- 여러 이벤트에서 비슷한 상태 업데이트가 반복되면 오히려 코드가 정리됨
2. 가독성
- useState
- 단순한 상태 변경은 직관적이고 읽기 쉬움
- 상태 로직이 복잡해지면 컴포넌트가 비대해질 수 있음
- useReducer
- “무슨 일이 발생했는지(action)”와 “상태가 어떻게 바뀌는지(reducer)”가 분리됨
- 복잡한 상태 로직에서 구조적으로 더 명확함
3. 디버깅
- useState
- 상태 변경 지점이 흩어져 있어 원인 추적이 어려울 수 있음
- useReducer
- reducer에 로그를 추가해 어떤 action으로 상태가 바뀌었는지 추적 가능
- 대신 디버깅 단계와 코드 양은 더 많아질 수 있음
4. 테스팅
- useState
- 컴포넌트 단위 테스트에 의존
- useReducer
- reducer는 순수 함수이므로 독립적으로 테스트 가능
- 복잡한 상태 전이 로직 검증에 유리
5. 개인적 취향
- 어느 쪽이 “더 낫다”기보다는 상황과 선호의 문제
- 두 Hook은 동일한 React 상태 관리 메커니즘 위에 있음
- 같은 컴포넌트 안에서도 혼용 가능
결론
- 단순한 상태 → useState
- 복잡한 상태, 잦은 버그, 구조화 필요 → useReducer
- 모든 곳에 reducer를 쓸 필요는 없으며, 필요한 곳에만 선택적으로 적용하는 것이 이상적임
Reducer 잘 작성하기
-
Reducer는 반드시 순수해야한다.
-
React에서 reducer는 렌더링 과정 중에 실행
-
즉, 화면을 그리기 위해 “다음 state가 무엇인지 계산”하는 역할
- 같은
state + action→ 항상 같은 결과 - 외부 세계에 영향 ❌
- 내부 데이터 직접 변경 ❌
- 같은
-
하면 안되는 것들
렌더링을 여러 번 호출할 수도 있고, 중간에 취소했다가 다시 실행할 수도 있기 때문
// ❌ reducer 안에서 절대 금지 fetch('/api/data'); // 요청 보내기 setTimeout(() => {}, 1000); // 타이머 등록 localStorage.setItem(...); // 외부 상태 변경 state.count++; // 기존 객체 직접 수정
-
-
각 action은 데이터 안에서 여러 변경들이 있더라도 하나의 사용자 상호작용을 설명해야 한다.
-
action은 ‘코드 이벤트’가 아니라 ‘사용자의 행동’을 표현한다
action은 단순히 “state를 이렇게 바꿔라”가 아니라 **“사용자가 무엇을 했다”**를 설명
-
❌ 나쁜 예 (기계적인 action)
dispatch({ type: 'set_name', value: '' }); dispatch({ type: 'set_email', value: '' }); dispatch({ type: 'set_age', value: 0 }); dispatch({ type: 'set_address', value: '' }); dispatch({ type: 'set_phone', value: '' });
겉보기엔 문제 없어 보이지만:
- “왜 이 action들이 연속으로 발생했는지” 알기 어렵고
- 로그를 봐도 의도를 파악하기 힘듦
-
✅ 좋은 예 (의미 있는 action)
dispatch({ type: 'reset_form' });
- 사용자가 **“폼을 초기화했다”**는 의미가 명확
- reducer 안에서 여러 필드를 한 번에 초기화
- 로그만 봐도 흐름이 바로 이해됨
-
https://ko.react.dev/learn/extracting-state-logic-into-a-reducer#challenges
과제
https://ko.react.dev/learn/extracting-state-logic-into-a-reducer#writing-reducers-well 4번
-
나의 풀이
import { useState } from 'react'; export function useReducer(reducer, initialState) { const [state, setState] = useState(initialState); // ??? const dispatch = (action) =>{ const newState = reducer(state, action) setState(newState) } return [state, dispatch]; }