[React] ContextAPI 이렇게 써보자

신세원·2021년 10월 10일
2

React

목록 보기
1/28
post-thumbnail

구글링을 하다가 어떠한 글을 보았다.

Context API는 왜 안쓰나요?

ContextAPI를 사용하는 작성자에게 굉장히 관심이 가는 글이었고, 내용의 결론은 소규모 프로젝트에서는 ContextAPI가 좋지만 성능 때문에 추천하지는 않는다였다.
물론 저 글을 쓰신 분은 프로젝트의 규모가 큰 곳에서 종사하시기 때문에 애초에 ContextAPI가 아닌 커뮤니티도 좋고 많은 사람들이 이용하는 Redux를 사용하시기에 별로 관심이 없으실테지만, ContextAPI를 사용하는 당사자의 입장에서는 ContextAPI도 관리만 잘하면 성능 부분에서도 나쁘지 않게 사용할 수 있다라는 걸 알리고 싶었기에 해당 글을 작성한다.

1. Context 준비하기

ContextAPI CodeSandBox 테스트용

첨부한 링크에 간단한 테스트용 컴포넌트를 만들었고, 여기서는 컴포넌트 부분 보다는 context.js에만 집중하도록 하자.
먼저 src 디렉터리에 context 디렉터리를 만들고, 그 안에 context.js 파일을 만든다.
그리고, 제일 먼저 context.js 파일 안에 두 개의 context를 만들어 보도록 하자.

여기서 중요한 점은 하나는 상태(state) 전용 context 이고, 또 다른 하나는 디스패치(dispatch) 전용 context 이다.
이렇게 두 개의 context 를 만들면 불필요한 렌더링을 방지 할 수 있다.

만약 상태(state)와 디스패치(dispatch) 함수를 한 context 에 넣게 된다면, 상태는 필요하지 않고 디스패치 함수만 필요한 컴포넌트도 상태가 업데이트 될 때 리렌더링하게 된다. 두 개의 context를 만들어서 관리한다면 이를 방지 할 수 있다.

상태(state) 전용 Context

이제 상태 전용 context를 만들어보자.

src/context/context.js

import { createContext, useReducer, useContext, useMemo } from "react";

// 추후 Provider를 사용하지 않았을 때에는 context의 값이 null이 되어야 하기때문에 null 값을 선언해준다.
const StateContext = createContext(null)
const DispatchContext =createContext(null)

const factoryUseContext = (name, context) => () => {
  const ctx = useContext(context);
  if (ctx === undefined) {
    throw new Error(
      `use${name}Context must be used withing a ${name}ContextProvider.`,
    );
  }
  return ctx;
};


export const useStateContext = factoryUseContext("StateContext", StateContext);
export const useDispatchContext = factoryUseContext("DispatchContext",DispatchContext);

const initialState = {
  number: 1
};

const reducer = (state, action) => {
  switch (action.type) {
    case "INCREASE":
      return {
        number: state.number + 1
      };
    case "DECREASE":
      return {
        number: state.number - 1
      };
    case "RESET":
      return {
        number: 1
      };
    default:
      return state;
  }
};

export function ContextProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);
const values = useMemo(() => state, [state]);
  
  return (
    <StateContext.Provider value={values}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
}

이제 context 준비는 어느정도 끝이 났다.
그 전에 위 코드에 대해 몇가지 짚고 넘어갈 점이 있어 정리 좀 하고 가기로 한다.

아래에서 (1)번과 (2)번은 동일하다.

(1)번
const factoryUseContext = (name, context) => () => {
  const ctx = useContext(context);
  if (ctx === undefined) {
    throw new Error(
      `use${name}Context must be used withing a ${name}ContextProvider.`,
    );
  }
  return ctx;
};

export const useStateContext = factoryUseContext("StateContext", StateContext);
export const useDispatchContext = factoryUseContext("DispatchContext",DispatchContext);
(2)번
export function useStateContext() {
  const state = useContext(StateContext);
  if (!state) throw new Error('useStateContext must be used withing a StateContextProvider');
  return state;
}

export function useDispatchContext() {
  const dispatch = useContext(DispatchContext);
  if (!dispatch) throw new Error('useDispatchContext must be used withing a DispatchContextProvider');
  return dispatch;
}

(1)번과 같은 경우 useContext 함수에 대해 반복적 이므로 재사용 가능한 팩토리 함수로 추상화해 보았다.
이 factoryUseContext함수는 name 오류 메시지에 사용될 context 매개변수와 소비될 매개변수를 허용한다.

만약factoryUseContext함수 이름을 앞에 use가 붙는 usefactoryUseContext로 바꾸게 되면 eslint에서 콜백 내에서 호출할 수 없는 오류가 발생할 수 있으므로 eslint를 비활성화 해야 할 수 있다.
이 eslint 오류는 함수 useContextFactory가 use후크용으로 예약된 단어로 시작 하기 때문에 발생한다.
그래서 작성자는 factoryUseContext와 같은 다른 이름으로 함수의 이름을 바꾸었다 .

이렇게 만약 함수 내부에서 필요한 값이 유효하지 않다면 에러를 throw 하여 각 Hook이 반환하는 값의 타입은 언제나 유효하다는 것을 보장 받을 수 있다. (만약 유효하지 않다면 브라우저의 콘솔에 에러가 바로 나타난다.)

import React, { ..., useMemo } from 'react'; 

{...}

export function ContextProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);
  const values = useMemo(() => state, [state]); // 이 부분 주목

  return (
    <StateContext.Provider value={values}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>

contextAPI는 state가 바뀌면 하위 컴포넌트들이 전부 재렌더링이 되기 때문에 성능 최적화에서 힘든 부분이 있다.
아래처럼 쓰면 컴포넌트가 재렌더링이 될때마다 value 객체가 새로 재렌더링이 되는데, 이럴경우 성능에 문제가 생길수 있어서 캐싱을 해줘야하기 때문에
useMemo를 사용해준다.

2. Context 사용하기

ContextProvider로 감싸기

그 다음 해야 할 작업은 index.js 컴포넌트에서 ContextProvider 를 불러와서 기존 내용을 감싸주는 것이다.

src/index.js


import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { ContextProvider } from "./context/context";
import App from "./App";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <StrictMode>
    <ContextProvider>
      <App />
    </ContextProvider>
  </StrictMode>,
  rootElement
);

Context 사용하기

src/App.js

import React, { useCallback } from "react";
import "./styles.css";
import Child from "./Child";
import { useDispatchContext } from "./context/context";

export default function App() {
  const dispatch = useDispatchContext();

  const onRest = useCallback(() => {
    dispatch({ type: "RESET" });
  }, []);

  return (
    <div className="App">
      <Child />
      <button className="button" onClick={onRest}>
        Reset
      </button>
    </div>
  );
}

src/Child.js

import React, { memo, useCallback } from "react";
import { useStateContext, useDispatchContext } from "./context/context";

function Child() {
  console.log("Child render");

  const { number } = useStateContext();
  const dispatch = useDispatchContext();

  const onIncrease = useCallback(() => {
    dispatch({ type: "INCREASE" });
  }, []);

  const onDecrease = useCallback(() => {
    dispatch({ type: "DECREASE" });
  }, []);

  return (
    <div className="border-container">
      <p>Child of</p>
      <div className="btn-container">
        <button onClick={onDecrease}>-</button>
        {number}
        <button onClick={onIncrease}>+</button>
      </div>
    </div>
  );
}
export default memo(Child);

React 렌더링 성능 최적화하는 7가지 방법 (Hooks 기준)

여기서 함수는 useCallback으로 감싸고, 컴포넌트는 memo로 감싸는게 중요하다.
위에 첨부한 링크를 보면 React를 최적화 하는 방법이 기재되어 있는데, 저 글에 있는 방법들만 숙지하면 최적화 하는데 문제가 없을것으로 본다.

이번 포스팅에서는 리액트의 Context API를 효과적으로 활용하는 방법에 대해서 알아보았다.
Context API를 사용하여 상태를 관리 할 때 useReducer를 사용하고 상태 전용 Context 와 디스패치 함수 전용 Context를 만들면 매우 유용합니다. 거기에 다른 hooks 까지 응용하는 방법을 알아 보았다.
이번 포스팅에서는 엄청 간단한 기능을 구현해 보았고 이런 방식이 무조건 정답은 아니다.
그러니 맥락만 이해하고 잘 응용해서 사용하는게 중요하다!

4. TypeScript 와 Context API 활용하기
React 기초 학습 18.ContextAPI__성능최적화(Zerocho님 강의 학습 의식의 흐름대로 노트 정리)
useMemo inside Context API - React
How to Use Context API with Hooks Efficiently While Avoiding Performance Bottlenecks
React Context 및 useContext() 후크 가이드

profile
생각하는대로 살지 않으면, 사는대로 생각하게 된다.

0개의 댓글