UMC 8th Web 워크북

🍎 useReducer를 왜 쓸까

minnote29 2025. 5. 29. 00:20

useState와 다른 게 무엇이 있는지 확인하기 위해서 useReducerPage를 만들어 볼 것이다.

import { useState } from "react";

export default function UseReducerPage() {
    const [count, setCount] = useState(0);

    const handleIncrease = () => {
        setCount(count + 1);
    };
    return (
        <div>
            <h1>{count}</h1>
            <button onClick={handleIncrease}>Increase</button>
        </div>

    );
}

 이 코드를 비교하면서 차이점을 보려고 한다. 

일단, useState와 useReducer 둘 다 상태를 관리하고 업데이트를 시키는 훅이기는 하다. 근데 좀 다른 점은 리덕스 트리키 이전에 리덕스 패턴을 사용하는 것을 useReducer라고 한다. 

 

useReducer 같은 경우

첫 번째 초기값을 반환한다. 그리고 dispatch set 함수랑 동일한 역할을 한다. (값을 변경시켜주는 리듀서 함수.) 

dispatch 같은 경우에는 안에 뭔가 액션 함수들을 정의해서 액션을 기반으로 이 상태에 대해서 어떤 작업을 하기 위해서 그냥 값을 변경시키는게 아니라 이를 복사한 복사본 형태를 스프레드 연산자를 활용해서 만들어주고 몇 가지를 변경한 다음에 상태의 사본으로 다시 돌아가서 새로운 상태를 만드는 즉, 불변성 이뮤터블을 중요하게 여기는 패턴이라고 보면 된다. -> 데이터를 직접 변형해서는 안 되고, 새로운 사본 상태를 만들어서 그 값을 변이시키는 그런 형태를 반환해야 된다고 알아 두면 된다. 

useReducer는 리액트에서 제공을 해준다.

handleIncrease는 count에 연결이 돼있고 useReducer의 state로 봐도 무관하다. counter 값을 증가시켜 주려면 이 dispatch를 활용해서 값을 증가 시켜 줘야 하는데 그것을 onClick을 통해 value를 전달해 줄 수 있는데 이 value 같은 경우는 중괄호를 통해 타입을 불러올 수 있다. switch를 통해서 case: 'INCREASE'를 적어주고 counter를 증가시켜 줄거다 라고 지정하면 아마 되긴 할 것이다. 근데 reducer에서 중요한 것은 원본 배열을 유지시키고 항상 사본을 조작해야 한다. 현재 코드에서는 문제가 일어나지 않을 수 있지만, error를 스트링으로 받고 널이라는 값이 있다고 해보자. 그럼 기존 코드는 이 에러에 대한 것을 건드릴 일은 없지만 에러에 대한 값을 잃어버리게 되서 항상 state를 통해서 원본 값은 항상 유지시켜 줘야 한다.

 

콘솔을 찍어서 확인을 해보면, 에러는 null로 찍히는데 increase를 누르는 경우 error 값에 대해서 잃어버리게 된다. 이렇게 되면 필요한 순간 error 값을 참고할 수 없게 된다. -> 원본 데이터를 잃어버리게 됨. 

항상 중요한 점 -> 값을 mutate, 변경을 시켜 줄 때는 항상 원본 배열을 유지시켜 줘야 한다. 그걸 이제 스프레드 연산자를 통해서 구현 해주면 새로고침을 했을 때 increase를 하더라도 항상 원본 값을 유지를 할 수 있다.

case "INCREASE": {
            return {
                ...state,
                counter: state.counter + 1,
            };
        }

이런 식으로 ...state로 조작을 해야 하는 것을 인지하고 있어야 한다. 이를 편리하게 해주기 위해서 immer라는 라이브러리를 사용할 수 있다. immer를 다운 받으면 원본 값을 기존과는 다르게 값을 푸쉬만 해도 원본 값을 유지할 수 있게 해준다. 

암튼, 원본 값을 유지하는 것이 중요하다는 것을 키 포인트로 알고 가자.

 

이어서, 한 가지 더 알아야 하는 패턴이 있는데 increase 할 때 1만 업그레이드 하는 것이 아니라 내가 준 값 (임의로 적용한 값) 을 넘겨줘야 할 때도 있을 것이다. 일단, action을 보면 동작이 일어나면 타입에 대한 정보 밖에 없지만 사실은 이 액션에 대해서는 페이로드 또한 받아볼 수 있다. 이 페이로드가 전달할 값들을 의미한다. 그래서 페이로드는 number 이런 식으로 정의를 해주면 타입을 통해서 값을 넘기는 과정에서 이 값을 넘길 때 페이로드를 3 이렇게 지정해서 넘겨 줄 수 있다. 원하는 값을 dispatch에 넘겨 주고 이에 대한 값을 페이로드를 통해 받아 볼 수 있다. 그리고 counter는 + payload를 해주면 increase를 눌렀을 때 3씩 증가를 할 수 있다. -> increase 할 때 페이로드를 전달해주니까 3씩 증가.

<button onClick={() => dispatch({
                        type: "INCREASE",
                        payload: 3,
                    })
                }>Increase</button>

다른 코드는 payload를 따로 지정을 안 해줬기 때문에 NaN이 나올 것이다.

import { useReducer, useState } from "react";

// 1. state에 대한 interface 
interface IState {
    counter: number;
}

// 2. reducer에 대한 interface
interface IAction {
    type: "INCREASE" | "DECREASE" | "RESET_TO_ZERO";
    payload?: number; 
}

function reducer(state: IState, action: IAction) {
    const { type, payload } = action;

    switch (type) {
        case "INCREASE": {
            return {
                ...state,
                counter: state.counter + payload,
            };
        } 
        case "DECREASE": {
            return {
                ...state,
                counter: state.counter - payload,
            };
        }
        case "RESET_TO_ZERO": {
            return {
                ...state,
                counter: 0,
            };
        }
        default:
            return state;
    }
}

export default function UseReducerPage() {
    // 1. useState
    const [count, setCount] = useState(0);
    
    // 2. useReducer
    const [state, dispatch] = useReducer(reducer, {
        counter: 0,
    });

    const handleIncrease = () => {
        setCount(count + 1);
    };
    return (
        <div className="flex flex-col gap-10">
            <div>
                <h2 className="text-3xl">UseState</h2>
                <h2>useState훅 사용: {count}</h2>
                <button onClick={handleIncrease}>Increase</button>
            </div>
            <div>
                <h2 className="text-3xl">UseReducer</h2>
                <h2>useReducer훅 사용: {state.counter}</h2>
                <button onClick={() => dispatch({
                        type: "INCREASE",
                        payload: 3,
                    })
                }>Increase</button>
                <button onClick={() => dispatch({
                        type: "DECREASE",
                    })
                }>Decrease</button>
                <button onClick={() => dispatch({
                        type: "RESET_TO_ZERO",
                    })
                }>Reset</button>
            </div>
        </div>
    );
}

 

다음은 예제를 만들어봤다. 기본에는 error, setError 이런 식으로 해서 일일이 처리해줘야 했을 것이다. 하나의 상태면 상관없는데 여러 개의 상태를 고려해서 관리를 해야 될 때 큰 문제가 될 것이다. 여러 개의 조건들을 처리하기 위해서 reducer 패턴을 활용하면 좋다. 어떤 식으로 처리할 수 있냐면 아래 switch 문을 이용해서 처리할 수 있다. 

import { ChangeEvent, useReducer, useState } from "react";

type TActionType = "CHANGE_DEPARTMENT" | "RESET";;

interface IState {
    department: string;
    error: string | null;
}

interface IAction {
    type: TActionType;
    payload?: string;
}

function reducer(state: IState, action: IAction): IState {
    const { type, payload } = action;

    switch (type) {
        case "CHANGE_DEPARTMENT": {
            const newDepartment = payload;
            const hasError = newDepartment !== '카드메이커';
            return {
                ...state,
                department: hasError ? state.department : newDepartment,
                error: hasError ? "거부권 행사가능, 카드메이커만 입력 가능합니다." : null,
            };
        }
        default:
            return state;
    }
}

export default function UseReducerCompany() {
    const [state, dispatch] = useReducer(reducer, {
        department: "Software Developer",
        error: null,
    });

    const [department, setDepartment] = useState("");
    const handleChangeDepartment = (e: ChangeEvent<HTMLInputElement>) => {
        setDepartment(e.target.value);
    }
    
    return (
        <div>
            <h1>{state.department}</h1>
            {state.error && <p className="text-red-500 font-2xl">{state.error}</p>} 

            <input 
                className="w-[600px] border mt-10 p-4 rounded-md"
                placeholder="변경하시고 싶은 직무를 입력하세요. 단, 거부권 행사 가능"
                value={department} 
                onChange={handleChangeDepartment}
            /> 

            <button onClick={() => 
                dispatch({
                        type: "CHANGE_DEPARTMENT",
                        payload: department,
                    })
                } 
            /> 
        </div>
    )
}
// 페이로드 값을 받아서 인풋으로 보여주는 코드