[Hook] useReducer

OlMinJe·2025년 9월 2일

React

목록 보기
14/19

리액트 공식 문서를 참고한 정리 내용 (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가 어떻게 바뀔지 정하는 함수
  • (state, action) => newState의 형식으로 순수 함수여야 한다.
  • 같은 입력(state, action)이면 항상 같은 결과(newState)를 반환해야 한다.

initialArg

  • 초기 state를 설정할 값
  • 그냥 처음에 쓰고 싶은 state 값이라고 생각하면 된다.(숫자, 문자열, 객체 다 가능하다

init (선택사항)

  • 초기 state를 만드는 함수
  • init이 없으면 → state = initialArg , 있으면 state = init(initialArg)
  • 주로 초기값을 계산해야 하거나, 복잡한 초기화 로직이 필요할 때 사용한다.

반환값

2개의 엘리먼트로 구성된 배열을 반환한다.

현재 state

  • 지금 컴포넌트가 가지고 있는 상태 값(읽기 전용)
  • 처음에는 initialArg 또는 init(initialArg)로 정해짐

diapatch 함수

  • state를 바꾸고 싶을 때 호출하는 함수
  • dispatch({ type: 'ACTION_NAME', payload: ... }) 같은 식으로 action을 넘겨줌
  • 호출하면 reducer가 실행돼서 새로운 state를 만들고, 컴포넌트가 리렌더링 됨

useReducer의 반환값인 [state, dispatch에서 dispatch(action을 호출하면,

  1. React가 지금 state와 전달한 actionreducer 함수에 넘긴다
  2. reducer가 새로운 state를 계산해서 반환하면,
  3. 그 값을 새로운 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)

  • 이벤트 핸들러 안에서 dispatchsetState를 여러 번 호출해도,
  • React는 바로 리렌더링하지 않고, 전부 모았다가 한 번만 리렌더링을 한다. (이래야 성능이 좋아짐!)
  • 만약 지금 당장 리렌더링 해야하는 특수한 상황이라면 flushSync를 사용하면 된다.

주의사항

1) 호출 위치

  • useReducer는 다른 Hook과 마찬가지로 컴포넌트 최상위커스텀 Hook에서만 사용해야 함.
  • 반복문, 조건문 안에서는 ❌ → 필요하다면 새로운 컴포넌트로 분리해서 써야 함.

2) dispatch 함수

  • dispatch는 항상 변하지 않는 안정된 함수라서,
  • 의존성 배열에 넣어도 Effect가 다시 실행되지 않음.

3) Strict Mode

  • React가 버그를 잡으려고 reducerinit 함수를 두 번 실행하며, 결과 중 하나는 버려져서 실제 동작에는 영향 없다.
  • 단, 순수 함수여야 문제가 생기지 않음 (→ 같은 입력 → 항상 같은 출력)

사용법

컴포넌트에 reducer 추가하기

컴포넌트 맨 위에서 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를 저장한 뒤 컴포넌트를 다시 렌더링하여 화면을 업데이트한다.

👌🏻useReduceruseState와 매우 유사하지만, state 업데이트 로직을 이벤트 핸들러에서 컴포넌트 외부의 단일함수로 분리할 수 있다는 차이점이 있다. (useStateuseReducer 비교하기 참고)


초기 state 재생성 방지하기

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을 넣어도 된다.

트러블 슈팅

1) dispatch 했는데 콘솔엔 예전 state가 찍힌다.

state는 사진(snapshot) 같은 것으로, dispatch를 해도 지금 실행 중인 함수 안에서는 옛 사진만 볼 수 있다. 새 값은 다음 렌더링 때 반영된다.

그래서 console.log(state)dispatch 바로 뒤에 찍으면, 여전히 예전 값이 보이는 게 정상이다!

setTimeout으로 늦춰 찍어도 그 콜백이 캡처한 건 여전히 옛 스냅샷일 수 있다.
이럴때는 “다음 값”을 확인하고 싶다면 직접 계산해야 한다.

const action = { type: 'incremented_age' };
const nextState = reducer(state, action);
console.log(nextState); // 예상되는 다음 값
dispatch(action);

또는 useEffectstate 변경을 관찰해서 그때 로그/사이드이펙트 처리.


2) dispatch 했는데 화면이 안 바뀌네요..

리듀서 안에서 기존 객체/배열을 직접 수정(mutate)하고 같은 참조를 반환하면, React는 “값이 안 바뀌었네”라고 판단해 렌더를 건너뛴다.

// ❌ 나쁨 (변이)
state.age++; 
return state;

이럴 때는 새로운 객체 혹은 배열을 만들어 반환하면 된다.

// ✅ 좋음 (불변 업데이트)
return { ...state, age: state.age + 1 };
// 배열이면: return [...state.todos, newTodo]

3) 일부 필드가 dispatch 이후 undefined가 됩니다

다음 state를 만들 때 기존 필드를 안 복사했기 때문이다.

// ❌ name이 날아감
return { age: state.age + 1 };

스프레드로 기존 필드를 복사한 뒤 바꿀 것만 덮어쓰기

// ✅
return { ...state, age: state.age + 1 };

4) 전체 state가 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);
}

5) 5) Too many re-renders 무한 렌더 에러

렌더링 중에 dispatch를 호출해서 렌더→dispatch→렌더… 무한 반복.

// ❌ 핸들러를 호출해버림 (렌더 타이밍에 dispatch 발생)
<button onClick={handleClick()}>Click</button>

함수 참조를 넘기거나, 조건 기반 갱신은 useEffect에서

// ✅
<button onClick={handleClick}>Click</button>
// 혹은
<button onClick={(e) => handleClick(e)}>Click</button>

6) 리듀서/초기화 함수가 두 번 호출된다. (개발 모드)

Strict Mode의 의도적인 스트레스 테스트. 순수하지 않은 코드(변이, 숨은 부작용)를 잡으려는 장치라 개발 환경에서만 발생, 배포에선 한 번만 호출.

리듀서는 순수 함수여야 한다. 같은 입력 → 같은 출력, 부작용/변이 금지

// ❌ 변이
state.todos.push(newTodo);
return state;

// ✅ 불변
return { ...state, todos: [...state.todos, newTodo] };
profile
큐트걸

0개의 댓글