React Context 올바르게 사용하기
이번 포스팅에서는 React의 내재된 상태관리 API인 Context에 대한 사용 방법을 알아보려고 한다.
리액트에서 데이터는 일반적으로 props, state로 관리하며 부모 컴포넌트에서 자식 컴포넌트로 흐르게 된다. 이 부분에 대해서는 이번 글에서는 길게 설명하지는 않겠다.
예를 들어 E 컴포넌트에서 state를 만들고 그걸 prop drilling 방식으로 G에 내려 주어서 사용하는 식이다. 그런데 때에 따라서는 정말 많은 컴포넌트에서 데이터를 써야 할 일이 있을 수도 있다.
위의 그림처럼 많은 컴포넌트에서 데이터를 사용해야 한다면, 사실상 전역으로 사용한다고 보아도 무방하다. 이 경우는 Root Component에서 state를 만들고 각각 자식 컴포넌트로 props 를 내려주는 식으로 사용을 해야 할 것이다.
만약 여기서 G Component에서 데이터를 추가했고 그 값을 J Component에서 사용한다면 어떻게 해야 할까? G → E → C → A → Root → H → J 이렇게 가야 할 것이다. 여기에서 컴포넌트 구조가 더 깊어진다면..? 그러면 데이터는 더 먼 길을 돌아서 하나의 컴포넌트에서 다른 컴포넌트로 이동을 하게 되며 이는 성능 저하 이슈를 일으킬 수 있다.
이를 해결하기 위해 React 에서 Flux를 도입하였고 이 철학을 기반으로 Context API가 생겨나게 되었다.
Context를 사용하는 방법은 크게 세 단계로 나누어서 살펴볼 수 있다.
- Context 만들기
- Provider 만들기
- Consumer 만들기
첫 번째로 리액트의 빌트인 함수인 createContext
를 가지고 컨텍스트 인스턴스를 만든다.
import { createContext } from 'react';
const Context = createContext('Default Value');
그 후에 Context.Provider
컴포넌트를 만들어서 해당 Context를 자식 컴포넌트 영역에서 사용할 수 있게 만든다.
function Main() {
const value = 'My Context Value';
return (
<Context.Provider value={value}>
<MyComponent />
</Context.Provider>
);
}
그리고 이 컴포넌트를 사용하는 방법에는 두 가지가 있는데 useContext
hook을 사용하는 것을 권장한다.
import { useContext } from 'react';
function MyComponent() {
const value = useContext(Context);
return <span>{value}</span>;
}
이렇게 작업을 하면 context value가 바뀔 때 마다 useContext hook은 바뀐 값을 가지고 리렌더링을 컴포넌트에서 하게 된다.
Context를 이렇게 따로따로 작성하지 않고 하나의 context.js 파일을 만들어서 관리할 수도 있다. 어떤 데이터 대해서 값을 수정해주는 로직이 있는 경우도 있는데, 이 경우는 dispatch를 사용할 수 있다.
숫자를 카운트 하는 컨텍스트 예제코드를 한 번 살펴보도록 하자. Custom Provider와 Custom Consumer를 만든 부분에 주목하자.
import * as React from 'react'
const CountContext = React.createContext()
function countReducer(state, action) {
switch (action.type) {
case 'increment': {
return {count: state.count + 1}
}
case 'decrement': {
return {count: state.count - 1}
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
// Custom Provider
function CountProvider({children}) {
const [state, dispatch] = React.useReducer(countReducer, {count: 0})
const value = {state, dispatch}
return <CountContext.Provider value={value}>{children}</CountContext.Provider>
}
// Custom Consumer
function useCount() {
const context = React.useContext(CountContext)
if (context === undefined) {
throw new Error('useCount must be used within a CountProvider')
}
return context
}
// 최상단 컴포넌트에서 Custom Provider를, 각 컴포넌트에서 이 Custom Consumer를 사용할 수 있다.
export {CountProvider, useCount}
그렇다면 Context는 전역상태를 관리하기에 완벽한 방법일까? 개발 업계에 “There is no silver bullet.” 이라는 말이 있듯이 Context도 완벽하지는 않다.
먼저 Context는 하나당 하나의 데이터를 관리한다 위의 예제도 count라는 데이터만을 관리할 수가 있다. 프로젝트가 커지면 관리해야 하는 데이터가 정말 많은데, 그럴 때마다 새로운 Context를 추가해 주어야 한다. 컨텍스트가 다섯 개만 되어도 다음과 같이 중첩이 심하게 일어날 수가 있다.
function App() {
return (
<AProvider>
<BProvider>
<CProvider>
<DProvider>
<EProvider>
<Component />
</EProvider>
</DProvider>
</CProvider>
</BProvider>
</AProvider>
)
}
ReactDOM.render(<App />, document.getElementById('⚛️'))
더 큰 문제는 A,B,C,D,E Context가 한 번씩만 바뀌더라도 Component가 그 때마다 리렌더링을 해서 다섯 번의 리렌더링이 일어나게 되는데, 이는 Context가 많아지고 중첩이 될수록 성능 저하의 문제를 초래할 수 있다. 직접 내가 어떤 Context를 구독하지 않는데 의존성이 있는 감싸여진 다른 Context가 바뀌면서 불필요한 렌더링이 발생하는 것이다.
이렇게 React Context에 대해서 간단하게 알아보았다. 이 글을 읽는 개발자 분들이 필요한 상황에서 적절하게 사용해서 큰 도움을 받았으면 좋겠다.
참고자료
A Guide to React Context and useContext() Hook