I code, therefore I exist.

웹 프론트 엔드 개발을 공부하고 있는 Ocean이라고 합니다. 만나서 반갑습니다.

WEB/REACT

useReducer

Ocean 2024. 6. 29. 01:50

useReducer는 컴포넌트에 reducer를 추가하는 React Hook입니다.

const [state, dispatch] = useReducer(reducer, initialArg, init?)

 

한 컴포넌트에서 state 업데이트여러 이벤트 핸들러로 분산되는 경우가 있습니다.

const [tasks, setTasks] = useState(initialTasks);

  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를 업데이트하는 모든 로직을 reducer를 사용해 컴포넌트 외부로 단일 함수로 통합해 관리할 수 있습니다.

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);
  }
}

 

따라서 Reducer는 분산된 state 업데이트 로직을 하나의 외부 단일 함수로 통합하여 관리하는 일종의 디자인 패턴이라고 볼 수 있습니다.


기본 규칙

useReducer(reducer, initialArg, init?)

 

useReducer를 컴포넌트의 최상위에 호출하고, reducer를 이용해 state를 관리합니다.

import { useReducer } from 'react';

function reducer(state, action) {
  // ...
}

function MyComponent() {
  const [state, dispatch] = useReducer(reducer, { age: 42 });
  // ...

 

매개변수

reducer : state가 어떻게 업데이트 되는지 지정하는 리듀서 함수입니다. 리듀서 함수는 반드시 순수 함수여야 하며, state와 action을 인수로 받아야 하고, 다음 state를 반환해야 합니다. state와 action에는 모든 데이터 타입이 할당될 수 있습니다.

 

initialArg : 초기 state가 계산되는 값입니다. 모든 데이터 타입이 할당될 수 있습니다. 초기 state가 어떻게 계산되는지는 다음 init 인수에 따라 달라집니다.

 

init(optional) : 초기 state를 반환하는 초기화 함수입니다. 이 함수가 인수에 할당되지 않으면 초기 state는 initialArg로 설정됩니다. 할당되었다면 초기 state는 init(initialArg)를 호출한 결과가 할당됩니다.

 

반환값

useReducer는 2개의 엘리먼트로 구성된 배열을 반환합니다.

 

1. 현재 state. 첫번째 렌더링에서의 state는 init(initialArg) 또는 initialArg로 설정됩니다.

2. dispatch 함수. dispatch는 state를 새로운 값으로 업데이트하고 리렌더링을 일으킵니다.


dispatch 함수?

useReducer에 의해 반환되는 dispatch 함수는 state를 새로운 값으로 업데이트하고 리렌더링을 일으킵니다. dispatch의 유일한 인수는 action입니다.

const [state, dispatch] = useReducer(reducer, { age: 42 });

function handleClick() {
  dispatch({ type: 'incremented_age' });
  // ...

 

매개변수

1. action : 사용자에 의해 수행된 활동입니다. 모든 데이터 타입이 할당될 수 있습니다. 컨벤션에 의해 action은 일반적으로 action을 정의하는 type 프로퍼티추가적인 정보를 표현하는 기타 프로퍼티를 포함한 객체로 구성됩니다.

 

주의 사항

1. dispatch 함수는 오직 다음 렌더링에서 사용될 state 변수만 업데이트합니다. 만약 dispatch 함수를 호출한 직후에 state 변수를 읽는다면 호출 이전의 최신화되지 않은 값을 참조할 것 입니다. 이는 dispatch로 인한 리렌더링이 요청되지만, 이미 실행 중인 이벤트 핸들러에는 영향을 주지 않기 때문입니다.

 

2. Object.is 비교를 통해 새롭게 제공된 값과 현재 state를 비교한 값이 같을 경우, React는 리렌더링을 일으키지 않습니다. 이것은 최적화에 관련된 동작으로써 결과를 무시하기 전에 컴포넌트가 호출되지만, 호출된 결과가 코드에 영향을 미치지는 않습니다.


사용법

1. 컴포넌트 외부에 사용할 reducer를 정의합니다.

2. useReducer를 컴포넌트 최상단에서 호출하여, state와 dispatch를 반환 받습니다.

3. state를 변경하려면 dispatch() 함수에 인수로 특정 동작(action)을 전달합니다.

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>
    </>
  );
}

 

React는 현재 stateactionreducer 함수로 전달(dispatch)합니다. reducer는 다음 state를 계산한 후 반환합니다. React는 다음 state를 저장한 뒤에 컴포넌트와 함께 렌더링하고 UI를 업데이트 합니다.

 

useReducer는 상태를 불러오고 변경하는 useState와 매우 비슷하지만, state의 업데이트 로직을 이벤트 핸들러에서 컴포넌트 외부의 단일 함수로 분리할 수 있다는 차이점이 있습니다. 이를 통해서 state 변경에 대한 동작들을 통합하여 관리합니다.

 

보통은 컨벤션에 따라 switch 문을 사용합니다.

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      return {
        name: state.name,
        age: state.age + 1
      };
    }
    case 'changed_name': {
      return {
        name: action.nextName,
        age: state.age
      };
    }
  }
  throw Error('Unknown action: ' + action.type);
}

 

Actions(dispatch함수에 전달하는 객체 인자)는 다양한 형태가 될 수 있습니다.  하지만 컨벤션에 따라 액션이 무엇인지 정의하는 type 프로퍼티추가적인 정보를 담는 기타 프로퍼티를 포함하여 넘기는 것이 일반적입니다.


주의 사항

reducer의 반환값은 반드시 객체 리터럴이어야 합니다.

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      // 🚩 Don't mutate an object in state like this:
      state.age = state.age + 1;
      return state;
    }

 

위와 같이 전달 받은 state의 내부 프로퍼티를 읽고 변경하여 return 시키면, React는 해당 객체의 참조값은 변화하지 않았기 때문에 리렌더링을 촉발하지 않습니다. 이를 mutation(변이)이라고 하며, React가 state의 변경을 인식하고 올바르게 리렌더링 할 수 있게 객체 리터럴을 반환해 줘야합니다.

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      // ✅ Instead, return a new object
      return {
        ...state,
        age: state.age + 1
      };
    }

 


 

해당 게시글은 리액트 공식 문서를 정리한 글입니다. 추가적인 내용은 아래의 링크를 참고해 주세요. :)

https://ko.react.dev/reference/react/useReducer#usereducer

 

useReducer – React

The library for web and native user interfaces

ko.react.dev

 

'WEB > REACT' 카테고리의 다른 글

forwardRef와 useImperativeHandle  (1) 2024.12.22
useState와 클로저  (2) 2024.12.12
useContext  (0) 2024.06.19
useMemo  (0) 2024.06.19
useCallback  (0) 2024.06.18