goongoguma's blog

The ultimate guide to the React useReducer Hook

useReducer는 리액트 16.8버전에서 추가된 훅입니다. 이 훅은 useState 훅의 대안이며, 리액트 어플리케이션의 복잡한 상태 로직을 관리할 수 있도록 도와줍니다. useContext와 같이 다른 훅과 조합하면 리덕스와 MobX의 좋은 대안이 될 수 있으며 - 어쩔때는 더 나은 선택이 될 수 있습니다.

이 글은 규모가 큰 리액트 어플리케이션에서 전역상태관리를 하기 위한 최고의 옵션인 리덕스와 MobX를 비난하는것이 아닙니다. 하지만 리액트 개발자들은 훅을 사용해서 효과적으로 상태를 관리할 수 있을때도 필요 이상으로 상태관리 라이브러리를 사용합니다.

많은 양의 보일러플레이트 코드가 필요한 리덕스와 같은 써드파티 라이브러리를 시작하는 복잡성과 외부 패키지를 설치하거나 전역 상태를 관리하기 위해 여러 파일들과 폴더들을 어플리케이션에 추가할 필요가 없기에 리액트훅과 Context API를 사용해 상태 관리하는것은 꽤나 매력적 옵션입니다.

하지만 규칙은 여전히 존재합니다: 컴포넌트 상태를 위한 컴포넌트의 상태, 어플리케이션 상태를 위한 리덕스

How does useReducer work?

useReducer는 상태를 저장하고 업데이트하는데 사용됩니다, useState처럼 말이죠. 이 훅은 reducer 함수를 첫번째 인자로 받고 초기 상태를 두번째 인자로 받습니다.

useReducer는 현재 상태 값을 가지고 있는 배열과 상태값을 불러오기 위한 액션이 전달된 dispatch 함수를 반환합니다. 이러한 패턴들은 리덕스와 비슷하지만 몇몇 차이점이 있습니다.

예를 들어, useReducer 함수는 특정 리듀서와 밀접하게 연결되어있습니다. useReducer에서는 액션 객체를 오직 리듀서에게 보내지만, 리덕스에서는, dispatch 함수는 액션 객체를 스토어에게 보냅니다. dispatch되는 시간에, 컴포넌트는 액션을 처리하는 리듀서에 대해 알 필요가 없습니다.

리덕스에 대해 아직 익숙하지 않으신 분들을 위해, 이 개념에 대해 좀 더 살펴보도록 하겠습니다. 리덕스에는 중요한 세가지 요소가 있습니다.

  • 스토어: 어플리케이션의 상태 데이터를 가지고 있는 변하지 않는(불변의) 객체
  • 리듀서: 상태 데이터를 반환하는 함수이며, 액션의 type에 의해 작동된다.
  • 액션: 리듀서에게 상태 변경을 말해주는 객체. 반드시 type 프로퍼티를 가지고 있어야하며, 추가적으로 paylod 프로퍼티를 가질 수 있다.

예시를 통해 리덕스와 useReducer가 상태 관리하는 방식의 차이를 알아보겠습니다.

리덕스에서 사용하는 스토어의 예시입니다:

import { createStore } from 'redux'

const store = createStore(reducer, [preloadedState], [enhancer])

그리고 여기 useReducer에서 state를 초기화 하는 예시입니다:

const initialState = { count: 0 }

const [state, dispatch] = useReducer(reducer, initialState)

리덕스에서의 리듀서 함수는 앱의 전상태를 받고 액션을 dispatch 받으며, 다음 상태를 계산하고, 새로운 객체를 반환합니다.

리덕스의 리듀서에서 사용하는 구문:

(state = initialState, action) => newState

아래의 예시를 봐주세요:

// notice that the state = initialState and returns a new state

const reducer = (state = initialState, action) => {
   switch (action.type) {
      case 'ITEMS_REQUEST':
         return Object.assign({}, state, {
            isLoading: action.payload.isLoading
         })
      case ITEMS_REQUEST_SUCCESS':
         return Object.assign({}, state, {
            items: state.items.concat(action.items),
            isLoading: action.isLoading
         })
      default:
         return state;
   }
}
export default reducer;

리액트는 (state = initialState, action) => newState와 같은 구문을 사용하지 않습니다. 이것은 리덕스의 패턴입니다. useReducer의 reducer 함수는 조금 다르게 작동합니다. 아래의 예시처럼 리액트에서는 리듀서를 이렇게 만들 수 있습니다:

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}  

리덕스에서 사용되는 액션의 예시입니다:

{ type: ITEMS_REQUEST_SUCCESS, payload: { isLoading: false } }

// action creators
export function itemsRequestSuccess(bool) {
   return {
      type: ITEMS_REQUEST_SUCCESS,
      payload: {
      isLoading: bool,
    }
   }
}

// dispatching an action with Redux
dispatch(itemsRequestSuccess(false))    // to invoke a dispatch function, you need to pass action as an argument to the dispatch function

useReducer의 액션들 또한 비슷한 방식으로 작동합니다.

// not the complete code
switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    default:
      throw new Error();
  }

// dispatching an action with useReducer
 <button onClick={() => dispatch({type: 'increment'})}>Increment</button>

The reducer function

자바스크립트의 reduce() 메소드는 배열 각각의 요소에 reducer 함수들을 실행시키고 하나의 값을 반환합니다. 아래의 예시에서 reduce() 메소드는 최대 4개의 인자를 허용하는 리듀서 함수를 사용합니다.

const reducer = (accumulator, currentValue) => accumulator + currentValue;
[2, 4, 6, 8].reduce(reducer)
// expected output: 20

위의 예시는 리액트에서 useReducer가 어떻게 작동하는지 보여줍니다: useReducer는 하나의 값을 반환하는 reducer 함수를 받습니다.

const [count, dispatch] = useReducer(reducer, initialState);

reducer 함수 그 자체는 두개의 인자를 받으며 하나의 값을 반환합니다. 첫번째 인자는 현재 상태이며, 그리고 두번째 action입니다. 상태는 우리가 조작하고있는 데이터입니다. reducer 함수는 dispatch 함수에 의해 실행될 action을 받습니다.

function reducer(state, action) { }

dispatch({ type: 'increment' })

action은 reducer 함수에 전달하는 지시사항으로 생각하시면 됩니다. action이 어떻게 명시되었는지에 따라, reducer 함수는 필요한 상태 업데이트를 실행합니다. 만약에 리덕스와 같은 상태관리 라이브러리를 사용해보셨다면 이러한 상태 관리 패턴에 익숙하실겁니다.

Specifying the initial state

초기 상태값은 useReducer 훅에 두번째 인자로 전달이 되며, 기본 상태를 나타냅니다.

const initialState = { count: 1 }

// wherever our useReducer is located
const [state, dispatch] = useReducer(reducer, initialState, initFunc)

만약 useReducer의 세번째 인자를 넘겨주지 않는다면, useReducer는 두번째 인자를 초기 상태로 사용한다는것을 알아두세요. 세번째 인자인 init 함수는, 필수로 넣어줘야 하는 값은 아닙니다.

이 패턴은 또한 리덕스 상태관리의 황금률을 따라갑니다: 상태는 액션을 내보냄으로써 업데이트 됩니다. 상태를 직접 수정하려고 하지 마세요.

리덕스의 state = initialState 컨벤션은 useReducer에서 똑같이 적용되지 않습니다. 왜냐하면 초기값은 가끔씩 props에 의존하기 때문이죠.

Creating the initial state lazily

프로그래밍에서, 초기화 지연(lazy initialization)은 함수의 생성이나, 값의 계산, 혹은 다른 값비싼 프로세스를 해당 값이 필요하기 전까지 지연시키기 위한 전략입니다.

위에서 말했듯이, useReducer는 초기 상태를 지연적으로 생성하기 위해 필수값이 아닌 세번째 인자에 init 함수를 사용한다고 했습니다. 이 함수는 reducer 함수에서 초기 상태의 계산 로직을 함수 밖에서 가능하게 합니다. 아래의 예시를 봐주세요.

const initFunc = (initialCount) => {
    if (initialCount !== 0) {
        initialCount=+0
    }
  return {count: initialCount};
}

// wherever our useReducer is located
const [state, dispatch] = useReducer(reducer, initialCount, initFunc);

initFunc 함수는 컴포넌트가 마운트 되었을때 initialCount</count>를 0으로 리셋할겁니다. 만약에 값이 0이 아니라면, 객체형태인 상태를 반환합니다. initFunc가 단순히 배열이나 객체가 아닌 함수라는것을 알아두세요.

The dispatch method

dispatch 함수는 호출되었을때 실행하려는 작업 유형을 알려주는 객체를 받습니다. 기본적으로 이 함수는 리듀서 함수에게 액션을 보내 일을 하게 합니다. 여기서 일이란, 물론, 상태를 업데이트 하는것을 말합니다.

실행되어질 액션은 reducer 함수에 명시되어있으며, useRducer에 전달됩니다. 그리고 나서 리듀서 함수는 업데이트된 상태를 반환합니다.

컴포넌트에 의해 dispatch 되어질 액션들은 typepaylod를 담고있는 하나의 객체이며, type은 dispatch 될 액션을 식별하고 payload는 상태에 더해질 액션의 정보를 나타냅니다.

dispatch함수는 useReducer훅으로부터 반환될 두번째 값이며 상태를 업데이트 하기 위해 JSX에 사용될 수 있습니다.

// creating our reducer function
function reducer(state, action) {
  switch (action.type) {
   // ...
      case 'reset':
          return { count: action.payload };
    default:
      throw new Error();
  }
}

// wherever our useReducer is located
const [state, dispatch] = useReducer(reducer, initialCount, initFunc);

// Updating the state with the dispatch functon on button click
<button onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset </button>

reducer 함수가 dispatch 함수로부터 전달된 payload를 어떻게 사용하는지 보셨나요? reducer 함수는 initialCount가 무엇이든간에 객체형태의 상태를 payload로 세팅합니다.

여기서 알아둬야할 점은 dispatch 함수를 다른 컴포넌트에 props 형태로 보낼 수 있다는 겁니다. 이러한 사실만으로도 리덕스를 useReducer로 대체할 수 있습니다.

dispatch 함수를 props로 컴포넌트에 보낸다고 생각해보죠. 아래의 예시처럼 부모 컴포넌트로부터 보내줄 수 있습니다.

<Increment count={state.count} handleIncrement={() => dispatch({type: 'increment'})}/>

그리고, 자식 컴포넌트는 부모로부터 props를 받아 dispatch 함수를 실행시키고 상태를 업데이트하죠.

<button onClick={handleIncrement}>Increment</button>

Bailing out of a dispatch

만약 useReducer 훅이 현재 상태와 같은 값을 반환한다면, 리액트는 자식 컴포넌트를 랜더링 하지 않습니다. 왜냐하면 useReducer훅은 Object.is로 비교 알고리즘을 사용하기 때문입니다.

Building a simple counter app with useReducer

우리가 지금까지 배워왔던 내용들을 토대로 useReducer를 사용해 간단한 카운터 앱을 만들어보겠습니다.

import React, { useReducer } from 'react';

const initialState = { count: 0 }
 // The reducer function
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    case 'reset':
      return {count: state.count = 0}
    default:
     return { count: state.count  }
  }
}

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, initialState)

  return (
    <div>
      Count: {state.count}
       <br />
       <br/>
       <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
       <button onClick={() => dispatch({ type: 'decrement'})}>Decrement</button>
       <button onClick={() => dispatch({ type: 'reset'})}>Reset</button>
    </div>
  );
};

export default Counter;

첫번째로, 상태를 0으로 초기화하고, 카운터의 현재 상태를 인자와 액션으로 받는 reducer 함수를 만듭니다. 상태는 액션 타입에 따라 업데이트가 됩니다. increment, decrement, 그리고 reset은 액션의 타입이며, 이런 액션들이 dispatch 되었을때, 앱의 설정에 따라 상태를 업데이트합니다.

그래서 아래 카운트인 상태를 증가시키기 위해서,

const initialState = { count: 0 }

increment 액션이 dispatch 되었을때 간단히 count를 state.count + 1로 세팅할 수 있습니다.

useState vs. useReducer

비록 useState가 간단한 상태 변경을 다루는 기본적인 훅이고 useReducer는 상대적으로 좀 더 복잡한 형태의 상태 로직을 다루는 추가적인 훅이지만, useState의 내부에서는 useReducer를 사용한다는것을 알아두면 좋습니다. 즉, useState를 사용할 수 있는 모든 것을 useReducer로도 또한 사용할 수 있음을 말합니다.

하지만, 이 두개의 훅에는 큰 차이가 있습니다. useReducer를 사용해서 콜백함수를 여러 컴포넌트에 내려주는것을 피할 수 있으나, 대신 제공된 dispatch 함수를 내려, 깊은곳에 위치한 컴포넌트에서 상태를 업데이트할때의 성능을 향상시킬 수 있습니다.

useState의 업데이트 함수가 매번 컴포넌트가 랜더될때마다 새로 호출된다는 뜻은 아닙니다. 제가 말하고자 하는것은 복잡한 상태를 업데이트 해야할때, setState 함수를 바로 사용해 상태를 업데이트 시키는것 대신에, 복잡한 함수를 만들것이고 그 안에서 상태를 업데이트 하는 setState를 호출한다는 것이죠.

그러므로, 컴포넌트가 리랜더 되는 사이에 바뀌지 않는 dispatch 메소드를 사용하고 reducer안에서 로직들을 조작할 수 있는 useReducer의 사용을 권장합니다.

또한 useState에서는 상태를 업데이트 하기위해 상태 업데이트 함수가 호출되지만, useReducer에서는 dispatch 함수가 대신 호출되며, 최소 하나의 액션이 들어갑니다.

두개의 훅들이 어떻게 선언되고 사용되는지 보시겠습니다:

Declaring state with useState

const [state, setState] = useState('default state');

useState는 현재 값을 가지고 있는 상태와 해당 상태를 업데이트하기 위한 setState를 배열 형태로 반환합니다.

Declaring state with useReducer

const [state, dispatch] = useReducer(reducer, initialState)

useReducer는 상태와 상태변화를 위한 setState와 같은 목적을 가진 dispatch 메소드를 배열 형태로 반환합니다.

Updating state with useState

<input type='text' value={state} onChange={(e) => setState(e.currentTarget.value)} />

Updating state with useReducer

<button onClick={() => dispatch({ type: 'decrement'})}>Decrement</button>

조금있다 dispatch 함수에 대해 조금 더 깊게 이야기 해보겠습니다. 액션 객체에 payload를 넣어줄 수 있습니다.

<button onClick={() => dispatch({ type: 'decrement', payload: 0})}>Decrement</button>

useReducer는 복잡한 형태의 상태를 관리하는데 유용합니다. 예를 들어, 상태가 단순한 형태가 아닌 중첩된 배열이나 객체일 경우:

const [state, dispatch] = useReducer(loginReducer,
  {
    users: [
      { username: 'Philip', isOnline: false},
      { username: 'Mark', isOnline: false },
      { username: 'Tope', isOnline: true},
      { username: 'Anita', isOnline: false },
    ],
    loading: false,
    error: false,
  },
);

이런 로컬 상태를 다루기 더 쉬워집니다. 왜냐하면 각각의 인자들은 서로를 의지하고, 모든 로직들은 하나의 reducer에 캡슐화 되기 때문이죠.

When to use the useReducer Hook

어플리케이션의 크기가 커지게되면, 더 복잡한 상태변화를 다뤄야할겁니다. 여기서, useState보다 좀 더 예측 가능한 상태의 변화를 위해 useReducer를 사용하는것이 좋습니다. useReducer의 사용은 상태 변화가 복잡해저 하나의 장소에서 상태를 관리하고 싶을때 더욱 중요해집니다.

useReducer는 원시타입 (예를들어, 문자열, 숫자, boolean)과 같은 상태 관리보다 복잡한 객체(예를들어, 원시타입이 추가된 배열이 들어있는 객체)에 사용하는것이 좋습니다.

만약 여러분이 보면서 배우길 원하신다면 아래의 영상은 언제 useReducer 훅을 사용해야 할지 실질적은 예시로 좀 더 자세히 설명해 줄겁니다.

이야기하던 내용을 마저 이야기 해보도록 하죠. 로그인 컴포넌트를 만들어 useState와 useReducer를 사용해 상태를 관리해 봄으로써, 언제 useReducer를 사용하면 좋을지 더 알아보도록 하겠습니다.

첫번째로, useState를 사용한 로그인 컴포넌트 입니다.

import React, { useState } from 'react';

export default function LoginUseState() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [isLoading, showLoader] = useState(false);
  const [error, setError] = useState('');
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const onSubmit = async (e) => {
    e.preventDefault();
    setError('');
    showLoader(true);
    try {
      await function login({ username, password }) {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            if (username === 'ejiro' && password === 'password') {
              resolve();
            } else {
              reject();
            }
          }, 1000);
        });
      }
      setIsLoggedIn(true);
    } catch (error) {
      setError('Incorrect username or password!');
      showLoader(false);
      setUsername('');
      setPassword('');
    }
  };
  return (
    <div className='App'>
      <div className='login-container'>
        {isLoggedIn ? (
          <>
            <h1>Welcome {username}!</h1>
            <button onClick={() => setIsLoggedIn(false)}>Log Out</button>
          </>
        ) : (
          <form className='form' onSubmit={onSubmit}>
            {error && <p className='error'>{error}</p>}
            <p>Please Login!</p>
            <input
              type='text'
              placeholder='username'
              value={username}
              onChange={(e) => setUsername(e.currentTarget.value)}
            />
            <input
              type='password'
              placeholder='password'
              autoComplete='new-password'
              value={password}
              onChange={(e) => setPassword(e.currentTarget.value)}
            />
            <button className='submit' type='submit' disabled={isLoading}>
              {isLoading ? 'Logging in...' : 'Log In'}
            </button>
          </form>
        )}
      </div>
    </div>
  );
}

위의 예시에서 사용자가 로그인 컴포넌트에서 어떠한 액션을 취해야 하는지 집중해야 할 때, 상태 변화(username, password, isLoading, error, isLoggedIn)가 어떻게 처리되는지 보셨나요?

다섯개의 useState 훅들을 사용했고, 각각의 상태들이 언제 변화해야하는지 걱정해야합니다. 위의 코드를 useReducer로 리팩토링 해보고 모든 로직과 상태 변화를 하나의 reducer 함수로 감싸겠습니다.

import React, { useReducer } from 'react';

function loginReducer(state, action) {
  switch (action.type) {
    case 'field': {
      return {
        ...state,
        [action.fieldName]: action.payload,
      };
    }
    case 'login': {
      return {
        ...state,
        error: '',
        isLoading: true,
      };
    }
    case 'success': {
      return {
        ...state,
        isLoggedIn: true,
        isLoading: false,
      };
    }
    case 'error': {
      return {
        ...state,
        error: 'Incorrect username or password!',
        isLoggedIn: false,
        isLoading: false,
        username: '',
        password: '',
      };
    }
    case 'logOut': {
      return {
        ...state,
        isLoggedIn: false,
      };
    }
    default:
      return state;
  }
}
const initialState = {
  username: '',
  password: '',
  isLoading: false,
  error: '',
  isLoggedIn: false,
};
export default function LoginUseReducer() {
  const [state, dispatch] = useReducer(loginReducer, initialState);
  const { username, password, isLoading, error, isLoggedIn } = state;
  const onSubmit = async (e) => {
    e.preventDefault();
    dispatch({ type: 'login' });
    try {
      await function login({ username, password }) {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            if (username === 'ejiro' && password === 'password') {
              resolve();
            } else {
              reject();
            }
          }, 1000);
        });
      }
      dispatch({ type: 'success' });
    } catch (error) {
      dispatch({ type: 'error' });
    }
  };
  return (
    <div className='App'>
      <div className='login-container'>
        {isLoggedIn ? (
          <>
            <h1>Welcome {username}!</h1>
            <button onClick={() => dispatch({ type: 'logOut' })}>
              Log Out
            </button>
          </>
        ) : (
          <form className='form' onSubmit={onSubmit}>
            {error && <p className='error'>{error}</p>}
            <p>Please Login!</p>
            <input
              type='text'
              placeholder='username'
              value={username}
              onChange={(e) =>
                dispatch({
                  type: 'field',
                  fieldName: 'username',
                  payload: e.currentTarget.value,
                })
              }
            />
            <input
              type='password'
              placeholder='password'
              autoComplete='new-password'
              value={password}
              onChange={(e) =>
                dispatch({
                  type: 'field',
                  fieldName: 'password',
                  payload: e.currentTarget.value,
                })
              }
            />
            <button className='submit' type='submit' disabled={isLoading}>
              {isLoading ? 'Logging in...' : 'Log In'}
            </button>
          </form>
        )}
      </div>
    </div>
  );
}

useReducer를 사용함으로써 사용자가 취할 액션에 좀 더 집중되었습니다. 예를들어, login action이 dispatch되었고, 어떤 일이 발생할지 정확히 알 수 있습니다. 현재 상태를 반환하고, error의 상태를 빈문자열로 세팅했으며 isLoading 상태를 true로 바꾸었습니다:

case 'login': {
      return {
        ...state,
        error: '',
        isLoading: true,
      };
    }

여기서 무엇이 정말 좋은지 알려드리자면: 더이상 상태변화에 집중하지 않아도 된다는 겁니다. 대신, 사용자가 취할 액션에 집중할 수 있습니다.

When not to use the useReducer Hook

위의 예제앱에서 복잡한 상태를 다루기 위해 useReducer를 사용했음에도 불구하고, 리덕스와 같은 라이브러리들이 더 나은 선택이 될 수도 있는 경우가 있습니다. 하지만 언제 사용해야 할까요?

간단히 답변해드리자면 리액트에서 문제가 발생하기전에 리덕스와 같은 상태관리 라이브러리의 사용은 피하는것이 좋습니다. 그래도 이러한 라이브러리의 필요성이 고민된다고 하더라도, 사용이 필요할 가능성은, 거의 없습니다. 리덕스나 MobX와 같은 라이브러리의 사용이 필요할 경우를 설명하겠습니다.

When your application needs a single source of truth

리덕스와 같은 라이브러리를 사용해서 어플리케이션의 상태와 로직들을 중앙으로 집중시키면 서버의 상태를 클라이언트 앱으로 쉽게 직렬화 할 수 있으므로 전역 어플리케이션의 상태를 쉽게 생성할 수 있습니다. 하나의 진리의 원천(single source of truth)을 사용하면 실행 취소/ 사시 실행 기능과 같은 강력한 기능을 손쉽게 구현할 수 있습니다.

When you want a more predictable state

리덕스와 같은 라이브러리의 사용은 다른 환경에서도 항상 같은 행동을 하는 어플리케이션을 작성할 수 있도록 도와줍니다. 만약에 같은 상태와 action이 리듀서에게 전달되었다면, 같은 결과물이 항상 나올겁니다 왜냐하면 리듀서는 순수한 함수이기 때문이죠. 또한 리덕스에 있는 상태들은 읽기 전용이며, 이러한 상태를 바꾸기 위한 유일한 방법은 무엇을 해야할지 알려주는 action과 객체를 발생시키는것 뿐입니다.

When state-lifting to the top-level component no longer suffices

리액트 최상위 요소에서 상태를 관리하는것이 더이상 어려울때 리덕스와 같은 라이브러리를 사용하는것이 좋습니다.

State persistence

리덕스나 MobX와 같은 라이브러리들에서 상태들은 쉽게 localStorage에 저장되어지고 최종 사용자가 사용할 수 있도록 해줍니다. 페이지가 새로고침이 된 이후에도 말이죠.

이러한 모든 이점들에도, 리덕스와 같은 라이브러리의 사용은 순수 리액트에 있는 useReducer를 사용하는것과는 대조적으로 생각해야할 점이 존재합니다. 리덕스는 높은 러닝 커브를 가지고 있으며 작성해야할 코드의 양이 늘어납니다. 대신, 앱에서 상태를 확실하고 예측이 가능하도록 관리하는 방법이기도 합니다.

Conclusion

우리는 리액트 함수형 컴포넌트에서 복잡한 상태를 관리하기 위한 훅인 useReducer를 살펴보았습니다. 그리고 어떤 상황에서 useReducer의 사용이 상태관리에 이점을 가지는지 알아보았습니다.

리액트앱을 만드는데에 있어, 하나의 훅이 모든 문제를 풀기 어렵다는것을 알아두는것이 좋습니다. 어떤 훅이 상황에 맞고 어떻게 사용하는지, 그리고 상태관리를 위해 여러 훅들을 조합해 상태관리를 함으로써 좀 더 예측 가능할 수 있을지 이해를 해야합니다.

또한 위에서 말했듯이, useReducer 훅은 항상 최상의 선택은 아닙니다. 어플리케이션의 사이즈와 구조에 따라 가장 맞게 사용하는것이 중요합니다.

여기까지입니다! 이 글에 대해 어떻게 생각하는지 아래 댓글로 알려주세요. 저는 트위터깃헙이 있습니다. 읽어주셔서 감사합니다.

원문: The ultimate guide to the React useReducer Hook by Ejiro Asiuwhu