
리액트 애플리케이션에서는 데이터가 컴포넌트의 props를 통해 부모에서 자식으로, 단방향으로 전달됩니다. 하지만 여러 컴포넌트에 걸쳐 굉장히 자주 사용되는 데이터의 경우 기존 방식을 사용하면 코드가 너무 복잡해집니다.
Context 는 리액트 컴포넌트들 사이에서 데이터를 기존의 props을 통해 전달하는 방식 대신 컴포넌트 트리를 통해 곧바로 컴포넌트로 전달하는 새로운 방식을 말합니다. 이를 통해 어떤 컴포넌트든지 데이터에 쉽게 접근할 수 있습니다.

여러 컴포넌트에 걸쳐서 자주 사용되는 데이터(로그인 여부, 프로필 정보 등...)를 전달하려면 반복적인 코드가 많이 생기고 지저분해집니다.

데이터를 필요로 하는 컴포넌트에 곧바로 데이터를 전달할 수 있습니다. 코드도 매우 깔끔해지고, 데이터를 한 곳에서 관리하기 때문에 디버깅을 하기에도 굉장히 좋습니다.
여러 개의 컴포넌트들이 접근해야 하는 데이터에는 로그인 여부, 로그인 정보, UI 테마, 현재 선택된 언어, 현재 지역 정보, 캐싱된 데이터 등.. 이 있습니다.
예를 들어 웹사이트 상단에 위치한 네비게이션 바에 사용자의 로그인 여부에 따라서 로그인 버튼과 로그아웃 버튼을 선택적으로 보여주고 싶은 경우 현재 로그인 상태 데이터에 접근해야 합니다.
function App(props) {
return <Toolbar theme="dark" />;
}
function Toolbar(props) {
// 이 Toolbar 컴포넌트는 ThemedButton에 theme를 넘겨주기 위해서 'theme' prop을 가져야만 합니다.
// 현재 테마를 알아야 하는 모든 버튼에 대해서 props로 전달하는 것은 굉장히 비효율적입니다.
return (
<div>
<ThemeButton theme={props.theme} />
</div>
);
}
function ThemeButton(props) {
return <Button theme={props.theme} />;
}
가장 상위 컴포넌트인 App 컴포넌트에서는 Toolbar 컴포넌트를 사용하고 있습니다. 이 때 theme이라는 이름의 props으로 현재 테마인 dark를 넘깁니다.
Toolbar 컴포넌트에서는 ThemeButton 컴포넌트를 사용하는데, ThemeButton 컴포넌트에서 현재 테마를 필요로 합니다. props에서 받은 theme를 하위 컴포넌트인 ThemeButton 컴포넌트에 전달합니다.
최종적으로 ThemeButton 컴포넌트에서는 props.theme으로 데이터에 접근하여 버튼에 어두운 테마를 적용합니다.
이처럼 props를 통해서 데이터를 전달하는 기존 방식은 실제 데이터를 필요로 하는 컴포넌트까지의 깊이가 깊어질수록 복잡해집니다. 또한, 반복적인 코드를 계속해서 작성해야 하므로 비효율적이고 객관적이지도 않습니다.
// 컨텍스트는 데이터를 매번 컴포넌트를 통해 전달할 필요 없이 컴포넌트 트리로 곧바로 전달하게 해줍니다.
// 여기에서는 현재 테마를 위한 컨텍스트를 생성하며, 기본값은 'light' 입니다.
const ThemeContext = React.createContext('light');
// Provider를 사용하여 하위 컴포넌트들에게 현재 테마 데이터들을 전달합니다.
// 모든 하위 컴포너트들은 컴포넌트 트리 하단에 얼마나 깊이 있는지에 관계없이 데이터를 읽을 수 있습니다.
// 여기에서는 현재 테마값으로 'dark'를 전달하고 있습니다.
function App(props) {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
// 이제 중간에 위치한 컴포넌트는 테마 데이터를 하위 컴포넌트로 전달할 필요가 없습니다.
function Toolbar(props) {
return (
<div>
<ThemeButton />
</div>
);
}
function ThemeButton(props) {
// 리액트는 가장 가까운 상위 테마 Provider를 찾아서 해당되는 값을 사용합니다.
// 만약 해당되는 Provider가 없을 경우 기본값(여기에서는 'light')을 사용합니다.
// 여기에서는 상위 Provider가 있기 때문에 현재 테마의 값은 'dark'가 됩니다.
return (
<ThemeContext.Consumer>
{value => <Button theme={value} />}
</ThemeContext.Consumer>
);
}
이렇게 하면 Provider의 모든 하위 컴포넌트가 얼마나 깊이 위치해 있는지 관계없이 컨텍스트의 데이터를 읽을 수 있습니다.
컴포넌트와 Context가 연동되면 재사용성이 떨어지기 때문에 무조건 Context를 사용하는 것은 좋지 않습니다.
다른 레벨의 많은 컴포넌트가 데이터를 필요로 하는 경우가 아니라면 기존에 사용하던 방식대로 prop을 통해 데이터를 전달하는 컴포넌트 컴포지션 방법이 더 적합합니다.
// Page 컴포넌트는 PageLayout 컴포넌트를 렌더링
<Page user={user} avatarSize={avatarSize} />
// PageLayout 컴포넌트는 NavigatorBar 컴포넌트를 렌더링
<PageLayout user={user} avatarSize={avatarSize} />
// NavigatorBar 컴포넌트는 Link 컴포넌트를 렌더링
<NavigatorBar user={user} avatarSize={avatarSize} />
// Link 컴포넌트는 Avatar 컴포넌트를 렌더링
<Link href={user.permalink}>
<Avatar user={user} avatarSize={avatarSize} />
</Link>
가장 하위 레벨에 위치한 Avatar 컴포넌트가 user와 avatarSize를 필요로 하기 때문에 이를 위해 여러 단계에 걸쳐서 props을 통해 user와 avatarSize를 전달해주고 있습니다.
Context를 사용하지 않고 문제를 해결하고 싶다면, Avatar 컴포넌트를 변수에 저장하여 직접 넘겨주는 방법이 있습니다.
function Page(props) {
const user = props.user;
const userLink = (
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
);
// Page 컴포넌트는 PageLayout 컴포넌트를 렌더링
// 이 때 props로 userLink를 함께 전달함.
return <PageLayout userLink={userLink} />;
}
// PageLayout 컴포넌트는 NavigatorBar 컴포넌트를 렌더링
<PageLayout userLink={...} />
// NavigatorBar 컴포넌트는 props로 전달받은 userLink element를 리턴
<NavigatorBar userLink={...} />
user와 avatarSize가 props로 들어간 Avatar 컴포넌트를 userLink라는 변수에 저장한 뒤에 해당 변수를 하위 컴포넌트로 넘겼습니다. 이렇게 하면 가장 상위 레벨에 있는 Page 컴포넌트만 Avatar 컴포넌트에서 필요로 하는 user와 avatarSize에 대해 알고 있으면 됩니다.
이런 방식은 중간 레벨의 컴포넌트를 통해 전달해야 하는 props를 없애고 코드를 더욱 간결하게 만들어줍니다. 또한 최상위 컴포넌트에 좀 더 많은 권한을 부여해줍니다.
하지만 데이터가 많아질수록 상위 컴포넌트에 몰리기 때문에 상위 컴포넌트는 점점 더 복잡해지고 하위 컴포넌트는 너무 유연해지는 단점이 있습니다.
function Page(props) {
const user = props.user;
const topBar = (
<NavigationBar>
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
</NavigationBar>
);
const content = <Feed user={user} />;
return (
<PageLayout
topBar={topBar}
content={content}
/>
);
}
앞에서 사용한 방법을 응용해서 하위 컴포넌트르 여러 개의 변수로 나눠서 전달할 수도 있습니다. 하위 컴포넌트의 의존성을 상위 컴포넌트와 분리할 필요가 있는 대부분의 경우에 적합합니다.
리액트에서 제공하는 Context API를 통해 컨텍스트를 사용할 수 있습니다. 컨텍스트를 사용하려면 먼저 컨텍스트를 생성해야 합니다.
const MyContext = React.createContext(기본값);
리액트에서 렌더링이 일어날 때 컨텍스트 객체를 구독하는 하위 컴포넌트가 나오면 현재 컨텍스트의 값을 가장 가까이에 있는 상위 레벨의 Provider로부터 받아오게 됩니다. 만약 상위 레벨에 매칭되는 Provider가 없다면 기본값이 사용됩니다. 기본값으로 undefinded를 넣으면 기본값이 사용되지 않습니다.
Context.Provider
하위 컴포넌트들이 해당 컨텍스트의 데이터를 받을 수 있도록 설정해야 합니다. 모든 컨텍스트 객체는 Provider 라는 데이터를 제공해주는 리액트 컴포넌트를 갖고 있습니다. Context.Provider 컴포넌트로 하위 컴포넌트들을 감싸주면 모든 하위 컴포넌트들이 해당 컨텍스트의 데이터에 접근할 수 있게 됩니다.
<MyContext.Provider value={/* some value */}>
Provider 컴포넌트에는 value라는 prop이 있으며, Provider 컴포넌트 하위에 있는 컴포넌트들에게 전달됩니다. 이 하위 컴포넌트들이 데이터를 소비한다는 뜻에서 consumer 컴포넌트라고 부릅니다. consumer 컴포넌트는 컨텍스트의 값의 변화를 지켜보다가 만약 값이 변경되면 재렌더링 됩니다.
참고로 하나의 Provider 컴포넌트는 여러 개의 consumer 컴포넌트와 연결될 수 있으며, 여러 개의 Provider 컴포넌트는 중첩되어 사용될 수 있습니다.
Provider 컴포넌트로 감싸진 모든 consumer 컴포넌트는 Provider 컴포넌트의 value prop이 바뀔 때마다 재렌더링됩니다. Provider 컴포넌트가 재렌더링될 때마다 모든 하위 consumer 컴포넌트가 재렌더링 됩니다.
이 때 값의 변화를 판단하는 기준은 자바스크립트 객체의 object.is라는 함수와 같은 방식으로 판단합니다.
Provider 컴포넌트가 재렌더링될 때마다 모든 하위 consumer 컴포넌트가 재렌더링 되는걸 방지하기 하려면, state를 선언하고 state의 값을 Provider에 넣어 불필요한 재렌더링을 막을 수 있습니다.
function App(props) {
const [value, setValue] = useState({ something: 'something' });
return (
<MyContext.Provider value={value}>
<Toolbar />
</MyContext.Provider>
);
}
Provider 하위에 있는 class 컴포넌트에서 context의 데이터에 접근하기 위해 사용하는 것입니다. class 컴포넌트는 거의 사용하지 않기 때문에 참고만 하면 됩니다.
class MyClass extends React.Component {
componentDidMount() {
let value = this.context;
/* MyContext의 값을 이용하여 원하는 작업을 수행 가능 */
}
componentDidUpdate() {
let value = this.context;
/* ... */
}
componentWillUnmount() {
let value = this.context;
/* ... */
}
render() {
let value = this.context;
/* ... */
}
}
MyClass.contextType = MyContext;
MyClass.contextType = MyContext라고 해주면 MyClass라는 클래스 컴포넌트는 MyContext의 데이터에 접근할 수 있게 됩니다. 클래스 컴포넌트에 있는 contextType 속성에는 react.createContext 함수를 통해 생성된 context 객체가 대입될 수 있습니다. 이 속성을 사용하게 되면 this.context를 통해 상위에 있는 Provider 중에서 가장 가까운 것의 값을 가져올 수 있습니다.
렌더 함수를 포함한 모든 생명주기 함수 어디에서든지 this.context를 사용할 수 있습니다. 참고로 이 API를 사용하면 단 하나의 컨텍스트만을 구독할 수 있습니다.
컨텍스트의 데이터를 구독하는 컴포넌트입니다. 클래스 컴포넌트에서는 Class.contextType을 사용하고, 함수 컴포넌트에서는 Context.Consumer을 사용합니다.
<MyContext.Consumer>
{value => /* 컨텍스트의 값에 따라서 컴포넌트들을 렌더링 */}
</MyContext.Consumer>
컴포넌트의 자식으로 함수가 올 수 있는데, 이것을 function as a child라고 부릅니다. Context.Consumer으로 감싸주면 자식으로 들어간 함수가 현재 컨텍스트의 value를 받아서 리액트 노드로 리턴하게 됩니다. 이 때 함수로 전달되는 value는 Provider의 value prop과 동일합니다.
만약 상위 컴포넌트에 Provider가 없다면 이 value 파라미터는 create context를 호출할 때 넣는 기본 값과 동일한 역할을 합니다.
컴포넌트의 자식으로 함수를 사용하는 방법입니다.
// children이라는 prop을 직접 선언하는 방식
<Profile children={naem => <p>이름: {name}</p>} />
// Profile 컴포넌트로 감싸서 children으로 만드는 방식
<Profile>{name => <p>이름: {name}</p>}</Profile>
리액트에서는 기본적으로 하위 컴포넌트들을 children이라는 prop으로 전달해주는데, children으로 컴포넌트 대신 함수를 사용할 수 있습니다.
컨텍스트 객체는 displayName이라는 문자열 속성을 가집니다. 또한 크롬의 리액트 개발자 도구에서는 Context의 Provider나 Consumer를 표시할 때 이 displayName를 표시해 줍니다.
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
// 개발자 도구에 "MyDisplayName.Provider"로 표시됨
<MyContext.Provider>
// 개발자 도구에 "MyDisplayName.Consumer"로 표시됨
<MyContext.Consumer>
context.Provider를 중첩해서 사용하면 여러 개의 Context를 사용할 수 있습니다.
// 테마를 위한 컨텍스트
const ThemeContext = React.createContext('light');
// 로그인 한 사용자를 위한 컨텍스트
const UserContext = React.createContext({
name: 'Guest',
});
class App extends React.Component {
render () {
const { signedInUser, theme } = this.props;
// App Component that provides initial context values
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}
ThemeContext와 UserContext를 사용합니다. App 컴포넌트에서는 각 컨텍스트에 대해 2개의 Provider를 사용하여 자식 컴포넌트인 Layout을 감싸주었습니다.
function Layout() {
return (
<div>
<Sidebar />
<Content />
</div>
);
}
// 컨텍스트 컴포넌트는 두 개의 컨텍스트로부터 값을 가져와서 렌더링함
function Content() {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<ProfilePage user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
실제 컨텍스트 데이터를 사용하는 Content 컴포넌트에서는 2개의 Consumer 컴포넌트를 사용하여 데이터를 전달하고 있습니다.
이렇게 하면 여러 개의 컨텍스트를 동시에 사용할 수 있습니다. 하지만 2개 또는 그 이상의 컨텍스트의 값이 자주 함께 사용될 경우 모든 값을 한 번에 제공해주는 별도의 render prop 컴포넌트를 직접 만드는 것을 고려해야 합니다.
함수 컴포넌트에서 컨텍스트를 사용하기 위해 컴포넌트를 매번 Consumer 컴포넌트로 감싸주는 것보다 useContext 훅을 사용하면 더 간단합니다.
function MyComponent(props) {
const value = useContext(MyContext);
return (
...
)
}
react.createContext 함수 호출로 생성된 컨텍스트 객체를 인자로 받아서 현재 컨텍스트의 값을 리턴합니다. useContext 훅을 사용하면 컨텍스트의 값을 다른 방식과 동일하게 컴포넌트 트리 상에서 가장 가까운 상위 Provider로부터 받아오게 됩니다.
만약 컨텍스트의 값이 변경되면 변경된 값과 함께 useContext 훅을 사용하는 컴포넌트가 재렌더링 됩니다. 만약 useContext 훅을 사용하는 컴포넌트의 렌더링이 꽤 무거운 작업일 경우, 별도로 최적화 작업을 해줄 필요가 있습니다.
// 올바른 사용법
useContext(MyContext);
// 잘못된 사용법
useContext(MyContext.Consumer);
useContext(MyContext.Provider);