ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [유데미x스나이퍼팩토리] 프로젝트 캠프 : Next.js 2기 - 사전직무교육 2주차 - useReducer
    프로젝트 캠프: Next.js 과정 2기 2024. 7. 26. 02:03
    728x90

    useReducer

    useReducer를 언제 사용하면 좋을까?

    1. 다수의 하윗값을 포함하는 복잡한 정적 로직을 만드는 경우
    2. 다음 상태가 이전 상태에 의존적인 경우

    기본 형태

    const [state, dispatch] = useReducer(reducer, initialArg)
    1. state: useReducer에 의해 관리되는 상태. 상태를 나타내는 직관적인 변수명을 사용하는 것이 좋음
    2. dispatch: state를 업데이트하고 리렌더링을 일으킴
    3. reducer: (state, action) ⇒ {} 형태의 함수. dispatch를 확인해서 state 업데이트하고 state 반환.
    4. initalArg: state에 전달할 초기값. 모든 데이터 타입 할당 가능.
    💡 초기값의 타입은 reducer의 반환값의 타입에 의존한다. reducer는 보통 별도의 함수를 만들어서 관리한다.

     

    사용 방법

    useState를 사용

    박스의 border-color와 border-radius의 값을 설정하는 코드를 작성해봤다.

    import { useState } from 'react';
    import './App.css';
    import Input from './components/Input';
    
    function App() {
      const [style, setStyle] = useState({ borderColor: '', borderRadius: 0 });
    
      const setBorderColor = (e: React.ChangeEvent<HTMLInputElement>) => {
        setStyle({ ...style, borderColor: e.target.value });
      };
    
      const setBorderRadius = (e: React.ChangeEvent<HTMLInputElement>) => {
        setStyle({ ...style, borderRadius: Number(e.target.value) });
      };
    
      const reset = () => {
        setStyle({ borderColor: '', borderRadius: 0 });
      };
    
      return (
        <>
          <div className="flex flex-col gap-3 items-start justify-center h-min-full">
            <Input
              type="color"
              id="borderColor"
              value={style.borderColor}
              onChange={setBorderColor}
            />
    
            <Input
              type="number"
              id="borderRadius"
              min={0}
              value={style.borderRadius}
              onChange={setBorderRadius}
            />
    
            <button onClick={reset}>reset</button>
    
            <div
              style={{
                height: '100px',
                border: '5px solid',
                borderColor: style.borderColor,
                borderRadius: style.borderRadius,
              }}
            />
          </div>
        </>
      );
    }
    
    export default App;
    type TInputProps = React.ComponentPropsWithRef<'input'>;
    
    const Input = ({ id, ...rest }: TInputProps) => {
      return (
        <div className="flex flex-row gap-2">
          <label htmlFor={id}>{id}</label>
          <input id={id} {...rest} />
        </div>
      );
    };
    
    export default Input;
    

    이제 이 코드에 useReduce를 적용해보겠습니다.

     

    useReducer 적용

    reducer 생성

    // src/reducer/borderReducer.ts
    export const reducer = (
      state: TStyle,
      action: { type: string; payload?: string | number }
    ) => {
      if (action.type === 'borderColor') {
        return { ...state, borderColor: action.payload };
      } else if (
        action.type === 'borderRadius') {
        return { ...state, borderRadius: action.payload };
      } else if (action.type === 'reset') {
        return { borderColor: '', borderRadius: 0 };
      } else {
        return state;
      }
    };

    action의 첫 번째 인자인 type은 상태를 업데이트시킬 로직을 구분하기 위해 사용되며, payload는 업데이트시킬 새로운 값을 의미한다.

    // App.tsx
    const [style, dispatch] = useReducer(reducer, {
      borderColor: "",
      borderRadius: 0,
    });

     

    그런데 이때 위와 같이 reducer를 작성한다면 리듀서가 반환하는 상태의 타입과 사용리듀서에 의해 관리되는 상태의 타입이 달라져 타입오류가 발생하게 된다. 타입 오류를 방지하기 위한 방법은 다음과 같다.

     

    타입 가드

    export const reducer = (
      state: TStyle,
      action: { type: string; payload?: string | number }
    ) => {
      if (action.type === 'COLOR' && typeof action.payload === 'string') {
        return { ...state, borderColor: action.payload };
      } else if (
        action.type === 'RADIUS' &&
        typeof action.payload === 'number'
      ) {
        return { ...state, borderRadius: action.payload };
      } else if (action.type === 'RESET') {
        return { borderColor: '', borderRadius: 0 };
      } else {
        return state;
      }
    };

    타입 지정

    export type TStyleAction =
      | { type: 'COLOR'; action: string }
      | { type: 'RADIUS'; action: number }
      | { type: 'RESET' };
    export const reducer = (
      state: TStyle,
      action: TStyleAction
    ) => {
      switch (action.type) {
        case "COLOR":
          return { ...state, borderColor: action.payload };
        case "RADIUS":
          return { ...state, borderRadius: action.payload };
        case "RESET":
          return { borderColor: '', borderRadius: 0 };
        default:
          return state;
      }
    };

    둘 중 더 편하다고 느껴지는 것을 사용하면 된다.

     

    상태 업데이트

    <div className="flex items-start justify-center h-min-full">
      <Input
        type="color"
        id="borderColor"
        value={style.borderColor}
        onChange={(e) => dispatch({ type: "COLOR", payload: e.target.value })}
      />
      <Input
        type="number"
        id="borderRadius"
        min={0}
        value={style.borderRadius}
        onChange={(e) =>
          dispatch({ type: "RADIUS", payload: Number(e.target.value) })
        }
      />
    
      <button onClick={() => dispatch({ type: "RESET" })}>reset</button>
    
      <div
        style={{
          height: "100px",
          border: "5px solid",
          borderColor: style.borderColor,
          borderRadius: style.borderRadius,
        }}
      />
    </div>;

    위와 같이 dispach에 상태 변화 타입을 구분하는 인자와 업데이트할 값을 함께 넘기면 위에서 작성한 reducer 함수 안의 로직에 따라 기능이 동작하게 된다.

    이제 어느정도의 사용방법은 익힌 것 같다. 리덕스를 공부한 적 있어서 그런지 리덕스와 매우 비슷하게 느껴졌고, 그 덕분에 조금 더 빠르게 이해할 수 있었던 것 같다. 더 다양한 예제 코드를 작성하면서 익히는 것이 좋을 것 같다는 생각이 들었다.

     

    실행 코드

    Vitejs - Vite (forked) - StackBlitz

    728x90

    댓글

Designed by Tistory.