이번 프로젝트에서 Context api를 통해 상태 관리를 진행해 보기로 해서 Context api에 대해 공부한 글을 정리해 보려고 한다. 지금까지 Context api도 제대로 사용해 본 적 없으면서 편한 상태 관리 도구들을 덥썩 썼던 게 후회가 된다... React의 많은 상태 관리 도구가 Context api를 기반으로 동작한다고 하니, Context api를 공부해 보는 게 상태관리의 전반적인 개념을 이해하는 데 많은 도움이 될 수 있을 것 같다.
💡 React에서 단계마다 일일이
props
를 넘겨주지 않고도, 컴포넌트 트리 전체에 데이터를 제공할 수 있게끔 하는 도구를 말한다.
props
를 통해 전달됨props
의 경우, 이 과정이 번거로울 수 있다!class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}
function Toolbar(props) {
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
class ThemedButton extends React.Component {
render() {
return <Button theme={this.props.theme} />;
}
}
App
→ Toolbar
→ ThemedButton
theme
색깔이 필요한 컴포넌트는 ThemedButton
인데,Toolbar
는 오로지 ThemedButton
에게 theme
을 전달해 주기만을 위해 App
에게서 theme
을 받음Toolbar
스스로는 theme
을 사용 안 할 거고 필요없는데, App
에서는 ThemedButton
이 아닌 Toolbar
를 렌더링 하고 있기 때문에, 어쩔 수 없이 theme
을 받아서 Toolbar
에 전달해 주는 과정이 필요한 것const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
React.createContext()
를 통해 'light'를 기본값으로 하는 context인 ThemeContext
생성Provider
를 통해 하위 트리에 값을 보내줄 수 있음Provider
로 값을 전달하면, 해당 컴포넌트 아래에 아무리 깊숙히 있어도, 모든 컴포넌트가 이 값을 읽을 수 있다!Toolbar
컴포넌트에서 일일이 테마값을 넘겨줄 필요 XThemedButton
에서는 현재 선택된 테마 값을 읽기 위해 contextType
지정React
는 가장 가까이 있는 테마 Provider를 찾아 그 값을 사용할 것Toolbar
에서 또 Provider
로 테마 값을 해당 값이 쓰였을 거라는 말인듯여러 레벨에 걸쳐
props
를 넘기는 걸 대체하기 위해 context보다 컴포넌트 합성이 더 간단한 해결책이 되어줄 수 있음!!
<Page user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<Link href={user.permalink}>
<Avatar user={user} size={avatarSize} />
</Link>
Link
와 Avatar
컴포넌트에, user
와 avatarSize
를 props
로 전달해야 하는 Page
컴포넌트가 있다고 생각해 보자Avatar
컴포넌트 뿐인데, user
와 avatarSize
props를 여러 단계에 걸쳐 보내줘야 해서 번거로움Avatar
컴포넌트 자체를 넘겨주면 context를 사용하지 않고 이를 해결할 수 있다!!function Page(props) {
const user = props.user;
const userLink = (
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
);
return <PageLayout userLink={userLink} />;
}
<Page user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<PageLayout userLink={...} />
// ... 그 아래에 ...
<NavigationBar userLink={...} />
// ... 그 아래에 ...
{props.userLink}
Link
와 Avatar
컴포넌트 자체를 userLink
변수에 담아 props
로 전달해주는 방식Link
와 Avatar
가 user
와 avatarSize
props를 사용한다는 걸 알아야 하는 건, 가장 위에 있는 Page
뿐!props
의 수는 줄고,function Page(props) {
const user = props.user;
const content = <Feed user={user} />;
const topBar = (
<NavigationBar>
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
</NavigationBar>
);
return (
<PageLayout
topBar={topBar}
content={content}
/>
);
}
render
props를 이용하면, 렌더링 되기 전부터 자식 컴포넌트가 부모 컴포넌트의 소통하게 할 수도 있음React.createContext
const MyContext = React.createContext(defaultValue);
Provider
로부터 현재값을 읽음defaultValue
매개변수는 트리 안에서 적절한 Provider
를 찾지 못했을 때만 쓰이는 값Provider
를 통해 undefined
를 값으로 보낸다고 해도, 구독 컴포넌트들이 defaultValue
를 읽지는 않고, 그대로 undefined
를 읽지는 않는다는 점에 유의하자!Context.Provider
<MyContext.Provider value={/* 어떤 값 */}>
Provider
는, context를 구독하는 컴포넌트들에게 context의 변화를 알리는 역할value
prop을 받아서 그 값을 하위 컴포넌트에 전달함Provider
하위에 또 다른 Provider
배치도 가능하며, 이 경우 하위 Provider
의 값이 우선시됨 (가까이에 있으니까)Provider
하위에서 context를 구독하는 모든 컴포넌트는, Provider
의 value
prop이 바뀔 때마다 다시 렌더링됨!!!Provider
로부터 하위 consumer로의 전파는, shouldComponentUpdate
메서드가 적용되지 않으므로, 상위 컴포넌트가 업데이트를 건너 뛰더라도 consumer가 업데이트 됨Object.is
와 동일한 매커니즘을 통해 이전 값와 새로운 값을 비교하여 측정됨value
로 보내는 경우 문제가 생길 수 있음 (유의해서 사용하자)Class.contextType
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;
React.createContext()
로 생성한 Context 객체를, 원하는 클래스의 contextType
프로퍼티로 지정 가능contextType
으로 이 Context 객체를 가져요~ 라는 걸 알려주는 느낌인듯this.context
를 이용해, 해당 context의 가장 가까운 Provider
를 찾아 그 값을 읽을 수 있게 됨render
를 포함한 모든 컴포넌트 생명주기 메서드에서 사용 가능class MyClass extends React.Component {
static contextType = MyContext;
render() {
let value = this.context;
/* context 값을 이용한 렌더링 */
}
}
Context.Consumer
<MyContext.Consumer>
{value => /* context 값을 이용한 렌더링 */}
</MyContext.Consumer>
Context.Consumer
의 자식은 함수여야 함value
매개변수 값은, 해당 context의 Provider
중 상위 트리에서 가장 가까운 Provider
의 value
prop과 동일Provider
가 없다면, value
매개변수 값은 createContext()
에 보냈던 defaultValue
값Context.displayName
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" in DevTools
<MyContext.Consumer> // "MyDisplayName.Consumer" in DevTools
displayName
문자열 속성 설정 가능theme-context.js
export const themes = {
light: {
foreground: '#000000',
background: '#eeeeee',
},
dark: {
foreground: '#ffffff',
background: '#222222',
},
};
export const ThemeContext = React.createContext(
themes.dark // 기본값
);
dark
를 가짐themed-button.js
import {ThemeContext} from './theme-context';
class ThemedButton extends React.Component {
render() {
let props = this.props;
let theme = this.context;
return (
<button
{...props}
style={{backgroundColor: theme.background}}
/>
);
}
}
ThemedButton.contextType = ThemeContext;
export default ThemedButton;
ThemedButton
은 contextType
으로 ThemeContext
를 가짐app.js
import {ThemeContext, themes} from './theme-context';
import ThemedButton from './themed-button';
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() {
return (
<Page>
<ThemeContext.Provider value={this.state.theme}>
<Toolbar changeTheme={this.toggleTheme} />
</ThemeContext.Provider>
<Section>
<ThemedButton />
</Section>
</Page>
);
}
}
const root = ReactDOM.createRoot(
document.getElementById('root')
);
root.render(<App />);
ThemeProvider
안에 있는 ThemedButton
, 즉 Toolbar
안에 있는 ThemedButton
은 state
로부터 theme
값을 읽지만,Provider
밖에 있는 그냥 ThemedButton
은 기본값인 dark
사용theme-context.js
export const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {},
});
createContext
에 보내는 기본값의 모양을, 하위 컴포넌트가 받고 있는 매개변수 모양과 동일하게 만들어야 하는 것 잊지 말자!!theme-toggler-button.js
import {ThemeContext} from './theme-context';
function ThemeTogglerButton() {
return (
<ThemeContext.Consumer>
{({theme, toggleTheme}) => (
<button
onClick={toggleTheme}
style={{backgroundColor: theme.background}}>
Toggle Theme
</button>
)}
</ThemeContext.Consumer>
);
}
export default ThemeTogglerButton;
ThemeTogglerButton
은 context로부터 theme
값과 toggleTheme
메서드를 함께 받고 있음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,
}));
};
this.state = {
theme: themes.light,
toggleTheme: this.toggleTheme,
};
}
render() {
return (
<ThemeContext.Provider value={this.state}>
<Content />
</ThemeContext.Provider>
);
}
}
function Content() {
return (
<div>
<ThemeTogglerButton />
</div>
);
}
const root = ReactDOM.createRoot(
document.getElementById('root')
);
root.render(<App />);
state
에 업데이트 메서드도 포함되어 있으므로, 이것도 context Provider
를 통해 전달됨Provider
에 state
전체 넘겨줌const ThemeContext = React.createContext('light');
const UserContext = React.createContext({
name: 'Guest',
});
class App extends React.Component {
render() {
const {signedInUser, theme} = this.props;
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}
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>
);
}
ThemeContext
UserContext
App
컴포넌트Content
를 보면, ThemeContext
와 UserContext
모두 구독하고 있는 것을 볼 수 있음!!render
prop 컴포넌트를 만드는 것도 고려해 보자Provider
의 부모가 렌더링 될 때마다 불필요하게 하위 컴포넌트가 다시 렌더링 되는 문제가 생길 수도 있음class App extends React.Component {
render() {
return (
<MyContext.Provider value={{something: 'something'}}>
<Toolbar />
</MyContext.Provider>
);
}
}
App
에서는 value
가 바뀔 때마다 매번 새로운 객체가 생성되므로, Provider
가 렌더링 될 때마다 그 하위에서 구독하고 있는 컴포넌트 모두가 다시 렌더링됨...class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {something: 'something'},
};
}
render() {
return (
<MyContext.Provider value={this.state.value}>
<Toolbar />
</MyContext.Provider>
);
}
}