리액트 공식 문서를 참고한 정리 내용 (25.08 기준)
컴포넌트에 reducer를 추가하는 Hook으로,
useReducer를 컴포넌트의 최상위에 호출하고,reducer를 이용해state를 관리한다.
const [state, dispatch] = useReducer(reducer, initialArg, init?)
위와 같은 형식으로 사용하며, 아래의 구조처럼 적용한다.
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
reducer(state, action) => newState의 형식으로 순수 함수여야 한다.initialArginit (선택사항)init이 없으면 → state = initialArg , 있으면 state = init(initialArg)2개의 엘리먼트로 구성된 배열을 반환한다.
initialArg 또는 init(initialArg)로 정해짐diapatch 함수dispatch({ type: 'ACTION_NAME', payload: ... }) 같은 식으로 action을 넘겨줌reducer가 실행돼서 새로운 state를 만들고, 컴포넌트가 리렌더링 됨
useReducer의 반환값인[state, dispatch에서dispatch(action을 호출하면,
- React가 지금
state와 전달한action을reducer함수에 넘긴다reducer가 새로운 state를 계산해서 반환하면,- 그 값을 새로운
state로 저장하고 컴포넌트를 다시 렌더링한다.const [state, dispatch] = useReducer(reducer, { age: 42 }); function handleClick() { dispatch({ type: 'incremented_age' }); // ...
dispatch는 “이 액션을 처리해줘!”라고 React에 알려주는 버튼 같은 함수이고,reducer가 그걸 받아 새로운 state를 계산해 준다.매개변수
action
- 어떤 동작을 할지와 필요한 정보를 담아
dispatch로 보내는 메시지이다.- 모든 테이터 타입이 할당될 수 있다
- 보통 객체 형태로 쓰는데
type은 어떤 동작인지 설명 (예: "increment", "decrement"), 그 외에 필요한 데이터(ex, "추가할 항목의 이름"이나 "변경할 값" 같은 것)// 예: 카운터 증가 dispatch({ type: 'increment' }); // 예: 할 일 추가 dispatch({ type: 'add_todo', text: 'React 공부하기' });반환값
dispatch함수는 어떤 값도 반환하지 않느다.주의사항
dispatch는 즉시 state를 바꾸지 않는다.
dispatch를 호출하면 state가 바로 바뀌지 않고 다음 렌더링 때 새로운 값이 적용되기 때문에,dispatch직후에state를 읽으면 여전히 이전 값일 수 있다.값이 같으면 리렌더링 안 함
Object.is로 이전 state와 새로운 state가 같은지 비교한다.- 같으면 화면에 변화가 없다고 판단하고 리렌더링을 건너뛰어, 불필요한 렌더링을 줄여 성능을 최적화 한다.
업데이트는 모아서 처리(batch)
- 이벤트 핸들러 안에서
dispatch나setState를 여러 번 호출해도,- React는 바로 리렌더링하지 않고, 전부 모았다가 한 번만 리렌더링을 한다. (이래야 성능이 좋아짐!)
- 만약 지금 당장 리렌더링 해야하는 특수한 상황이라면
flushSync를 사용하면 된다.
useReducer는 다른 Hook과 마찬가지로 컴포넌트 최상위나 커스텀 Hook에서만 사용해야 함.dispatch는 항상 변하지 않는 안정된 함수라서,reducer와 init 함수를 두 번 실행하며, 결과 중 하나는 버려져서 실제 동작에는 영향 없다.컴포넌트 맨 위에서 useReducer를 호출하고, useReducer(reducer, 초기값) → [state, dispatch]를 준다.
import { useReducer } from 'react';
function reducer(state, action) {
if (action.type === 'incremented_age') {
return {
age: state.age + 1
};
}
throw Error('Unknown action.');
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
return (
<>
<button onClick={() => {
dispatch({ type: 'incremented_age' })
}}>
Increment age
</button>
<p>Hello! You are {state.age}.</p>
</>
);
}
(1) 화면을 업데이트하려면 사용자가 어떤 행동을 했는지를 나타내는 action 객체를 만들어 dispatch에 전달한다.
(2) 그러면 React는 현재 state와 action을 reducer 함수에 넘기고, reducer 함수가 새로운 state를 계산해 반환한다.
React는 이 새로운 state를 저장한 뒤 컴포넌트를 다시 렌더링하여 화면을 업데이트한다.
👌🏻
useReducer는useState와 매우 유사하지만, state 업데이트 로직을 이벤트 핸들러에서 컴포넌트 외부의 단일함수로 분리할 수 있다는 차이점이 있다. (useState와useReducer비교하기 참고)
function createInitialState(username) {
// 무거운 연산 예: DB 조회, 큰 배열 생성 등
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, createInitialState(username));
}
createInitialState(username)은 사실 처음 렌더링할 때만 필요한데, 위의 코드처럼 작성하면 렌더링할 떄마다 함수가 계속 실행된다.
즉, 불필요한 성능 낭비가 발생!
이를 해결하기 위해서는 “초기화 함수를 전달”하면 된다.
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
}
여기에서는 createInitialState(username) 실행 결과를 주는 게 아니라, reateInitialState라는 함수 자체를 넘겨준다.
그러면 React가 최초 렌더링에서만 createInitialState(username)을 실행하고, 이후 렌더링에서는 절대 다시 실행하지 않는다.
✅ 기억할 점은
createInitialState()처럼 호출해서 넘기지 말고, createInitialState 함수 자체를 세 번째 인수로 넘겨야 한다.null을 넣어도 된다.state는 사진(snapshot) 같은 것으로, dispatch를 해도 지금 실행 중인 함수 안에서는 옛 사진만 볼 수 있다. 새 값은 다음 렌더링 때 반영된다.
그래서 console.log(state)를 dispatch 바로 뒤에 찍으면, 여전히 예전 값이 보이는 게 정상이다!
setTimeout으로 늦춰 찍어도 그 콜백이 캡처한 건 여전히 옛 스냅샷일 수 있다.
이럴때는 “다음 값”을 확인하고 싶다면 직접 계산해야 한다.
const action = { type: 'incremented_age' };
const nextState = reducer(state, action);
console.log(nextState); // 예상되는 다음 값
dispatch(action);
또는 useEffect로 state 변경을 관찰해서 그때 로그/사이드이펙트 처리.
dispatch 했는데 화면이 안 바뀌네요..리듀서 안에서 기존 객체/배열을 직접 수정(mutate)하고 같은 참조를 반환하면, React는 “값이 안 바뀌었네”라고 판단해 렌더를 건너뛴다.
// ❌ 나쁨 (변이)
state.age++;
return state;
이럴 때는 새로운 객체 혹은 배열을 만들어 반환하면 된다.
// ✅ 좋음 (불변 업데이트)
return { ...state, age: state.age + 1 };
// 배열이면: return [...state.todos, newTodo]
dispatch 이후 undefined가 됩니다다음 state를 만들 때 기존 필드를 안 복사했기 때문이다.
// ❌ name이 날아감
return { age: state.age + 1 };
스프레드로 기존 필드를 복사한 뒤 바꿀 것만 덮어쓰기
// ✅
return { ...state, age: state.age + 1 };
undefined가 돼요어떤 case에서 return을 뺴먹었거나, action.type이 오타 혹은 미처리라 switch 밖으로 흘러나간다.
이럴 때는 마지막에 안전장치 추가 + 모든 case에서 반드시 return
switch (action.type) {
case 'incremented_age': return { ...state, age: state.age + 1 };
// ...
default:
throw Error('Unknown action: ' + action.type);
}
렌더링 중에 dispatch를 호출해서 렌더→dispatch→렌더… 무한 반복.
// ❌ 핸들러를 호출해버림 (렌더 타이밍에 dispatch 발생)
<button onClick={handleClick()}>Click</button>
함수 참조를 넘기거나, 조건 기반 갱신은 useEffect에서
// ✅
<button onClick={handleClick}>Click</button>
// 혹은
<button onClick={(e) => handleClick(e)}>Click</button>
Strict Mode의 의도적인 스트레스 테스트. 순수하지 않은 코드(변이, 숨은 부작용)를 잡으려는 장치라 개발 환경에서만 발생, 배포에선 한 번만 호출.
리듀서는 순수 함수여야 한다. 같은 입력 → 같은 출력, 부작용/변이 금지
// ❌ 변이
state.todos.push(newTodo);
return state;
// ✅ 불변
return { ...state, todos: [...state.todos, newTodo] };