ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Kent C. dodds - Epic React] Advanced React Hooks(1) - useReducer 이해하기
    Frontend 2020. 11. 8. 18:06

    useReducer

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

     

     

    대부분 useState hook으로 리액트의 상태 관리를 하게 되는데, 가끔 상태 로직을 상태 변화가 일어나는 컴포넌트로부터 분리시키고 싶을 때 useReducer를 유용하게 사용할 수 있다. useReducer는 주로 복잡한 상태 로직을 가졌거나 다음 state가 기존 state에 의존성을 가질 때 사용된다. 또한 useReducer를 사용하면 하위 컴포넌트에게 callback 함수가 아니라 dispatch 함수를 넘겨줄 수 있어 유지보수 관점에서 매우 편리해진다. (하위 컴포넌트들에게 계속 prop으로 전달, 전달, 전달을 할 필요가 없어지니까!)

     

    사실 Redux의 주요 작동 원리 중 하나라서 만약에 Redux를 자주 사용했다면 useReducer가 어떻게 동작하는지 쉽게 이해할 수 있을 것이다. 보통 useReducer는 state 객체와 함께 사용되지만 좀 더 쉽게 이해하기 위해서 Epic React 강의에 나온대로 single value와 함께 사용한 예제를 살펴보자~!

     

     


    import React, { useState } from "react";
    
    const initialName = "Joe";
    
    export default function App() {
      const [name, setName] = useState(initialName);
      const handleChange = (e) => setName(e.target.value);
    
      return (
        <div className="App">
          <label>
            Name:
            <input defaultValue={initialName} onChange={handleChange} />
          </label>
          <p>Hello,My name is {name}</p>
        </div>
      );
    }
    

     

    엄청나게 간단한 앱이다. input 창에 이름을 치면 아래 해당 이름에 따라 글자가 바뀌는데,

    useState hook을 useReducer hook으로 바꾸면 아래와 같이 된다.

     

    import React, { useReducer } from "react";
    
    const nameReducer = (prevName, newName) => newName;
    const initialName = "Joe";
    
    export default function App() {
      const [name, setName] = useReducer(nameReducer, initialName);
      const handleChange = (e) => setName(e.target.value);
    
      return (
        <div className="App">
          <label>
            Name:
            <input defaultValue={initialName} onChange={handleChange} />
          </label>
          <p>Hello,My name is {name}</p>
        </div>
      );
    }
    

     

    위 코드는 아까 useState hook을 사용한 것과 정확히 동일한 기능을 한다.

    여기서 중요하게 살펴봐야할 것은, useReducer에 첫 번째 인자로 넣은 nameReducer 함수의 2개의 arguments다.

     

    1. prevName: 기존의 state값이다.

    2. newName: dispatch 함수(위의 경우에 setName)가 호출될 때의 state 값이다. 주로 이것을 'action'이라고 부른다.

     

    이제 위 앱에서 input의 값이 바뀔 때마다 다음과 같은 흐름으로 실행된다.

     

    - input의 value가 바뀔 때마다 setName(dispatch)이 바뀐 input value값과 함께 실행된다.

    - setName(dispatch)이 실행되면, nameReducer가 실행되는데, 첫 번째 인자로 기존의 name값이 들어오고, 두 번째 인자로는 바뀐 input value값이 들어온다.

    - reducer 함수가 바뀐 input value을 return 하므로 해당 값으로 state가 업데이트된다.

     


    import React, { useReducer } from "react";
    import "./styles.css";
    
    const countReducer = (prevState, step) => prevState + step;
    const initialValue = 0;
    
    export default function App() {
      const [count, setCount] = useReducer(countReducer, initialValue);
      const increment = () => setCount(1);
    
      return (
        <div className="App">
          <p>{count}</p>
          <button onClick={increment}>increment</button>
        </div>
      );
    }
    

     

    이제 increment 버튼을 클릭하면 화면의 숫자가 하나씩 증가하는 앱을 만들어보자.

    reducer 함수를 통해 기존 state에 dispatch의 인자로 넘기는 숫자를 더한 값을 update하도록 만들었다.

     

    즉, 버튼을 클릭하면 dispatch가 실행되는데,

    dispatch와 함께 실행되는 1과 함께 countReducer가 실행되고

    원래 state에 1이 더해진 값이 새로운 상태로 업데이트된다.

     

     

    클래스 컴포넌트의 setState와 똑같은 state updater 함수 만들기

    이제 useReducer의 초기 값을 object로 바꾸고, object state를 업데이트하는 함수를 useReducer로 만들어보자.

    (클래스 컴포넌트를 사용해봤다면 this.setState 함수를 통해 state 객체를 통째로 업데이트해본 적이 있을 것이다.)

     

    import React, { useReducer } from "react";
    import "./styles.css";
    
    const countReducer = (prevState, newState) => ({ ...prevState, ...newState });
    const initialValue = { count: 0 };
    
    export default function App() {
      const [state, setState] = useReducer(countReducer, initialValue);
    
      const { count } = state;
      const increment = () => setState({ count: count + 1 });
    
      return (
        <div className="App">
          <p>{count}</p>
          <button onClick={increment}>increment</button>
        </div>
      );
    }
    

     

    위와 같이 만들면 된다!

    initialValue는 count라는 property와 초기값 0을 가진 객체이다.

    이제, setState라는 dispatch 함수를 실행시킬 때마다 count 값에 1을 더하는 action 객체와 함께 호출한다.

    그러면 countReducer 함수에서 기존 state객체에 새 state 객체를 합치면서 상태값이 업데이트된다.

     

    이렇게 만들면 클래스 컴포넌트의 setState와 똑같은 기능을 하는 함수가 만들어진다.

    그런데 알고있다시피 setState는 객체 뿐만 아니라 함수를 인자로 넘길 수 있어야 한다.

     

    setState(currentState => ({ count: currentState + 1 }));

     

    이렇게 함수를 넣었을때도 동일하게 작동하려면 어떻게 해야할까?

     

    import React, { useReducer } from "react";
    import "./styles.css";
    
    const countReducer = (state, action) => ({
      ...state,
      ...(typeof action === "function" ? action(state) : action)
    });
    const initialValue = { count: 0 };
    
    export default function App() {
      const [state, setState] = useReducer(countReducer, initialValue);
    
      const increment = () =>
        setState((currentState) => ({ count: currentState.count + 1 }));
    
      const { count } = state;
    
      return (
        <div className="App">
          <p>{count}</p>
          <button onClick={increment}>increment</button>
        </div>
      );
    }
    

     

    countReducer를 위와 같이 바꾸면 된다! 넘나 간단하다 🎉

     

    action 인자의 type이 function이면 state를 인자로 넣어서 실행시킨 후,

    리턴되는 객체를 state에 합쳐서 업데이트를 하고,

    type이 객체면 그냥 바로 state에 업데이트하면 된다.

     

     

    Redux 스타일의 reducer 만들어보기

    이제 redux 라이브러리를 쓰면서 많이 봐왔던 dispatch, action, reducer를 만들어보도록 하자.

     

    import React, { useReducer } from "react";
    import "./styles.css";
    
    const countReducer = (state, action) => {
      switch (action.type) {
        case "INCREMENT":
          return {
            ...state,
            count: state.count + action.step
          };
        default:
          throw new Error("Not a valid action type");
      }
    };
    const initialState = { count: 0 };
    
    export default function App() {
      const [state, dispatch] = useReducer(countReducer, initialState);
      const step = 1;
    
      const increment = () => {
        dispatch({ type: "INCREMENT", step });
      };
    
      const { count } = state;
    
      return (
        <div className="App">
          <p>{count}</p>
          <button onClick={increment}>increment</button>
        </div>
      );
    }
    

     

    reducer 함수에 switch 구문을 써서 action으로 들어오는 객체의 type에 따라

    새로운 state를 리턴하도록 만들어준다.

     

    action 객체의 타입이 'INCREMENT'라면

    기존 state 객체의 count에 action 객체로 들어오는 step value를 더해주도록 했다.

     

    이제 INCREMENT라는 타입과 step 객체와 함께 dispatch 함수를 실행시키면,

    countReducer에 의해 기존 count 값에 step인 1을 더한 값이 든 새로운 객체가 새로운 state로 업데이트된다.

     

     

    import React, { useReducer } from "react";
    import "./styles.css";
    
    const countReducer = (state, action) => {
      switch (action.type) {
        case "INCREMENT":
          return {
            ...state,
            count: state.count + action.step
          };
        case "DECREMENT":
          return {
            ...state,
            count: state.count - action.step
          };
        default:
          throw new Error("Not a valid action type");
      }
    };
    
    const initialState = { count: 0 };
    
    export default function App() {
      const [state, dispatch] = useReducer(countReducer, initialState);
      const step = 1;
    
      const increment = () => {
        dispatch({ type: "INCREMENT", step });
      };
    
      const decrement = () => {
        dispatch({ type: "DECREMENT", step });
      };
    
      const { count } = state;
    
      return (
        <div className="App">
          <p>{count}</p>
          <button onClick={increment}>increment</button>
          <button onClick={decrement}>decrement</button>
        </div>
      );
    }
    

     

    countReducer에 여러 가지 type을 추가해서 다양하게 사용할 수 있다!

     

    솔직히 그 동안 redux를 쓰면서도 redux 내부에서 어떤 구조로 움직이는지 완전히 파악하지는 못했는데

    useReducer 동작 원리를 들여다보니 훨씬 이해가 잘 되는 것 같다.

    반응형

    COMMENT