상세 컨텐츠

본문 제목

[React.js 1팀] 7장. useReducer와 상태 관리

24-25/React.js 1

by mingging17 2025. 1. 10. 10:00

본문

728x90

 

1. useReducer 이해하기

useReducer컴포넌트에서 상태 변화 코드를 분리할 때 유용한 리액트 훅이다. 

 

1 - (1). 실습 준비하기

상태 변화 개념을 이해하기 전, 실습을 통해 useReducer의 기능을 알아보자. [할 일 관리] 앱을 열고 component 폴더에 TestComp 컴포넌트를 만든다.

 

TestComp.js 파일의 내용은 다음과 같다.

import {useState} from "react";

function TestComp() {
    const [count, setCount] = useState(0);

    const onIncrease = () => {
        setCount(count+1);
    }

    const onDecrease = () => {
        setCount(count-1);
    }

    return (
        <div>
            <h4>테스트 컴포넌트</h4>
            <div>
                <bold>{count}</bold>
            </div>
            <div>
                <button onClick={onIncrease}>+</button>
                <button onClick={onDecrease}>-</button>
            </div>
        </div>
    )
}

export default TestComp;

TestComp 컴포넌트는 임의의 수를 표시하는 카운트와 이 값을 1씩 증감하는 버튼 2개로 구성한다. 카운트는 State 변수 count를 사용해 관리하는데, + 버튼을 누르면 함수 onIncrease를 호출해 1를 증가시키고, - 버튼을 누르면 onDecrease를 호출해 1 감소시킨다.

 

이제, 이 TestComp 컴포넌트를 App 컴포넌트의 자식으로 배치해 페이지에 랜더링하자.

 

(...)
import TestComp from './component/TestComp';

(...)

function App() {
  (...)
  return (
    <div className="App">
      <TestComp/>
      (...)
    </div>
  );
}

export default App;

 

1 - (2). 상태 변화 코드란?

 

상태 변화 코드State 값을 변경하는 코드로, 앞서 만든 TestComp 컴포넌트에서 함수 onIncrease와 onDecrease 모두 count 값을 증감하므로 상태변화 코드라 볼 수 있다.

 

상태 변화 코드를 컴포넌트에서 분리한다는 말은 컴포넌트 내부에 작성했던 상태 변화 코드를 외부에 작성한다는 뜻이다. 그러나, 지금처럼 useState를 이용해 State를 생성하면 상태 변화 코드는 반드시 컴포넌트 안에 작성해야 하며, 분리할 수 없다. 반면 리액트 훅 useReducer을 사용하면 상태 변화 코드의 분리가 가능하다.

 

상태 변화 코드의 분리는 코드의 가독성을 높이고 유지 보수를 용이하게 만들기 위해 필요하다.

 

1 - (3). useReducer의 기본 사용법

useReducer은 useState과 같이 컴포넌트에서 State를 관리하는 리액트 훅이다. 차이점은 useReducer의 경우, State 관리를 컴포넌트 내부가 아닌 외부에서 할 수 있게 만들기 때문에 상태 변화 코드와 컴포넌트의 분리를 가능하게 한다.

 

이제, useReducer을 이용해 상태 변화 코드를 컴포넌트로부터 분리하자.

 

다음과 같이 TestComp에서 useState로 만든 기능을 모두 제거한다.

function TestComp() {

    return (
        <div>
            <h4>테스트 컴포넌트</h4>
            <div>
                <bold>0</bold>
            </div>
            <div>
                <button>+</button>
                <button>-</button>
            </div>
        </div>
    )
}

export default TestComp;

 

그 후 useState를 이용해 만들었던 카운트 기능을 useReducer을 이용해 구현하겠다. 먼저 TestComp 컴포넌트를 다음과 같이 수정한다.

import { useReducer } from "react"; // 1

function reducer() {} // 2

function TestComp() {
    const [count, dispatch] = useReducer(reducer, 0); // 3
	(...)
}

export default TestComp;
  1. useReducer을 사용하기 위해 react 라이브러리에서 불러옴.
  2. 새로운 함수 reducer을 컴포넌트 밖에 생성.
  3. useReducer을 호출하고 2개의 인수를 전달. 첫번째 인수 = 함수 reducer, 두번째 인수 = State의 초깃값.

useReducer도 useState처럼 배열을 변환하는데, 배열의 첫 번째 요소(해당 코드에서는 count)는 State 변수이고 두 번째 요소는 상태 변화를 촉발하는 함수 dispatch이다.

 

그 다음, 현재의 State 값을 담은 count를 페이지에 랜더링한다.

import { useReducer } from "react";

function reducer() {}

function TestComp() {
    const [count, dispatch] = useReducer(reducer, 0);

    return (
        <div>
            <h4>테스트 컴포넌트</h4>
            <div>
                <bold>{count}</bold>
            </div>
            <div>
                <button>+</button>
                <button>-</button>
            </div>
        </div>
    )
}

export default TestComp;

 

이제 버튼을 클릭하면 카운트를 증감하는 기능을 만들어야 한다. 다음과 같이 버튼을 클릭했을 때 함수 dispatch를 호출하도록 onClick 이벤트 핸들러를 설정한다.

import { useReducer } from "react";

function reducer() {}

function TestComp() {
    const [count, dispatch] = useReducer(reducer, 0);

    return (
        <div>
            <h4>테스트 컴포넌트</h4>
            <div>
                <bold>{count}</bold>
            </div>
            <div>
                <button onClick={() => dispatch({type: "INCREASE", data: 1})}>+</button> //1
                <button onClick={() => dispatch({type: "DECREASE", data: 1})}>-</button> //2
            </div>
        </div>
    )
}

export default TestComp;

1, 2 모두 버튼을 클릭하면 상태 변화를 촉발하는 함수 dispatch를 호출하고 인수로 객체를 전달한다. 이 객체에는 State의 변경 정보를 담고 있다. 이를 다른 표현으로 'action 객체'라고도 한다.

 

객체의 type 프로퍼티는 어떤 상황이 발생했는지를 나타내고, data 프로퍼티는 상태 변화에 필요한 값을 의미한다. + 버튼의 경우, type에는 증가를 의미하는 INCREASE를, 증가값은 1이므로 data에는 1을 넣은 것이다.

 

그러나, 아직은 버튼을 클릭해도 카운트 값의 변화가 없다. 그 이유는 실제 상태 변화는 함수 reducer에서 일어나기 때문이다. dispatch를 호출하면 함수 reducer이 실행되고, 이 함수가 반환하는 값이 새로운 State 값이 되는 것이다.

 

따라서 다음과 같이 TestComp에서 함수 reducer을 작성한다.

import { useReducer } from "react";

function reducer(state, action) { // 1
    switch (action.type) {
        case "INCREASE": // 2
            return state + action.data;
        case "DECREASE": // 3
            return state - action.data;
        default: // 4
            return state;
    }
}

function TestComp() {
    const [count, dispatch] = useReducer(reducer, 0);

    return (
        <div>
            <h4>테스트 컴포넌트</h4>
            <div>
                <bold>{count}</bold>
            </div>
            <div>
                <button onClick={() => dispatch({type: "INCREASE", data: 1})}>+</button>
                <button onClick={() => dispatch({type: "DECREASE", data: 1})}>-</button>
            </div>
        </div>
    )
}

export default TestComp;
  1. 함수 reducer에는 2개의 매개변수가 있음. 첫번째 state에는 현재 State 값이, 두번째 action에는 함수 dispatch를 호출하면서 인수로 전달한 action 객체가 저장됨.
  2. 함수 reducer가 반환하는 값 = 새로운 State 값. (기존 State 값 + data) 반환.
  3. (기존 State 값 - data) 반환.
  4. action 객체의 type이 INCREASE, DECREASE도 아닌 경우 state 값을 그대로 반환하므로 아무런 상태 변화가 일어나지 않음.

이제 + 버튼, - 버튼을 누르면 카운트가 변화하는 것을 확인할 수 있다.

 

useReducer은 함수 reducer을 이용해 상태 변화 코드를 컴포넌트 외부로 분리하므로, 새로운 상태 변화가 필요할 경우 함수 reducer를 수정해 대응하면 된다. 다음은 TestComp에 초기화 버튼을 추가한 코드이다.

import { useReducer } from "react";

function reducer(state, action) {
    switch (action.type) {
    	(...)
        case "INIT": // 1
            return 0;
        default:
            return state;
    }
}

function TestComp() {
    const [count, dispatch] = useReducer(reducer, 0);

    return (
        <div>
            <h4>테스트 컴포넌트</h4>
            <div>
                <bold>{count}</bold>
            </div>
            <div>
                (...)
                <button onClick={() => dispatch({type: "INIT"})}>초기화</button> // 2
            </div>
        </div>
    )
}

export default TestComp;
  1. 함수 reducer에서 카운트 값을 초기화하는 새로운 case를 추가.
  2. 카운트 초기화 버튼 생성.

지금까지 useReducer을 이용한 State 관리 방법을 알아보았다. TestComp는 이후 실습에선 사용하지 않으니 App 컴포넌트에 작성한 관련 코드는 주석 처리하자.

 

2. [할 일 관리] 앱 업그레이드

함수 useReducer로 상태 변화 코드를 컴포넌트로부터 분리해 앱을 업그레이드 하자.

 

2 - (1). useState를 useReducer로 바꾸기

[할 일 관리] 앱에서 useState를 이용해 관리했던 App 컴포넌트의 State를 useReducer로 변경하자.

 

App.js 파일을 다음과 같이 수정한다.

import { useReducer, useRef } from "react"; // 1
(...)

function reducer(state, action) { // 2
  // 상태 변화 코드
  return state;
}

function App() {
  const [todo, dispatch] = useReducer(reducer, mockTodo); // 3
  (...)
}

export default App;
  1. useReducer을 react 라이브러리에서 불러오고, 기존의 useState 코드는 모두 삭제함.
  2. 함수 reducer는 매개변수로 저장한 state를 지금은 그대로 반환하도록 작성. 추후 수정 예정.
  3. 기존의 useState를 삭제하고 useReducer로 대체. 인수로 함수 reducer와 mockTodo를 초깃값으로 전달.

여기까지 진행했다면, setTodo가 사라져 오류가 발생할 것이다. 앞으로 상태 변화가 있을 때 setTodo 대신 dispatch를 호출할 것이다. 따라서 App 컴포넌트에서 함수 setTodo를 호출하는 코드를 모두 제거하자.

(...)
function reducer(state, action) {
  return state;
}

function App() {
  const [todo, dispatch] = useReducer(reducer, mockTodo);
  const idRef = useRef(3);

  const onCreate = (content) => {
    idRef.current += 1;
  };

  const onUpdate = (targetId) => {

  };

  const onDelete = (targetId) => {

  };
  
  (...)
}

export default App;

onCreate, onUpdate, onDelete에 작성했던 함수 setTodo를 모두 삭제한다.

 

2- (2). Create: 할 일 아이템 추가하기

onCreate에서 dispatch를 호출하고, 인수로 할 일 정보를 담은 action 객체를 전달한다.

 

App 컴포넌트를 다음과 같이 수정한다.

(...)
function App() {
  (...)
  const onCreate = (content) => {
    dispatch({ // 1
      type: "CREATE", // 2
      newItem: { // 3
        id: idRef.current, 
        content,
        isDone: false,
        createDate: new Date().getTime(), 
      },
    });
    idRef.current += 1;
  };
  (...)
}

export default App;
  1. 새 할 일 아이템 생성을 위해 함수 dispatch 호출.
  2. 할 일을 추가할 것이므로 type = "CREATE".
  3. newItem에 추가할 데이터 설정.
(...)

function reducer(state, action) {
  switch (action.type) {
    case "CREATE": {
      return [action.newItem, ...state]; // 1
    }
    default:
      return state;
  }
}
(...)

함수 reducer 코드도 위와 같이 변경하자. 호출 시에 기존의 배열에 새로운 아이템을 추가한다.

 

2 - (3). Update: 할 일 아이템 수정하기

(...)
const onUpdate = (targetId) => {
    dispatch({
      type: "UPDATE",
      targetId,
    })
  };
(...)

type에는 수정을 의미하는 UPDATE를, targetId에는 체크 여부로 수정할 아이템의 id 설정한다.

 

function reducer(state, action) {
  switch (action.type) {
    case "CREATE": {
      return [action.newItem, ...state];
    }
    case "UPDATE": {
      return state.map((it) =>
        it.id === action.targetId
          ? {
              ...it,
              isDone: !it.isDone,
            }
          : it
      );
    }

    default:
      return state;
  }
  return state;
}

map 서드로 순회하며 매개변수 state에 저장된 아이템 배열에서 action.targetID와 id를 비교해 일치하는 아이템의 isDone을 토글한 새 배열 반환.

 

2 - (4). Delete: 할 일 삭제 구현하기

(...)
const onDelete = (targetId) => {
      dispatch({
        type: "DELETE",
        targetId,
      })
  };
 (...)

할 일을 삭제하는 함수 onDelete를 위와 같이 수정한다. 

 

그 후 다음과 같이 함수 reducer에 case를 추가해준다.

(...)
    case "DELETE": {
      return state.filter((it) => it.id !== action.targetId);
    }
(...)

action.type이 DELETE일 때, filter 서드로 id와 targetId를 비교하여 일치하는 할 일 아이템만 제외한 배열을 생성하여 반환한다.

 

이로써 기존의 useState를 사용해 구현한 [할 일 관리] 앱을 useReducer을 사용해 구현하였다. 실제 실무에서 State를 관리할 때에 간단한 경우에는 useState를, 복잡한 경우에는 useReducer을 사용한다고 하니, 리액트 훅을 적절히 선택하여 사용해 보자.

 


출처 :  이정환, 『한 입 크기로 잘라먹는 리액트』, 프로그래밍인사이트(2023).

Corner React.js 1

Editor: Mingging

728x90

관련글 더보기