Context API 를 컴포넌트로 사용하기

sxungchxn.dev·2022년 7월 17일
0
post-thumbnail

Context 컴포넌트로 사용하기

해당 포스트는 Context API에 대한 이해를 요구합니다. Context API에 대해 처음이시라면 velopert님의 포스트를 참고해주세요.

React 에서는 전역 상태 관리를 위하여 자체적으로 ContextAPI 를 제공해준다.

ContextAPI 를 적절히 이용하면, Props Drilling을 이용해 Props를 전달해야했던 불편함은 해소되고 불필요한 Props 공유를 막을 수 있어 매우 좋은 도구중 하나이다. 간단하게 카운터 앱에서 카운트 숫자와 카운트를 증가하는 Dispatch 함수를 공유하는 context를 정의해보자.

🎈counterContext 정의 해보기

  • App.tsx
import { useState, createContext, Dispatch, useContext } from 'react';
import './App.css';
import Counter from './components/Counter';
import DisplayCount from './components/DisplayCount';

interface counterContextValue {
  count: number;
  setCount: Dispatch<number>;
}

export const counterContext = createContext<counterContextValue | undefined>(undefined);

export const useCounterContext = () => {
  const context = useContext(counterContext);
  if(!context){
    throw new Error('useCounterContext must be used within a CounterContextProvider');
  }
  return context;
}

function App() {
  const [count, setCount] = useState<number>(0);
  return (
    <div className="App">
      <counterContext.Provider value={{count, setCount}}>
        <DisplayCount/>
        <Counter/>
      </counterContext.Provider>
    </div>
  );
}

export default App;
  • components/Counter.tsx
import { useCounterContext } from "../App";

export default function Counter() {
    const { count, setCount } = useCounterContext();

    return (
        <div>
            <button onClick={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    );
}
  • components/DisplayCount.tsx
import { useCounterContext } from "../App";

export default function DisplayCount ()  {
    const {count} = useCounterContext();
    return <p>You clicked {count} times</p>;
}

간단하게 App.tsx 카운트 숫자와 카운트를 증가시키는 Dispatch 함수를 담는 counterContext 를 정의하고 이를 Counter 라는 컴포넌트와 DisplayCount 라는 컴포넌트에게 Context API 를 통해 카운트 숫자와 증가 함수를 전달시키는 방식이다. 여기서 기존과는 다른 방식이 약간 존재한다.

우선 타입스크립트에서 createContext 를 하려고하면 초기화 할때 기본값을 넘겨주어야 할텐데 아래의 코드를 이용할 경우 불필요한 기본값을 정의할 필요없이 undefined 로 초기화 할 수 있다.

우선 타입스크립트에서 createContext 를 하려고하면 초기화 할때 기본값을 넘겨주어야 할텐데 아래의 코드를 이용할 경우 불필요한 기본값을 정의할 필요없이 undefined 로 초기화 할 수 있다.

export const counterContext = createContext<counterContextValue | undefined>(undefined);

하지만 이 상태로 바로 Counter 컴포넌트에서 Context를 사용하려고 하면 다음과 같은 타입 오류가 발생한다.

이는 Counter 컴포넌트 입장에서는 counterContext 에서 넘어오는 값이 counterContextValue | undefined 로 정의 되어져 있고 둘 중 어느 타입이 올지 정확히 알 수 없기 때문이다. 여기서 Counter 컴포넌트는 counterContext 로 부터 타입이 counterContextValue 인 값만 받아오면 되기 때문에 타입가드를 적절히 활용하여 특정 타입만 오도록 타입을 제한하면 될 것이다.


export const useCounterContext = () => {
  const context = useContext(counterContext);
  if(!context){
    throw new Error('useCounterContext must be used within a CounterContextProvider');
  }
  return context;
}

이를 위해 위와 같은 커스텀 훅을 도입하게 되면 Counter 컴포넌트는 undefined 를 제외한 값들을 받아올 수 있게 된다. 뿐만 아니라 불필요하게 useContext 훅과 counterContext 를 import 할 필요없이 useCounterContext 만 import하면 counterContext 를 사용할 수 있게되어 코드량도 줄일 수 있다!

import { useCounterContext } from "../App";

export default function Counter() {
    const { count, setCount } = useCounterContext();

    return (
        <div>
            <button onClick={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    );
}

🧐 기존 context 사용 방법의 문제점들

import { useState, createContext, Dispatch, useContext } from 'react';
import './App.css';
import Counter from './components/Counter';
import DisplayCount from './components/DisplayCount';

interface counterContextValue {
  count: number;
  setCount: Dispatch<number>;
}

export const counterContext = createContext<counterContextValue | undefined>(undefined);

export const useCounterContext = () => {
  const context = useContext(counterContext);
  if(!context){
    throw new Error('useCounterContext must be used within a CounterContextProvider');
  }
  return context;
}

function App() {
  const [count, setCount] = useState<number>(0);
  return (
    <div className="App">
      <counterContext.Provider value={{count, setCount}}>
        <DisplayCount/>
        <Counter/>
      </counterContext.Provider>
    </div>
  );
}

export default App;

앞서 봤던 App.tsx 다시 한번 확인해보자. 크게 두가지 문제점들이 있다. 우선, counterContext 를 정의하기 위해 타이핑을 위한 interface 선언, createContext 를 통해 context를 생성하는 부분, 편의성을 위해 정의한 커스텀 훅 등 App.tsx 파일 내부에 너무 많은 코드들이 작성되어 있다. App.tsx 내부에 여러 컴포넌트들을 추가하면서 코드 길이가 길어진다면 읽기가 매우 힘들어질 것이다.

import { useCounterContext } from "../App";

export default function Counter() {
    const { count, setCount } = useCounterContext();

    return (
        <div>
            <button onClick={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    );
}

또한 이를 counterContext 사용하기 위해 App.tsxCounter.tsx 서로가 import 하게 되는 순환형 참조가 일어나게 된다. 이를 방지 하기 위해서는 기존의 context 사용 방법을 개선해야될 필요가 있는데 이는 바로 context 자체를 컴포넌트화 하는 것이다.


🚧 Context를 컴포넌트화 하기

  • components/counterContext.tsx
import { createContext, useState, ReactNode, Dispatch, useContext } from "react"

interface counterContextValue {
  count: number;
  setCount: Dispatch<number>;
}

const counterContext = createContext<counterContextValue | undefined>(undefined);

const CounterProvider = ({children}: {children: ReactNode}) => {
    const [count, setCount] = useState<number>(0);
    return (
        <counterContext.Provider value={{count, setCount}}>
            {children}
        </counterContext.Provider>
    );
}

const useCounterContext = () => {
    const context = useContext(counterContext);

    if(!context){
        throw new Error('useCounterContext must be used within a CounterContextProvider');
    }
    return context;
}

export {CounterProvider, useCounterContext};

기존의 context 관련 코드들을 모두 하나의 컴포넌트로 담는 것이 핵심이다. 또한 이를 counterContext 의 공급책인 counterContext.Provider 를 사용하는 대신 이를 포함하는 컴포넌트인 CounterProvider 를 정의하여 좀 더 간결하게 사용할 수 있도록 할 수 있다. 아래처럼 말이다.

  • App.tsx
import './App.css';
import Counter from './components/Counter';
import {CounterProvider} from './components/counterContext';
import DisplayCount from './components/DisplayCount';

function App() {
  return (
    <div className="App">
      <CounterProvider>
        <DisplayCount/>
        <Counter/>
      </CounterProvider>
    </div>
  );
}

export default App;
  • DisplayCount.tsx
import { useCounterContext } from "./counterContext";

export default function DisplayCount ()  {
    const {count} = useCounterContext();
    return <p>You clicked {count} times</p>;
}
  • Counter.tsx
import { useCounterContext } from "./counterContext";

export default function Counter() {
    const { count, setCount } = useCounterContext();

    return (
        <div>
            <button onClick={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    );
}

기존에는 Context API 를 사용하면서 아쉬웠던 점이 코드가 굉장히 지저분해진다 였는데 이렇게 컴포넌트화 하여 사용하니 코드도 깔끔해지면서 굉장히 사용하기 편리해졌다. Context API가 필요하다면 이런식으로 컴포넌트화 하여 사용하는 것을 추천한다!

🚩 출처와 참고자료

How to use React context effectively

velopert - 22. Context API 를 사용한 전역 값 관리

profile
🏠 버튼을 누르면 더 많은 글들을 보실 수 있습니다

0개의 댓글