리액트에서 데이터는 state와 props로 관리되며, topdown 형태로 데이터를 전달한다는 점은 리액트를 공부하는 사람들이라면 익히 알고 있는 사실이다. 이러한 리액트의 단방향 데이터 흐름 때문에 컴포넌트의 깊이가 깊어지면 props drilling이라는 문제가 생기고, Redux나 MobX, recoil 등이 이러한 문제를 개선하기 위해 전역에서 상태를 관래해주는 라이브러리들이다. 나와 같은 초보 개발자들 중 일부는 React만으로도 전역에서 상태를 관리해줄 수 있다는 사실을 모르고 있는 경우가 많다!
결론을 먼저 말하자면, 우리는 React의 ContextAPI를 이용해 컴포넌트 트리를 넘어 데이터를 공유할 수 있다! 특히 애플리케이션 전체에서 공유되는 데이터들, 예를 들어, UI테마, 선호 로케일, 현재 로그인한 유저 등 전역적으로 관리되어야 하는 데이터들의 경우 ContextAPI를 사용하면 좋다. 특히 이 API는 리액트 자체에 내장된 것으로 따로 라이브러리를 설치하지 않아도 된다는 장점이 있다. 작은 규모의 프로젝트라면 Context API를 통해 간편하게 전역 상태 관리를 하면 좋을 것이다!
1) App, 2) ButtonBox, 3) ThemedButton 총 세 개의 컴포넌트로 구성된 App이 있다고 하자. 우리가 만약 App에서 ThemedButton으로 theme을 전달한다고 하면, 기존의 방식에서는 늘 ButtonBox를 거쳐야했다. 아래 예시와 같이!
그러나 ContextAPI를 활용하면 중간 엘리먼트인 ButtonBox에 props를 넘겨주지 않아도 된다!
여기까지가 ContextAPI의 아주 단순한 예시를 통해 알아본 전체적인 컨셉이다. 그렇다면 우선 각 API들의 역할을 좀 더 살펴보도록 하자.
컨텍스트 객체를 만들 때 사용된다. 기본적으로 React는 트리 상위에서 가장 가까이에 있는 Provider를 탐색하고 감지될 경우 해당 Provider로부터 값을 읽어온다.
const MyContext = React.createContext(defaultValue);
createContext는 위와 같이 사용하는데, 매개변수 defaultValue는 트리 상위에서 적절한 Provider를 탐색해내지 못했을 때 쓰이는 값이다.
가시 말해 Provider가 감싸고 있지 않은 컴포넌트들의 경우 defaultValue를 가져오게 된다는 점!
<ThemeContext.Provider value={this.state.theme}>
<ThemedButton changeTheme={this.toggleTheme}/>
</ThemeContext.Provider>
<ThemedButton />
위와 같은 구조가 있을 때, Provider로 감싸진 ThemedButton은 context를 잘 참조해오지만, 바깥에 있는 ThemedButton은 defaultValue를 가져온다는 것이다.
context를 구독하는 컴포넌트 들에게 context의 변화를 알린다.
<MyContext.Provider value={/* 어떤 값 */}>
기본적으로 Provider는 Context 객체에 포함된 React 컴포넌트이고 value prop을 받아서 하위 컴포넌트에게 전달한다. Provider 하위에서 context를 구독하는 모든 컴포넌트는 Provider의 props가 바뀔 떄 재랜더링 된다!
컴포넌트가 구독할 context를 지정해주는 역할을 한다. 이 API를 이용하면 하나의 context만 구독할 수 있다는 점에 주의해야 한다.
Provider가 context의 변화를 알려주는 역할을 한다면, Consumer는 변화를 구독하고 감지해오는 역할을 한다. 이 컴포넌트를 사용하면 함수 컴포넌트 안에서 context를 구독할 수 있다!
<MyContext.Consumer>
{value => /* context 값을 이용한 렌더링 */}
</MyContext.Consumer>
Consumer 내부의 value는 상위 Provider 중 가장 가까운 Provider의 value prop과 같다. 상위에 적절한 Provider가 없으면 위에서 언급한 defaultValue를 가져온다.
딱히 중요한 API는 아니다. 리액트 개발자 도구에서 출력될 Context 객체의 이름을 정할 수 있다.
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" in DevTools
<MyContext.Consumer> // "MyDisplayName.Consumer" in DevTools
아래는 CodeSandBox를 통해 구현한 간단한 ContextAPI 활용사례이다.
그러나 하나 주의할 점은 ContextAPI는 literally 맥락, 컨텍스트에 의존적이다. 어떤 프로바이드 컴포넌트 안에 있느냐에 따라 렌더링 된 모습이 달라질 수 있다. 때문에 재사용성이 떨어지는 결과를 낳게 된다!