
해당 포스트는 Context API에 대한 이해를 요구합니다. Context API에 대해 처음이시라면 velopert님의 포스트를 참고해주세요.
React 에서는 전역 상태 관리를 위하여 자체적으로 ContextAPI 를 제공해준다.
ContextAPI 를 적절히 이용하면, Props Drilling을 이용해 Props를 전달해야했던 불편함은 해소되고 불필요한 Props 공유를 막을 수 있어 매우 좋은 도구중 하나이다. 간단하게 카운터 앱에서 카운트 숫자와 카운트를 증가하는 Dispatch 함수를 공유하는 context를 정의해보자.
App.tsximport { 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.tsximport { useCounterContext } from "../App";
export default function Counter() {
const { count, setCount } = useCounterContext();
return (
<div>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
components/DisplayCount.tsximport { 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>
);
}
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.tsx와 Counter.tsx 서로가 import 하게 되는 순환형 참조가 일어나게 된다. 이를 방지 하기 위해서는 기존의 context 사용 방법을 개선해야될 필요가 있는데 이는 바로 context 자체를 컴포넌트화 하는 것이다.
components/counterContext.tsximport { 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.tsximport './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.tsximport { useCounterContext } from "./counterContext";
export default function DisplayCount () {
const {count} = useCounterContext();
return <p>You clicked {count} times</p>;
}
Counter.tsximport { 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가 필요하다면 이런식으로 컴포넌트화 하여 사용하는 것을 추천한다!