Context API + useReducer로 상태관리하기

김정훈·2023년 9월 30일
0

팀프로젝트

목록 보기
5/5

상태 관리는 모든 React 애플리케이션에서 중요한 역할을 합니다. 이 글에서는 React의 Context APIuseReducer를 활용하여 상태 관리를 하는 방법을 작성해볼까 합니다. 제가 진행했던 프로젝트에서 이를 이용해 구현한 주요기능을 같이 소개해 드리면서 진행하겠습니다.

어떤 기능을 구현했는데??

백카사전(백과사전 + car)은 어려운 자동차 용어에 대한 설명을 제공하는 기능입니다. 이 기능을 어떻게 구현을 해야할지 고민을 많이 했습니다.

어떻게 만들어야 할까?

헤더에 있는 버튼을 클릭해 기능을 활성화시키면 특정 단어를 하이라이팅해야하네?
그런데 하이라이팅 된 단어의 위치는 특정할 수 없어. 즉, 어떤 컴포넌트에 위치할지 알 수 없네?

이 고민들을 근거로 전역으로 백카사전에 관련된 상태를 관리하는 영역이 필요할 것 같다고 느꼈습니다. 부트캠프 기간 중에 Flux 패턴을 이용해서 과제를 수행했었는데, 이와 비슷하게 Store를 만들고 텍스트 하이라이팅 컴포넌트와 모달이 구독하는 방식으로 구현하면 되지 않을까 싶었죠. 여기서 Context APIuseReducer를 조합해 상태관리 도구를 만들면 되겠다고 생각했습니다.

Context API + useReducer로 Flux 패턴 구현하기

Flux 패턴이란?

사용자의 입력을 기반으로 Action을 만들고, Action을 Dispatcher에 전달하여 Store의 데이터를 변경한 뒤 View에 반영하게 되는 단방향 아키텍처를 의미합니다.

예를 들어 백카사전을 켜는 행위(action)를 하면 Dispatcher가 Store의 상태를 Off에서 On으로 업데이트하고 Store는 자신을 구독하는 View에게 업데이트 되었다는 사실을 알립니다. 그리고 View는 새로운 데이터를 받아 렌더링됩니다.(텍스트 하이라이팅, 모달 활성화 등) View에서 사용자의 새로운 입력이 발생하면 이에 해당하는 action을 발생시켜 이 과정을 다시 수행합니다.

조금 더 자세히 들어가서 Flux 패턴기반 상태관리도구가 어떻게 동작하는지 알아보겠습니다. ContextuseReducer로 구현하는 도구의 동작원리와 관련되어 있습니다.

이미지 출처:
https://redux.js.org/tutorials/essentials/part-1-overview-concepts

  1. 사용자가 버튼을 클릭하는 등 앱에서 어떤 일이 발생합니다.

  2. 발생한 작업(action)을 스토어(store)에 전달(dispatch)합니다.

  3. state저장소는 이전 상태와 현재 action으로 reducer 함수를 다시 실행하고 새로운 state로 반환된 값을 저장합니다.

    reducer는 action과 현재 state를 바탕으로 새로운 state를 업데이트하는 함수

  4. 스토어는 구독 중인 UI의 모든 부분에 스토어가 업데이트되었음을 알려줍니다.

  5. 스토어의 데이터가 필요한 각 UI 구성 요소는 필요한 상태 부분이 변경되었는지 확인합니다.

  6. 데이터가 변경된 각 컴포넌트는 새 데이터로 강제로 다시 렌더링되므로 화면에 표시되는 내용을 업데이트할 수 있습니다.

useReducer

useReducer는 컴포넌트에 reducer를 추가할 수 있는 리액트 훅입니다.

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

매개변수

  • reducer: stateaction을 매개변수로 받고 상태가 업데이트되는 방법을 지정하는 순수함수
  • initialArg: 초기 상태가 되는 값

반환값

  • [현재 상태, dispatch]형태의 배열

dispatch 함수를 호출하면 상태를 업데이트하고 다시 렌더링 할 수 있습니다.

dispatch({ type: 'incremented_age' });

reducer 함수에 현재 state와 dispatch한 action을 전달하고, 그 결과를 다음 state로 업데이트합니다.

이 처럼 useReducer훅은 Flux 패턴의 동작을 손쉽게 구현할 수 있도록 도움을 줍니다.

백카사전 만들기

그렇다면 본격적으로 Context와 useReducer를 이용해 상태관리 도구를 만들고, 이를 바탕으로 백카사전을 구현해보겠습니다.

설계

백카사전을 이용하는 사용자가 취할 수 있는 행동(action)은 다음과 같습니다.

  1. 백카사전 기능 on/off
  2. 단어 클릭
  3. 설명 모달 닫기

그리고 action에 따라 업데이트 될 상태는 다음과 같습니다.

  1. 백카사전 기능 on/off
    • 백카사전 기능 활성화 여부 전환
    • 설명 모달 닫기
  2. 단어 클릭
    • 모달에 제공할 데이터
    • 설명 모달 열기
  3. 설명 모달 닫기
    • 설명 모달 닫기

백카사전 Store을 구독할 컴포넌트의 종류와 영향을 받는 상태를 정해보겠습니다. 모든 컴포넌트는 공통적으로 백카사전 기능 on/off state에 영향을 받습니다. 텍스트 하이라이팅 컴포넌트와 모달 컴포넌트는 단어 클릭, 모달 활성화 여부에 추가적으로 영향을 받습니다.

구현하기

먼저 초기 상태와 action을 정의해보죠.

export const INIT_DATA = 'INIT_DATA'; //Store를 초기화합니다
export const CLICK_WORD = 'CLICK_WORD'; //단어를 클릭합니다
export const CLOSE_MODAL = 'CLOSE_MODAL'; //설명 모달을 닫습니다
export const TOGGLE_CARDICT = 'TOGGLE_CARDB'; //백카사전 기능을 켜거나 끕니다

const initialState = {
  dataObject: { //설명 모달에 보여질 데이터 객체
    keyword: '',
    description: '',
    imgSrc: null,
  },
  modalOpen: false, //설명 모달 활성화 여부
  carDictOn: false, //백카사전 기능 활성화 여부
};

각 action에 따른 state를 업데이트하는 reducer를 정의하겠습니다.

const carDictReducer = (state, action) => {
  switch (action.type) {
    case 'INIT_DATA': {
      const newDataObject = action.payload.dataObject;
      return {
        ...state,
        dataObject: newDataObject,
      };
    }
    case 'CLICK_WORD': {
      const newDataObject = action.payload.dataObject
      return {
        ...state,
        dataObject: newDataObject,
        modalOpen: true,
      };
    }
    case 'CLOSE_MODAL':
      return {
        ...state,
        modalOpen: false,
      };
    case 'TOGGLE_CARDB':
      return {
        ...state,
        modalOpen: false,
        cardbOn: !state.cardbOn,
      };
    default:
      return state;
  }
};

그런다음 Context를 Store로써 만들고 상태관리는 useReducer를 통해이뤄지도록 구현하겠습니다.

import React, {createContext, useContext, useReducer} from 'react';

const CarDictStateContext = createContext(undefined);
const CarDictDispatchContext = createContext(undefined,);

export const useCarDictState = () => {
  const state = useContext(CardbStateContext);
  if (!state) throw new Error('Cannot find StateContext');
  return state;
};

export const useCarDictDispatch = () => {
  const dispatch = useContext(CarDictDispatchContext);
  if (!dispatch) throw new Error('Cannot find DispatchContext');
  return dispatch;
};

export const CarDictProvider = ({children}) => {
  const [state, dispatch] = useReducer(carDictReducer, initialState);

  return (
    <CarDictStateContext.Provider value={state}>
      <CarDictDispatchContext.Provider value={dispatch}>
        {children}
      </CarDictDispatchContext.Provider>
    </CarDictStateContext.Provider>
  );
};

여기까지가 백카사전 Store를 구현한 코드입니다.

그런데 useCarDictStateuseCarDictDispatch를 커스텀 훅으로 만든 이유가 있을까?

일단 표현이 간단해집니다. 일일이 useContext 훅을 사용하지 않아도 커스텀 훅을 호출하면 어떤 컴포넌트에서도 Contexgt state에 접근할 수 있고, dispatch 함수를 사용할 수 있게됩니다. 게다가 어떤 Context의 상태와 dispatch인지 훅의 이름을 통해서 알 수 있겠습니다.

import {
  useCarDictState,
  useCarDictDispatch,
} from 'CardictContext.js';

function HighRight({children}: CardbProps) {// Text 하이라이팅 컴포넌트
  const cardbState = useCarDictState();
  const dispatch = useCarDictDispatch();

  // code ...

  return <TextBox>{children}</TextBox>;
}

function CardbModal() { // 설명 모달 컴포넌트
  const {dataObject, modalOpen} = useCarDictState();
  const dispatch = useCarDictDispatch();

  const handleClickBtn = () => {
    dispatch({type: CLOSE_MODAL, payload: {}});
  };

  return (
    // modal JSX
  );
}

결과물

참고자료
https://redux.js.org/tutorials/essentials/part-1-overview-concepts
https://react.dev/learn/scaling-up-with-reducer-and-context

0개의 댓글