해당 포스트는 Context API에 대한 이해를 요구합니다. Context API에 대해 처음이시라면 velopert님의 포스트를 참고해주세요.
React
에서는 전역 상태 관리를 위하여 자체적으로 ContextAPI
를 제공해준다.
ContextAPI
를 적절히 이용하면, Props Drilling
을 이용해 Props를 전달해야했던 불편함은 해소되고 불필요한 Props 공유를 막을 수 있어 매우 좋은 도구중 하나이다. 간단하게 카운터 앱에서 카운트 숫자와 카운트를 증가하는 Dispatch 함수를 공유하는 context
를 정의해보자.
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>
);
}
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.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
가 필요하다면 이런식으로 컴포넌트화 하여 사용하는 것을 추천한다!