Context
Context 를 사용하면 일일이 props 를 내려보내주지 않아도 데이터를 컴포넌트 트리 아래쪽으로 전달할 수 있습니다.
전형적인 React 어플리케이션에서, 데이터는 props 를 통해 위에서 아래로 (부모에서 자식으로) 전달됩니다.
하지만 이런 방식은 몇몇 유형의 props 에 대해서는 굉장히 번거로운 방식일 수 있습니다.
(예를 들어 언어 설정, UI 테마 등) 어플리케이션의 많은 컴포넌트들에서 이를 필요로 하기 때문입니다.
Contetxt 를 사용하면 prop 을 통해 트리의 모든 부분에 직접 값을 넘겨주지 않고도, 값을 공유할 수 있습니다.
언제 Context 를 사용해야 할까요?
Context 는 React 컴포넌트 트리 전체에 걸쳐 데이터를 공유하기 위해 만들어졌습니다.
그러한 데이터로는 로그인 된 사용자의 정보, 테마, 언어 설정 등이 있을 수 있겠죠.
예를 들어, 아래 코드에서는 Button 컴포넌트의 스타일링을 위해 “theme” prop 을 일일이 엮어주고 있습니다:
class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}
function Toolbar(props) {
// Toolbar 컴포넌트에서 별도의 "theme" prop을 받아서
// ThemedButton 컴포넌트에 이를 넘겨주어야 합니다.
// 만약 앱에서 사용되는 모든 버튼에 theme prop을 넘겨주어야 한다면
// 이는 굉장히 힘든 작업이 될 것입니다.
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
function ThemedButton(props) {
return <Button theme={props.theme} />;
}
Context 를 사용하면, 중간 계층에 위치하는 엘리먼트에 props 를 넘겨주는 작업을 피할 수 있습니다:
// Context를 사용하면 prop을 일일이 엮어주지 않고도
// 컴포넌트 트리의 깊은 곳에 값을 넘겨줄 수 있습니다.
// 테마에 대한 context를 만들어줍시다. ("light"를 기본값으로 합니다.)
const ThemeContext = React.createContext("light");
class App extends React.Component {
render() {
// Provider를 사용해서 현재 테마를 트리 아래쪽으로 넘겨줍시다.
// 어떤 컴포넌트든 이 값을 읽을 수 있습니다. 아주 깊은 곳에 위치해있더라도 말이죠.
// 아래에서는, "dark"라는 값을 넘겨주었습니다.
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// 이제 더이상 중간 계층에 있는 컴포넌트에서
// theme prop을 넘겨줄 필요가 없습니다.
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton(props) {
// 테마 context를 읽어오려면 Consumer를 사용하세요.
// React는 가장 가까운 Provider를 찾아서 그 값을 사용할 것입니다.
// 이 예제에서, theme 값은 "dark"가 됩니다.
return (
<ThemeContext.Consumer>
{theme => <Button {...props} theme={theme} />}
</ThemeContext.Consumer>
);
}
API
React.createContext
const { Provider, Consumer } = React.createContext(defaultValue);
{ Provider, Consumer } 쌍을 만듭니다. React 가 context Consumer 를 렌더링하면,
같은 context 로부터 생성된 가장 가까운 상위 Provider 에서 현재 context 의 값을 읽어옵니다.
defaultValue 인수는 오직 상위에 같은 context 로부터 생성된 Provider 가 없을 경우에만 사용됩니다.
이 기능을 통해 Provider 없이도 컴포넌트를 손쉽게 테스트해볼 수 있습니다.
주의: Provider 에서 undefined 를 넘겨줘도 Consumer 에서 defaultValue 를 사용되지는 않습니다.
Provider
<Provider value={/* some value */}>
Context 의 변화를 Consumer 에게 통지하는 React 컴포넌트입니다.
value prop 을 받아서 이 Provider 의 자손인 Consumer 에서 그 값을 전달합니다.
하나의 Provider 는 여러 Consumer 에 연결될 수 있습니다.
그리고 Provider 를 중첩해서 트리의 상위에서 제공해준 값을 덮어쓸 수 있습니다.
Consumer
<Consumer>
{value => /* render something based on the context value */}
</Consumer>
Context 의 변화를 수신하는 React 컴포넌트입니다.
function as a child 패턴을 사용합니다.
함수는 현재 context 의 값을 받아서 React 노드를 반환해야 합니다.
트리 상위의 가장 가까이 있는 Provider 의 value prop 이 이 함수에 전달됩니다.
만약 트리 상위에 Provider 가 없다면, createContext()에 넘겨진 defaultValue 값이 대신 전달됩니다.
Provider 의 자손인 모든 Consumer 는 Provider 의 value prop 이 바뀔 때마다 다시 렌더링됩니다.
이는 shouldComponentUpdate 의 영향을 받지 않으므로,
조상 컴포넌트의 업데이트가 무시된 경우라 할지라도 Consumer 는 업데이트될 수 있습니다.
Object.is 알고리즘을 통해 이전 값과 새 값을 비교함으로써 value prop 이 바뀌었는지를 결정합니다.
Example - 값이 변하는 Context
theme-context.js
export const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
export const ThemeContext = React.createContext(
themes.dark // default value
);
themed-button.js
import { ThemeContext } from "./theme-context";
function ThemedButton(props) {
return (
<ThemeContext.Consumer>
{theme => (
<button {...props} style= />
)}
</ThemeContext.Consumer>
);
}
export default ThemedButton;
app.js
import { ThemeContext, themes } from "./theme-context";
import ThemedButton from "./themed-button";
// ThemedButton를 사용하는 중간 계층의 컴포넌트입니다.
function Toolbar(props) {
return <ThemedButton onClick={props.changeTheme}>Change Theme</ThemedButton>;
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
theme: themes.light
};
this.toggleTheme = () => {
this.setState(state => ({
theme: state.theme === themes.dark ? themes.light : themes.dark
}));
};
}
render() {
// ThemeProvider 내부의 ThemedButton은
// state에 저장되어 있는 theme을 사용하는 반면, 바깥에서는
// 기본값으로 설정된 dark 테마가 사용됩니다.
return (
<Page>
<ThemeContext.Provider value={this.state.theme}>
<Toolbar changeTheme={this.toggleTheme} />
</ThemeContext.Provider>
<Section>
<ThemedButton />
</Section>
</Page>
);
}
}
ReactDOM.render(<App />, document.root);
Example - 중첩된 컴포넌트에서 context 갱신하기
컴포넌트 트리의 깊은 곳에 위치한 컴포넌트에서 context 의 값을 갱신해야 하는 경우가 종종 있습니다.
이런 경우 함수를 아래로 넘겨주어 consumer 가 context 의 값을 갱신하게 만들 수 있습니다:
theme-context.js
// createContext에 넘겨주는 기본값의 모양이
// 실제 consumer에서 사용되는 값과 일치하도록 신경써주세요!
export const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {}
});
theme-toggler-button.js
import { ThemeContext } from "./theme-context";
function ThemeTogglerButton() {
// ThemeTogglerButton 컴포넌트는 theme 뿐만 아니라
// toggleTheme 함수도 받고 있습니다.
return (
<ThemeContext.Consumer>
{({ theme, toggleTheme }) => (
<button
onClick={toggleTheme}
style=
>
Toggle Theme
</button>
)}
</ThemeContext.Consumer>
);
}
export default ThemeTogglerButton;
app.js
import { ThemeContext, themes } from "./theme-context";
import ThemeTogglerButton from "./theme-toggler-button";
class App extends React.Component {
constructor(props) {
super(props);
this.toggleTheme = () => {
this.setState(state => ({
theme: state.theme === themes.dark ? themes.light : themes.dark
}));
};
// state가 갱신 함수도 포함하고 있기 때문에, 갱신함수 역시
// provider로 넘겨질 것입니다.
this.state = {
theme: themes.light,
toggleTheme: this.toggleTheme
};
}
render() {
// 전체 state를 provider에 넘겨줍니다.
return (
<ThemeContext.Provider value={this.state}>
<Content />
</ThemeContext.Provider>
);
}
}
function Content() {
return (
<div>
<ThemeTogglerButton />
</div>
);
}
ReactDOM.render(<App />, document.root);
Example - 여러 context 에서 값 넘겨받기
각 consumer 를 별도의 노드로 만들어줄 수 있습니다.
// Theme context, default to light theme
const ThemeContext = React.createContext("light");
// Signed-in user context
const UserContext = React.createContext({
name: "Guest"
});
class App extends React.Component {
render() {
const { signedInUser, theme } = this.props;
// App 컴포넌트에서 context 값을 제공하고 있습니다.
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}
function Layout() {
return (
<div>
<Sidebar />
<Content />
</div>
);
}
// 하나의 컴포넌트에서 여러 context의 값을 가져올 수 있습니다.
function Content() {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => <ProfilePage user={user} theme={theme} />}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
둘 이상의 context 가 자주 함께 사용된다면, 이를 묶은 render prop 컴포넌트를 만드는 것을 고려해볼 수도 있습니다.
라이프사이클 메소드에서 context 에 접근하기
라이프사이클 메소드에서 context 값을 사용해야 하는 경우가 있습니다.
이 때에는 값을 prop 으로 넘겨준 뒤, 일반적인 prop 을 다루듯이 다루면 됩니다.
class Button extends React.Component {
componentDidMount() {
// ThemeContext value is this.props.theme
}
componentDidUpdate(prevProps, prevState) {
// Previous ThemeContext value is prevProps.theme
// New ThemeContext value is this.props.theme
}
render() {
const { theme, children } = this.props;
return <button className={theme ? "dark" : "light"}>{children}</button>;
}
}
export default props => (
<ThemeContext.Consumer>
{theme => <Button {...props} theme={theme} />}
</ThemeContext.Consumer>
);
Context 주의사항
Context 는 consumer 를 다시 렌더링해야하는 시점을 결정하기 위해 값의 참조가 동일한지를 비교하기 때문에, provider 의 부모가 렌더링될 때 consumer 가 불필요하게 다시 렌더링되는 문제가 생길 수 있습니다. 예를 들어, 아래 코드는 Provider 가 다시 렌더링될 때 모든 consumer 를 다시 렌더링시키는데, 이는 value 에 매번 새로운 객체가 넘겨지기 때문입니다:
class App extends React.Component {
render() {
return (
<Provider value=>
<Toolbar />
</Provider>
);
}
}
이 문제를 회피하려면, value 로 사용할 객체를 부모의 state 에 저장하세요:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: { something: "something" }
};
}
render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}
[ Context : 꼭 알아야 하는 두가지 교훈]
- 최상위 컴포넌트에서 최하위 컴포넌트로 손쉽게 데이터나 함수 를 내릴 수 있다.
- 외부세계의 역할만을 책임지는 콤포넌트를 만듦으로써 유지보수성을 높일 수 있다.