React 또는 Vue와 같은 SPA 프레임워크/라이브러리를 사용하다보면
항상 "상태 관리"에 대해 신경써야하는 경우가 발생한다.
컴포넌트는 props를 사용해서 상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달할 수 있다.
만약 트리구조를 갖는 컴포넌트에서 다른 분기에 있는 컴포넌트끼리 데이터를 주고 받아야 한다면
흔히 알고 있는 prop drilling이 발생하며 이는 코드 유지보수에 악영향을 끼친다.
따라서 "상태 관리"를 어떻게 하느냐에 따라 좋은 코드를 결정할 수 있다.
Context API를 제대로 한번 알아보자
리액트를 다루는 기술 책을 참고 하였습니다.
리액트 공식문서를 참조 하면 Context를 다음과 같이 소개한다.
Context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있습니다.
처음 Context API를 접했을 때, 너무 어렵게 이해하려는 나머지 중요한 본질을 넘어가고 있었다.
Context는 전역 상태 관리를 위한 Context를 만들어서 어떤 컴포넌트에서든 Context에 있는 데이터를 사용할 수 있게 만든다.
마찬가지로 공식문서에서는 다음과 같은 경우 Context를 사용하는 것을 권장한다.
Context는 React 컴포넌트 트리 안에서 전역적(global)이라고 볼 수 있는 데이터를 공유할 수 있도록 고안된 방법입니다. 그러한 데이터로는 현재 로그인한 유저, 테마, 선호하는 언어 등이 있습니다.
말 그대로 전역적으로 사용하는 데이터를 컴포넌트간 props으로 전달하지 않고 Context라는 일종의 저장소를 만들어서 각각의 컴포넌트에서 쉽게 접근할 수 있도록 만든다는 것이다.
Context API는 리액트 v16.3부터 사용하기 쉽게 많이 개선되었다고 한다.
또한 Context API는 리액트 관련 라이브러리에서도 많이 사용되고 있다. 우리가 잘 알고 있는 Redux, React-Router, Styled-Components 등의 라이브러리는 Context API를 기반으로 구현되어 있다고 한다.
우선 데이터를 갖고 있는 Context를 만들어 보자
import { createContext } from "react";
const ColorContext = createContext({ color: "black" });
export default ColorContext;
ColorContext라는 컴포넌트를 만든다. 이때 리액트에 내장되어 있는 createContext 함수를 사용한다. 이 함수를 사용하면 간단하게 Context를 만들 수 있다. 아주 간단하게 Context를 만들었다.
여기서 createContext 함수의 매개변수는 defaultValue다. 이 값은 트리 안에서 적절한 Provider를 찾지 못했을 때만 쓰이는 값이다.
Context를 만들었으니 누군가는 소비를 해야한다. 저장소에 있는 데이터를 가져가서 사용하는 소비자가 필요하다는 말이다.
import ColorContext from "../contexts/color";
const ColorBox = () => {
return (
<ColorContext.Consumer>
{value => (
<div style={{ widht: '64px', height: '64px', background: value.color}}/>
</ColorContext.Consumer>
);
};
export default ColorBox;
ColorBox 컴포넌트는 ColorContext를 가져온다. 여기서 ColorContext는 createContext를 통해 만들어진 Context다. ColorContext 안에 들어 있는 Consumer라는 컴포넌트를 사용한다. 이 컴포넌트는 함수를 자식으로 받는다.
Context.Consumer는 context 변화를 구독하는 React 컴포넌트입니다. 이 컴포넌트를 사용하면 함수 컴포넌트 안에서 context를 구독할 수 있습니다.
Context.Consumer의 자식은 함수여야합니다. 이 함수는 context의 현재값을 받고 React 노드를 반환합니다. 이 함수가 받는 value 매개변수 값은 해당 context Provider 중 상위 트리에서 가장 가까운 Provider의 value prop과 동일합니다. 상위에 Provider가 없다면 value 매개변수 값은 createContext( )에 보냈던 defaultValue와 동일할 것입니다.
ColorContext.Consumer 컴포넌트 사이에 함수를 전달하는데, 이 함수는 기본적으로 value라는 값을 갖고 있다. 이 value에서 원하는 데이터를 사용하면 된다.
여기서는 value 값을 default로 사용하고 있기 때문에 'black'이 된다.
Context 오브젝트에 포함된 React 컴포넌트인 Provider는 context를 구독하는 컴포넌트들에게 context의 변화를 알리는 역할을 합니다.
Provider를 사용하면 Context의 value를 변경할 수 있다.
import ColorBox from "./components/ColorBox";
import ColorContext from "./contexts/color";
const App = () => {
return (
<ColorContext.Provider value={{ color: "red" }}>
<div>
<ColorBox />
</div>
</ColorContext.Provider>
);
};
export default App;
App 컴포넌트에서 Context의 Provider를 사용해서 value의 color를 'red'로 변경했다.따라서 App 내부에 있는(하위) ColorBox 컴포넌트가 return 하는 Context.Consumer 컴포넌트에서 사용되는 value 값은 red가 된다.
Provider 컴포넌트는 value prop을 받아서 이 값을 하위에 있는 컴포넌트에게 전달합니다. 값을 전달 받을 수 있는 컴포넌트의 수에 제한은 없습니다. Provider 하위에 또 다른 Provider를 배치하는 것도 가능하며, 이 경우 하위 Provider의 값이 우선시됩니다.
정리하면 Provider를 사용해서 context의 value를 변경하였고, Provider 컴포넌트는 ColorBox를 감싸고 있다. ColorBox에서 Consumer를 사용해서 context의 값을 사용 한다면, default 값이 아닌 상위 Provider에서 변경된 value를 사용하게 되는 것이다. 만약 Provider 내부에서 또 다른 Provider를 사용한다면 Context를 사용하는 컴포넌트의 가장 가까운 상위 Provider 값을 사용한다는 것이다.
const App = () => {
return (
<ColorContext.Provider value={{ color: "red" }}>
<div>
<ColorBox /> // ColorContext.Provider의 value 사용
</div>
<OtherContext.Provider value={{ something: "nothing" }}>
<div>
<ColorBox /> // OtherContext.Provider의 value 사용
</div>
</OtherContext.Provider>
</ColorContext.Provider>
);
Provider 하위에서 context를 구독하는 모든 컴포넌트는 Provider의 value prop가 바뀔 때마다 다시 렌더링 됩니다. Provider로 부터 하위 consumer로의 전파는 shouldComponentUpdate 메서드가 적용되지 않으므로, 상위 컴포넌트가 업데이트를 건너 뛰더라도 consumer가 업데이트 됩니다.
지금까지 내용은 동적으로 Context를 사용하는 것이 아니다. 사실 동적으로 사용할 일이 더 많지만
Context API의 기본적인 원리를 이해하기 위해서 정리해 보았다. 다음에는 동적으로 Context를 사용하는 방법과 Context 관련해서 가장 많이 사용되는 Hook, useContext()에 대해서도 알아봐야겠다