작년에 React deep dive 스터디를 했었다. 당시 context API를 Props drilling을 해결하기 위한 의존성 주입 도구로 활용할 수 있다는 걸 배웠다. 하지만 실무에서 props drilling이 깊어질수록 복잡해졌고 무작정 전역상태로 넘겨버리는 방식을 택하게 되었다. 그러다 최근 토스 FF 레포지토리에서 논의되는 것들을 보며 다시금 Context API의 본질을 고민하게 되었고 이렇게 글을 쓰게 되었다.
Props Drilling
React는 부모 ⇒ 자식 컴포넌트로 이루어진 트리 구조를 가지고 있다.
부모가 가진 데이터를 자식에서 사용하려면 props를 통해 전달해야 한다.
<A props={something}>
<B props={something}>
<C props={something}>
<D props={something} />
</C>
</B>
</A>
위와 같은 구조에서 A ⇒ D 까지 데이터를 전달하려면 중간에 있는 모든 컴포넌트가 props를 받아서 하위 컴포넌트로 필요한 위치까지 넘겨야 한다. 이런 현상을 props drilling이라고 한다.
해결책? 전역 상태가 아니라 의존성 주입(Dependency Injection, DI)
많은 사람들이(나 포함) Props Drilling을 해결하기 위해 무작정 전역 상태 관리 라이브러리(Jotai, Redux, Zustand 등)를 도입한다. 사실 그보다 먼저 고려해야 하는 건 의존성 주입 방식이다.
의존성 주입으로 Props Drilling을 극복하기 위해 등장한 개념이 바로 Context이다. Context를 사용하면 명시적인 props 전달 없이도 선언한 하위 컴포넌트에서 모두 자유롭게 원하는 값을 사용할 수 있다.
이 개념을 좀 더 깊게 들여다보면, Context API는 단순이 값을 전달하는 것이 아니라 맥락(Context)을 제공하는 역할을 해야 한다. 컴포넌트 트리에서 특정 개념을 명확하게 담고 관련된 데이터와 메서드를 응집력 있게 묶는 것이 중요하다.
useContext
React에서 상위 컴포넌트에서 만들어진 Context를 함수형 컴포넌트에서 사용할 수 있도록 만들어진 훅.
import { createContext, useContext } from 'react';
const ThemeContext = createContext(null);
export default function MyApp() {
return (
<ThemeContext.Provider value="dark">
<Form />
</ThemeContext.Provider>
)
}
function Form() {
return (
<Panel title="Welcome">
<Button>Sign up</Button>
<Button>Log in</Button>
</Panel>
);
}
function Panel({ title, children }) {
const theme = useContext(ThemeContext);
const className = 'panel-' + theme;
return (
<section className={className}>
<h1>{title}</h1>
{children}
</section>
)
}
function Button({ children }) {
const theme = useContext(ThemeContext);
const className = 'button-' + theme;
return (
<button className={className}>
{children}
</button>
);
}
- 컴포넌트 최상위 수준에서 useContext를 호출하여 Context를 읽고 구독한다.
- useContext는 전달한 Context에 대한 Context Value를 반환한다. Context 값을 결정하기 위해 React는 Component Tree를 탐색하고 Context에 대해 상위에서 가장 가까운 Context Provider를 찾는다.
- 위 코드에서 Context를 Button에 전달하기 위해 상위 컴포넌트를 Context Provider로 감싼 것을 알 수 있다. Provider와 Button 사이에 컴포넌트 레이더 수는 중요하지 않다. Form 내부 Button이 어디서든 useContext(ThemeContext)를 호출하면, “dark”를 값으로 받는다.
- 다수의 Provider와 useContext를 사용할 때, 특히 타입스크립트를 사용한다면 아래와 같이 별도 함수로 감싸서 사용하는 것이 좋다. 타입 추론에도 유용하고 상위에 Provider가 없는 경우에도 사전에 쉽게 에러를 찾을 수 있기 때문.
function useMyContext(){
const context = useContext(MyContext)
if(context === undefined){
throw new Error(
'useMyContext는 ContextProvider 내부에서만 사용할 수 있습니다.'
)
}
return context
}
사용 시 주의할 점
useContext를 함수형 컴포넌트 내부에서 사용할 때는 항상 컴포넌트 재활용이 어려워진다. 사용하는 순간 Provider와의 의존성을 가지고 있는 셈이 되므로 독립적으로 동작할 수 없다.
🌟 컴포넌트를 최대한 작게 혹은 재사용되지 않을 컴포넌트에서 사용한다.
모든 Context를 최상위 루트 컴포넌트에 넣는 건 어떨까❓
Context 증가 시 Root Component에선 해당 props를 다수의 Component에서 사용할 수 있게끔 해야 하므로 불필요하게 리소스가 낭비된다. 따라서 Context가 미치는 범위는 최대한 좁게 만들어야 한다.
🌟 다시 한 번 말하지만 useContext는 상태 관리를 위한 것이 아니다 ⇒ 상태를 주입해 주는 API다!
상태 관리 라이브러리가 되기 위해선 최소 두 가지 조건을 만족해야 한다.
- 어떠한 상태를 기반으로 다른 상태를 만들어 낼 수 있어야 한다.
- 필요에 따라 이러한 상태 변화를 최적화할 수 있어야 한다.
Context는 단순히 props 값을 하위로 전달해 줄 뿐 렌더링 최적화를 해주지 않는다. 오히려 Context 값이 변경되면 하위의 모든 컴포넌트가 리렌더링 된다.
Jotai의 Providerless Mode 📌
Jotai는 Provider 없이도 사용할 수 있는 primitive atom을 지원한다. 이는 전역 상태(store)와 의존성 주입 방식의 차이를 보여준다. 즉, 전역 상태 관리 도구는 store 개념이 중심이고 의존성 주입 도구는 특정 계층 내에서만 데이터를 주입하는 방식이라는 것. Context API는 의존성 주입 방식과 유사한 API라는 걸 알 수 있다.
🌟 Context를 남용하는 대신 resolve alias, overlay-kit, prefetch 같은 다른 방법을 고려해 보는 것도 좋다.
위의 추가적인 방법에 대해서는 추후에 상세하게 다루어 보겠다.
결론
Context API는 의존성 주입 도구이며, 전역 상태 관리 도구가 아니다. 따라서 상태 관리가 필요할 때는 별도의 상태 관리 라이브러리를 고려해야 한다.
Theme, Auth, Locale 같은 글로벌 UI 설정을 공유할 때 혹은 특정 계층 내에서만 필요한 값을 주입할 때 Context API를 사용하면 적절할 것이고, 전역 상태 혹은 여러 개의 상태가 조합된 경우, 렌더링 최적화가 필요한 경우엔 지양하는 것을 권장한다.
📚 리액트 딥다이브 useContext 챕터
https://www.heropy.dev/p/EdhHX2
https://jwchung.github.io/DI는-IoC를-사용하지-않아도-된다
https://github.com/toss/frontend-fundamentals/discussions/5
https://ko.react.dev/reference/react/useContext
ChatGPT 🤖
'JavaScript > React' 카테고리의 다른 글
State: A Component’s Memory | 공식 문서, 책으로 useState 알아보기 (2) | 2024.03.10 |
---|---|
React Fiber... 어렵지만 친해지고 싶은 고마운 친구 (14) | 2023.12.18 |
React, TS에서의 Event Type interface와 React Event System 구조 (4) | 2023.11.29 |