이번 프로젝트에서 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 → ThemedButtontheme 색깔이 필요한 컴포넌트는 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>
);
}
ThemeContextUserContextApp 컴포넌트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>
);
}
}