Javascript

React useReducer 완벽 이해

드리프트2 2024. 3. 19. 22:22

 

 

안녕하세요?

 

오늘은 React의 useReducer에 대해 완벽히 이해하는 시간을 가져볼까 합니다.

 

** 목차 **


React 상태 관리의 최적 접근 방식

 

React에서는 컴포넌트 상태를 관리하기 위해 두 가지 훅인 useState와 useReducer가 있습니다.

 

이 두 가지 모두 상태 관리에 사용되지만, 사용 목적과 다룰 수 있는 상태의 복잡성에 따라 선택해야 합니다.

 

이 글에서는 useState와 useReducer의 기본 사용법부터 각각이 적합한 시나리오, 그리고 Immer 라이브러리를 사용하여 복잡한 상태 관리를 간소화하는 방법에 대해 TypeScript 코드 스니펫과 함께 설명하겠습니다.

 

useState와 useReducer 기본

 

React에서 상태 관리는 동적 데이터를 다루는 핵심 역할을 합니다.

 

useState와 useReducer는 이러한 상태 관리를 위한 두 가지 주요 훅입니다.

 

이 글에서는 이러한 훅의 사용 방법과 각각이 어떤 경우에 적합한지에 대해 설명하겠습니다.

 

useState 사용 예시

 

useState는 단일 상태 값을 관리하는 데 사용됩니다.

 

아래는 TypeScript로 작성된 카운터 컴포넌트의 구현 예시입니다:

import React, { useState } from 'react';

const CounterComponent: React.FC = () => {
  const [count, setCount] = useState<number>(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default CounterComponent;

 

이 예시에서는 useState 훅을 사용하여 count 상태를 관리하고 있습니다.

 

setCount 함수를 통해 상태가 업데이트되면 컴포넌트가 다시 렌더링됩니다.

 

useReducer 사용 예시

 

useReducer는 복잡한 상태 로직이나 여러 하위 값이 포함된 상태를 관리할 때 유용합니다.

 

아래는 useReducer를 사용하여 구현한 카운터 컴포넌트 예시입니다:

import React, { useReducer } from 'react';

interface State {
  count: number;
}

type Action = { type: 'increment' } | { type: 'decrement' };

const initialState: State = { count: 0 };

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

const CounterComponent: React.FC = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>증가</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>감소</button>
    </div>
  );
};

export default CounterComponent;

 

이 예시에서는 액션 타입에 기반하여 상태를 업데이트하는 로직이 reducer 함수에 정의되어 있습니다.

 

useReducer는 복잡한 상태 관리나 여러 액션을 통해 상태를 업데이트해야 할 때 특히 유용합니다.

 

두 개의 훅 비교

  • useState: 간단한 상태나 몇 개의 상태 값을 관리할 때 적합합니다.
  • useReducer: 상태 업데이트 로직이 복잡하거나 여러 하위 값이 포함된 상태를 다룰 때 유용합니다.

useReducer를 활용한 복잡한 상태 관리의 장점

 

useReducer 훅은 React에서 상태 관리를 위한 강력한 도구입니다.

 

특히 복잡한 상태 로직이나 다수의 상태를 가진 대규모 애플리케이션에서 그 가치를 발휘합니다.

 

이 섹션에서는 useReducer를 사용하는 주요 이점에 대해 자세히 살펴보겠습니다.

 

상태 업데이트 로직의 분리와 재사용성

 

useReducer를 사용하는 가장 큰 이점 중 하나는 컴포넌트에서 상태 업데이트 로직을 분리할 수 있다는 점입니다.

 

이를 통해 로직을 재사용하기 쉽고 테스트가 용이해집니다.

 

다음은 여러 상태를 관리하는 컴포넌트의 예시입니다:

import React, { useReducer } from 'react';

interface State {
  count: number;
  text: string;
}

type Action = 
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'setText'; payload: string };

const initialState: State = { count: 0, text: '' };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + 1 };
    case 'decrement':
      return { ...state, count: state.count - 1 };
    case 'setText':
      return { ...state, text: action.payload };
    default:
      return state;
  }
}

const ComplexComponent: React.FC = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>증가</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>감소</button>
      <p>Text: {state.text}</p>
      <input
        value={state.text}
        onChange={(e) => dispatch({ type: 'setText', payload: e.target.value })}
      />
    </div>
  );
};

export default ComplexComponent;

 

이 예시에서는 useReducer를 사용하여 카운트와 텍스트를 모두 관리하고 있습니다.

 

상태 업데이트 로직이 reducer 함수에 집중되어 있기 때문에 컴포넌트 자체가 더 간단하고 가독성이 좋아집니다.

 

이벤트 소스 모델로서의 활용

 

useReducer는 애플리케이션 내에서 발생하는 이벤트를 모델링하는 데에도 적합합니다.

 

각 액션은 애플리케이션에서 발생하는 이벤트를 나타내며, 이러한 이벤트를 통해 애플리케이션의 상태가 업데이트됩니다.

 

이는 특히 상태 변경을 추적하고 디버깅할 때 유용합니다.

 

복잡한 상태 변경의 명확화

 

useReducer를 사용하면 상태 변경이 어떻게 이루어지는지 명확해집니다.

 

각 액션과 해당 처리 방법이 한 곳에 정의되기 때문에 어떤 액션이 어떤 영향을 미치는지 이해하기 쉬워집니다.

 

이러한 이점으로 인해 useReducer는 복잡한 상태 관리가 필요한 애플리케이션에서 useState보다 더 적합한 선택지가 될 수 있습니다.

 

특히 애플리케이션 규모가 커질수록 이 이점은 더욱 두드러집니다.


Immer를 활용한 상태 업데이트 간소화

 

복잡한 상태 객체를 관리하는 것은 특히 중첩된 객체나 배열을 포함하는 경우 복잡해질 수 있습니다.

 

React의 useStateuseReducer를 사용할 때 Immer 라이브러리를 결합하면 이러한 복잡한 상태 업데이트를 쉽고 직관적으로 수행할 수 있습니다.

 

Immer는 변경 불가능한 데이터를 다룰 때 발생하는 문제를 해결하기 위해 설계되었으며, 가변 작업을 수행하면서도 내부적으로 불변성을 유지하는 새로운 상태를 생성합니다.

 

useReducer와 Immer의 통합

 

아래는 TypeScript를 사용하여 useReducer와 Immer를 통합한 복잡한 상태 관리 예시입니다:

import React, { useReducer } from 'react';
import produce from 'immer';

interface State {
  count: number;
  text: string;
}

type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'setText'; payload: string };

const initialState: State = {
  count: 0,
  text: '',
};

const reducer = produce((draft: State, action: Action) => {
  switch (action.type) {
    case 'increment':
      draft.count += 1;
      break;
    case 'decrement':
      draft.count -= 1;
      break;
    case 'setText':
      draft.text = action.payload;
      break;
  }
});

const ComplexComponent: React.FC = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
      <p>Text: {state.text}</p>
      <input
        value={state.text}
        onChange={(e) => dispatch({ type: 'setText', payload: e.target.value })}
      />
    </div>
  );
};

export default ComplexComponent;

 

이 예시에서는 Immer의 produce 함수를 사용하여 reducer를 생성했습니다.

 

Immer를 사용하면 코드를 작성할 때 스테이트 객체를 변경하는 것처럼 보이지만 실제로는 불변성을 유지하는 새로운 스테이트 객체를 생성합니다.

 

useState를 이용한 Immer 활용

 

Immer는 useReducer뿐만 아니라 useState를 사용할 때에도 유용합니다.

 

특히, 스테이트가 복잡한 객체나 중첩된 데이터 구조를 가지는 경우에 편리합니다.

 

아래는 useState와 Immer를 결합한 예시입니다:

import React, { useState } from 'react';
import produce from 'immer';

interface State {
  count: number;
  text: string;
}

const ComplexComponent: React.FC = () => {
  const [state, setState] = useState<State>({ count: 0, text: '' });

  const increment = () => {
    setState(produce(draft => {
      draft.count += 1;
    }));
  };

  const setText = (text: string) => {
    setState(produce(draft => {
      draft.text = text;
    }));
  };

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={increment}>Increment</button>
      <p>Text: {state.text}</p>
      <input
        value={state.text}
        onChange={(e) => setText(e.target.value)}
      />
    </div>
  );
};

export default ComplexComponent;

 

이 코드에서는 setState를 호출할 때 produce 함수를 사용하고 있습니다.

 

이 방법을 통해 스테이트의 업데이트 로직을 더 간결하고 직관적으로 작성할 수 있습니다.

 

Immer를 사용하면 복잡한 스테이트 관리를 단순화하고 코드의 유지 보수성과 가독성을 향상시킬 수 있습니다.

 

React 애플리케이션에서 이러한 현대적인 접근 방식을 적용하여 개발 효율성과 애플리케이션 성능을 모두 향상시킬 수 있습니다.


최적의 방법과 성능

 

React에서 상태 관리는 응용 프로그램의 성능에 큰 영향을 미칩니다.

 

useState와 useReducer를 적절하게 사용하는 것은 효율적이고 유지 관리하기 쉬운 코드를 작성하는 데 매우 중요합니다.

 

여기에서는 이러한 훅을 사용할 때의 최적의 방법과 성능에 대해 설명합니다.

 

useState와 useReducer의 적절한 사용

 

단순한 상태 값 관리: useState는 값이 단일하고 업데이트 로직이 간단한 경우에 적합합니다.

 

예를 들어, 양식 입력 값이나 토글 스위치 상태와 같은 것이 이에 해당합니다.

 

복잡한 상태 로직 관리: useReducer는 여러 하위 값을 가진 상태나 여러 작업에 의해 상태가 업데이트되는 복잡한 로직을 다루는 데 최적입니다.

 

액션에 기반하여 상태를 업데이트하는 방법을 정의함으로써 더 명확하고 재사용 가능한 코드를 작성할 수 있습니다.

 

성능에 대한 영향

 

React의 상태 업데이트는 비동기적으로 수행되므로 불필요한 렌더링을 피하는 것이 성능 향상에 직결됩니다.

 

다음은 성능을 고려한 상태 관리를 위한 몇 가지 힌트입니다:

 

상태 분할: 하나의 큰 객체를 상태로 관리하는 대신 필요에 따라 여러 useState나 useReducer를 사용하여 상태를 분할합니다. 이렇게 하면 관련 없는 UI 부분의 불필요한 다시 렌더링을 방지할 수 있습니다.

 

메모화된 콜백 사용: useCallback 훅을 사용하여 이벤트 핸들러나 부작용에서 사용하는 함수를 메모화하여 불필요한 다시 계산을 피할 수 있습니다.

 

선택적 렌더링 최적화: React.memo를 사용하여 프롭스가 변경될 때만 컴포넌트가 다시 렌더링되도록 합니다. 특히 목록이나 항목을 렌더링할 때 유용합니다.

 

React에서의 최상의 사례 예시

import React, { useState, useCallback } from 'react';

const CounterComponent: React.FC = () => {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>증가</button>
    </div>
  );
};

 

이 예제에서는 useCallback을 사용하여 increment 함수를 메모화하고 있습니다.

 

이렇게 하면 increment 함수가 의존하는 값이 변경되지 않는 한 동일한 함수 인스턴스가 재사용되어 컴포넌트의 불필요한 다시 렌더링을 방지할 수 있습니다.

 

적절한 상태 관리 접근 방식을 선택하고 성능에 영향을 미치는 코딩을 고려하여 React 애플리케이션의 효율성과 사용자 경험을 크게 향상시킬 수 있습니다.


결론

React에서 상태 관리는 애플리케이션의 기반이 됩니다.

 

useStateuseReducer를 적절히 사용하여 애플리케이션 규모에 맞는 유연하고 효과적인 상태 관리가 가능합니다.

 

또한 Immer를 활용하면 상태 업데이트의 복잡성을 해소하고 개발 생산성을 높일 수 있습니다.

 

React 애플리케이션 개발 시 이러한 도구들을 적절히 활용하고 항상 성능을 고려하는 것이 중요합니다.

 

적절한 상태 관리 전략을 채택하여 더 반응성이 높고 사용자에게 편안한 애플리케이션을 구축해보세요.

 

이 글이 React를 사용한 애플리케이션 개발에서 효과적인 상태 관리에 대한 이해에 도움이 되길 바랍니다.

 

감사합니다. 🙌