[React] 4-6장 | useReducer의 역할과 사용법

Re_Go·2024년 6월 13일
0

React

목록 보기
9/10
post-thumbnail

1. useReducer란?

일단 개념은 검색을 해보면 알 다음과 같습니다.

복잡한 상태 전이를 단순화하고, 여러 상태값을 일관성 있게 관리할 때 유용합니다. useReducer는 리듀서 패턴을 사용하여 상태 전이 로직을 관리
합니다.

여기서 주요 특징을 살펴보자면 다음과 같이 설명할 수 있는데요.

  1. 복잡한 상태 전이를 단순화
  2. 여러 상태값을 일관성 있게 관리
  3. 리듀서 패턴을 사용하여 상태 전이 로직을 관리.

우선 앞선 특징들을 하나씩 살펴보겠습니다.

2. useReducer의 기본 사용법

우선 useReducer를 생성하는 방법은 함수 정의, 초기 상태 정의, useReudcer 할당 순서로 이루어지는데요. 대강 생성 방법은 다음과 같습니다.

아래의 코드 예시에서 설명 드려보겠습니다.

import React, { useReducer, useRef } from 'react';

// ★ reducer 함수 정의
const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + action.payload };
    case 'DECREMENT':
      return { ...state, count: state.count - action.payload };
    case 'SET_INPUT_VALUE':
      return { ...state, inputValue: action.payload };
    default:
      return state;
  }
};

function Counter() {
  // ★ 초기 상태 정의
  const initialState = {
    count: 0,
    inputValue: '',
  };

  // ★ useReducer 할당
  const [state, dispatch] = useReducer(reducer, initialState);
  const { count, inputValue } = state;

  // useRef를 사용하여 input 요소를 참조
  const inputRef = useRef(null);

  // 버튼 클릭 이벤트 핸들러
  const onClickButton = (val) => {
    dispatch({ type: val > 0 ? 'INCREMENT' : 'DECREMENT', payload: Math.abs(val) });
  };

  // 엔터 키 이벤트 핸들러
  const onEnterPress = (e) => {
    if (e.key === 'Enter') {
      dispatch({ type: 'SET_INPUT_VALUE', payload: inputRef.current.value });
    }
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => onClickButton(1)}>Increment</button>
      <button onClick={() => onClickButton(-1)}>Decrement</button>
      <input ref={inputRef} type="text" placeholder="Type something" onKeyDown={onEnterPress} />
      <p>Input value: {inputValue}</p>
    </div>
  );
}

export default Counter;

우선 위의 코드에서 함수를 정의하는 부분은 다음과 같습니다.

// ★ reducer 함수 정의
const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + action.payload };
    case 'DECREMENT':
      return { ...state, count: state.count - action.payload };
    case 'SET_INPUT_VALUE':
      return { ...state, inputValue: action.payload };
    default:
      return state;
  }
};

함수 정의 부분에서는 switch-case 문을 사용하여 action의 타입 값에 따라 state의 이전값(...state)과 변화시킬 특정 값과 연산하여 할당해주는 코드가 들어갑니다.

정리하자면 기존의 상태 관리(useState)의 각 메서드들이 하던 처리해당 reducer 함수에 모아놓고, case에 따라 처리한다고 보시면 됩니다. 그래서 함수 정의 부분에서는 기존의 스테이트의 다양한 처리 메서드가 컴포넌트 안에 작성될 때 어려웠던 코드 관리나 가독성 저하를 해결할 수 있기도 한거죠.

// useState의 객체 상태 초기화
const [state, setState] = useState({
    count: 0,
    inputValue: '',
  });

// useReducer에서 사용할 객체 상태 정의 (초기화)
  const initialState = {
    count: 0,
    inputValue: '',
  };

다음은 초기 상태 정의 부분입니다. 정확히 말하자면 state의 초기값들을 정의하는 부분입니다. 편하게 객체 리터럴로 사용할 상태값들의 초기 상태를 지정해주면 됩니다.

const [state, dispatch] = useReducer(reducer, initialState);
// state (useReducer에서 관리할 최신 스테이트)값들을 각 스테이트에 할당한 후 화면에 렌더링 
const { count, inputValue } = state;

마지막 useReducer 할당 부분입니다. useReducer를 호출한 뒤 앞서 정의한 함수 부분과 초기 객체값을 useReducer로 처리한 뒤 그 결과값을 각각 state와 dispatch 메서드 할당해 준다고 보시면 되겠습니다.

이후에는 Dispatch 메서드가 실행될 때마다 reducer 함수에서 정의한 각각의 반환문(상태 변화 로직)이 실행된 뒤 state의 값을 반환해주는 것으로 useReducer의 최종 단계가 마무리 됩니다.

이때 두번째 매개변수 (초기값)useReducer가 최초로 실행될 때 state에 전달해주는 초기값 이므로 이후의 렌더링에는 사용되지 않는다(영향을 미치지 않는다)는 점을 알아두시면 됩니다.

3. useReducer의 발생 단계

언뜻 텍스트로 보면 이해가 안가실텐데요, 앞서 소개해드린 useReducer의 정의 부분이 끝났다고 가정하고, 발생 단계에 대해서 소개해 드리겠습니다.

  1. 상태 변화 : 버튼을 누르면 onClick 이벤트가 발생한 뒤 해당 메서드에 1을 전달합니다.
<button onClick={() => onClickButton(1)}>Increment</button>
  1. Dispatch 호출 : 해당 이벤트 핸들러가 dispatch 메서드를 호출한 후 Action 오브젝트 객체를 생성하여 전달합니다. 이때 기본적으로 type (reducer 함수에서 switch문을 실행할 코드)와 상태값을 바꾸게 할 때 사용하는 변수를 한 쌍으로 한 객체를 dispatch 메서드에 전달합니다.
 const onClickButton = (val) => {
    dispatch({ type: val > 0 ? 'INCREMENT' : 'DECREMENT', value: Math.abs(val) });
  };
  1. reducer 함수 실행 : Dispatch는 넘겨 받은 Action 객체(오브젝트)를 reducer에 넘겨주는 역할을 하는데요. 이때 reducer의 첫번째 매개변수는 기존의 상태값이(state), 두번째 매개변수에는 Action 객체가 할당됩니다.

앞서 이벤트 핸들러에가 Dispatch 메서드를 호출할 때 객체를 넘겨준다고 했죠? 그리고 이 객체를 Dispatch 메서드가 reducer 함수로 넘겨줄 때 할당되어 action.type과 action.value를 이용하여 조건문의 반환문을 실행하여 상태값을 반환받도록 할 수 있는 것이죠.

const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + action.value };
    case 'DECREMENT':
      return { ...state, count: state.count - action.value };
    case 'SET_INPUT_VALUE':
      return { ...state, inputValue: action.value };
    default:
      return state;
  }
};

정리하자면 useReducer각 이벤트 핸들러가 Dispatch를 호출하고, 실제 실행되는 코드는 앞서 정의한 reducer 함수에서 처리하므로, 컴포넌트 내부의 코드가 간결해지고, 각 이벤트 핸들러의 실행 함수를 모아 놓아 관리까지 간단하게 할 수 있는 편의성을 제공하는 것이죠.

이는 내부 로직이 복잡해지는 코드, 그러니까 프로젝트를 하다보면 당연히 내부 로직이 복잡해질테고, 이때 useReducer가 유용하게 사용되는 것입니다.

(자료 출처 : https://blog.stackademic.com/understanding-usereducer-in-react-part-1-0994812b5a2a)

위의 이미지에서는 더 쉽게 useReducer가 처리되는 과정을 다음과 같이 설명하고 있습니다.

  1. state 업데이트
  2. 이벤트 핸들러에 리렌더링
  3. 이벤트 헨들러에서 Dispatch 호출
  4. Dispatch에서 Action 오브젝트를 Reducer 함수에 전달
  5. Type 값에 따라 반환문 실행 후 state에 상태값 반환

4. useState와의 비교해보는 useReducer의 특징

이러한 useReducer를 사용하는 것과 안하는 것의 예시는 useState를 예로 들어 설명할 수 있는데요.

앞서 소개해드린 useReducer의 장점 세 가지를 useState를 쓸 때와 안쓸 때를 들어 설명해 보겠습니다.

  1. 복잡한 상태 전이를 단순화 : useState로 개별적인 이벤트 핸들러(상태 변화) 메서드가 늘어날 때 코드가 복잡해지는 것을 단순화 한다는 의미입니다.
// useState를 사용할 때

 const onClickButton = (val) => {
    if (val > 0) {
      setCount(count + val);
    } else {
      setCount(count - Math.abs(val));
    }
  };

  const onEnterPress = (e) => {
    if (e.key === 'Enter') {
      setInputValue(inputRef.current.value);
    }
  };

// useReducer를 사용할 때

const onClickButton = (val) => {
    dispatch({ type: val > 0 ? 'INCREMENT' : 'DECREMENT', value: Math.abs(val) });
  };

  const onEnterPress = (e) => {
    if (e.key === 'Enter') {
      dispatch({ type: 'SET_INPUT_VALUE', value: inputRef.current.value });
    }
  };
  1. 여러 상태값을 일관성 있게 관리 : 모든 상태가 하나의 객체 안에 묶여 있기 때문에 상태 간의 관계를 보다 일관성 있게 유지할 수 있습니다. 이는 상태 업데이트 로직이 중앙(reducer)에서 관리되기 때문에 상태의 일관성 유지가 가능하다는 뜻입니다.
// useState를 사용할 때

const [count, setCount] = useState(0);
const [inputValue, setInputValue] = useState('');

// useReducer를 사용할 때

const [state, dispatch] = useReducer(reducer, initialState);
const { count, inputValue } = state;
  1. 리듀서 패턴을 사용하여 상태 전이 로직을 관리. : 1번의 장점과도 비슷한데, 결국 모든 이벤트 핸들러 함수가 reducer에서 작성되므로 Type에 따라 실행할 이벤트 핸들러 함수를 호출하여 보다 쉽게 해당 함수 로직을 관리할 수 있음을 의미합니다.
profile
인생은 본인의 삶을 곱씹어보는 R과 타인의 삶을 배워 나아가는 L의 연속이다.

0개의 댓글