도움말 - 글감 수집하기 (인용)

도움말 - 부분 리뷰 작성하기

React 이해 3 - Context (Provider) 사용 해보기

리액트(React)로 컴포넌트를 만들 때, 상태 값 관리는 보통 props 또는 state로 관리한다. 리액트에는 사실 props와 state 외에도 상태를 관리하는 속성이 있는데, 이것이 context이다. 대부분의 케이스에서는 context를 쓸일이 없을 수 있다. 하지만, 우리가 많이 사용하는 라이브러리에서는 이 context 속성을 이미 많이 이용하고 있다.

상태 관리(state management) 라이브러리로 react-redux, mobx-react 또는 react-apollo 등을 사용하고 있거나, 사용 했다면 이미 이 context를 사용 하고 있는 것이다. 그리고 styled-componentsmaterial-ui 등의 ui 라이브러리에서도 이 context를 사용하고 있다. 보통 이러한 라이브러리를 이용하면 Provider라는 이름의 컴포넌트를 제공하는데, 이 컴포넌트 안에서 context 값을 핸들링 하고 있다. react-redux를 기준으로 한다면 앱을 실행할때 아래와 같은 방법으로 앱을 선언 할 것이다:

<Provider store={store}>
  <App />
</Provider>

이런 context가 유용하게 쓰이는 곳은 앱 상태 관리 뿐만 아니라, 공통 스타일, 앱의 테마 적용 등에 활용 할 수 있다.

그럼, 이 context를 어떻게 사용하는지 알아보고, 사용시에 주의할 점 등을 예제를 통해 알아보자.

여기서 전체 소스를 확인 할 수 있다.

예제 ThemeProvider 제작

먼저, ThemeProvider라는 컴포넌트를 만든다. 이 컴포넌트는 전체 앱에서 사용할 context 값을 만드는 컴포넌트이다. render() 메쏘드에서는 그냥 자식 컴포넌트를 그대로 렌더링 한다.

import React, { Children, Component } from 'react';
import PropTypes from 'prop-types';
class ThemeProvider extends Component {
static childContextTypes = {
theme: PropTypes.object,
};
getChildContext() {
return {
theme: {
button: {
borderRadius: 3,
border: 0,
color: 'white',
height: 48,
padding: '0 30px',
boxShadow: '0 3px 5px 2px #ddd',
fontSize: 20,
},
color: {
primary: 'linear-gradient(to right, #0cebeb, #20e3b2, #29ffc6)',
secondary: 'linear-gradient(to right, #f7971e, #ffd200)',
},
},
};
}
render() {
return Children.only(this.props.children);
}
}
export default ThemeProvider;

기본적인 리액트 컴포넌트를 만들 때와 다른 점은 childContextTypes static을 선언한것과 getChildContext 메쏘드에서 theme 객체(object)를 리턴 하도록 했고, render 함수에서는 children을 렌더링 하도록 했다. 이렇게 하면 ThemeProvider의 자식 컴포넌트들은 context의 값을 사용 할 수 있도록 준비가 된것이다.

그럼 이제 간단한 Button component를 class component, functional component로 만들어서 어떻게 context 값을 사용하는지 알아보자.

class component

import React, { Component } from 'react';
import PropTypes from 'prop-types';
class StatefulButtonWithContext extends Component {
static propTypes = {
color: PropTypes.string,
};
static contextTypes = {
theme: PropTypes.object,
};
render() {
const { color, ...rest } = this.props;
const { theme } = this.context;
return (
<button
{...rest}
style={{
...theme.button,
background: theme.color[color],
}}
/>
);
}
}
export default StatefulButtonWithContext;

class 기반 component에서는 this.context로 context에 접근 할 수 있다. 여기서도 위에서 ThemeProvider와 비슷하게 contextTypes라는 static을 선언해주어야 한다. 만약 contextTypes이 선언 되어 있지 않다면, this.context 값은 빈 객체이다. 

functional component

위와 같은 컴포넌트이지만 functional component로 만들면 이렇다.

import React from 'react';
import PropTypes from 'prop-types';
const StatelessButtonWithContext = (props, context) => {
const { color, ...rest } = props;
const { theme } = context;
return (
<button
{...rest}
style={{
...theme.button,
background: theme.color[color],
}}
/>
);
};
StatelessButtonWithContext.propTypes = {
color: PropTypes.string,
};
StatelessButtonWithContext.contextTypes = {
theme: PropTypes.object,
};
export default StatelessButtonWithContext;

functional component의 경우 this로 context를 접근 하는게 아니고(당연히 functional component에는 this가 없기 때문에), component의 두번째 파라미터로 context 값을 가져올 수 있다. class component와 마찬가지로 contextTypes을 선언해주어야 한다. 

주의할 점

context 선언과 값을 받아오는 것은 쉽다. 하지만, 리액트에서 context를 사용 할 때, 매번 context 값이 필요한 컴포넌트에서 contextTypes를 선언해서 사용하는 것은 추천하지 않는다. 그 이유는 크게 2가지 이유에 있다. 먼저, context api는 변경될 가능성이 있다. 리액트 공식문서에서도 밝혔지만, 이 api 자체가 실험적인(experimental) 기능이기 때문에 변경 될 가능성이 있다. 만약 context 관련 api가 변경 된다면, 이렇게 contextTypes를 선언한 모든 컴포넌트를 변경해주어야 한다. 또한, context를 사용하기 위해 이렇게 매번 contextTypes를 선언하는 것은 불편 할 뿐만 아니라 사용하려는 값이 context의 값인지 props 값 인지 헷갈리기도 한다. 

해결 방법

이를 해결하는 방법으로 가장 일반적으로 사용되는 테크닉에는 HOC(Higher Order Component)를 만들어 사용하는 방법이 있다. HOC를 사용해서 context 값을 prop으로 주입해서, 컴포넌트에서는 일관되게 props 값만 핸들링 할 수 있게하고, 추후에 변경될지 모르는 context api에도 대비 할 수 있다(HOC 한개만 수정하면 되니 때문이다). 

그럼 withTheme이라는 HOC를 만들고, 이 HOC를 사용해보자. 

import React from 'react';
import PropTypes from 'prop-types';
const withTheme = WrappedComponent => {
return class WithTheme extends React.Component {
static contextTypes = {
theme: PropTypes.object,
};
render() {
return <WrappedComponent {...this.props} theme={this.context.theme} />;
}
};
};
export default withTheme;

이 withTheme HOC는 래핑된 컴포넌트에 this.context.theme 값을 theme prop으로 전달하는 HOC이다.

이 HOC를 사용하는 건 아래와 같다. 위의 예제로 만들어본 Button 컴포넌트와 동일 하지만, contextTypes 선언 등의 불필요한 방법이 없이 theme 값을 prop으로 받아서 처리 할 수 있게 됐다. 

import React from 'react';
import withTheme from './withTheme';
const ButtonWithTheme = ({ color, theme, ...rest }) => {
return (
<button
{...rest}
style={{
...theme.button,
background: theme.color[color],
}}
/>
);
};
export default withTheme(ButtonWithTheme);

그렇다면 ThemeProvider의 theme 객체 값을  변경하고 싶다면 어떻게 해야 할까? 예제 소스를 기준으로, 기본 theme 객체는 유지하고, primary color만 변경하고 싶다면 어떻게 수정 해야 할까?

가장 쉬운 방법은 ThemeProvider 컴포넌트에 theme prop을 만들고 그 값을 머지(merge)하는 방법이 있을 수 있다. ThemeProvider에서 theme prop을 처리 할 수 있도록 수정해보자. 우선, getChildContext 메쏘드에서도 this.props로 prop 값 접근이 가능하기 때문에 어려운 일은 아니다. 아래와 같이 theme 객체를 prop으로 받아서 기존의 객체와 머지(merge)하도록 했다.

import React, { Children, Component } from 'react';
import PropTypes from 'prop-types';
class ThemeProvider extends Component {
static childContextTypes = {
theme: PropTypes.object,
};
static propTypes = {
theme: PropTypes.object,
};
static defaultProps = {
theme: {},
};

getChildContext() {
return {
theme: {
button: {
borderRadius: 3,
border: 0,
color: 'white',
height: 48,
padding: '0 30px',
boxShadow: '0 3px 5px 2px #ddd',
fontSize: 20,
...this.props.theme.button,
},
color: {
primary: 'linear-gradient(to right, #0cebeb, #20e3b2, #29ffc6)',
secondary: 'linear-gradient(to right, #f7971e, #ffd200)',
...this.props.theme.color,
},
},
};
}
render() {
return Children.only(this.props.children);
}
}
export default ThemeProvider;

위와 같이 ThemeProvider를 수정하고 ThemeProvider를 사용하는 곳에서 theme prop을 선언하면 버튼 색상이 변경 된 것을 알 수 있다.

import React, { Component } from 'react';
import './App.css';
import ThemeProvider from './ThemeProvider';
import StatefulButtonWithContext from './StatefulButtonWithContext';
import StatelessButtonWithContext from './StatelessButtonWithContext';
import ButtonWithTheme from './ButtonWithTheme';
class App extends Component {
render() {
return (
<ThemeProvider
theme={{
color: {
primary: 'linear-gradient(to right, #74ebd5, #acb6e5)',
secondary: 'linear-gradient(to right, #c02425, #f0cb35)',
},
}}

>
<div className="App">
<div className="App-header">
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
<StatefulButtonWithContext>
button with stateful context
</StatefulButtonWithContext>
<StatelessButtonWithContext color="primary">
button with stateless context
</StatelessButtonWithContext>
<ButtonWithTheme color="secondary">
button with theme HOC
</ButtonWithTheme>
</p>
</div>
</ThemeProvider>
);
}
}
export default App;

아래 링크는 전체 소스와 워킹 데모이니, 테스트 해보고 싶다면 아래에서 확인 해보자.


이 방법 말고도 context 값을 다이나믹하게 변경 할 수도 있는데, 공식 문서에 따르면 하지 말 것을 추천하고 있다. 그렇기 때문에 여기선 따로 다루진 않겠다. 안전하게 context 값을 변경하는 방법에 대해선 이 블로그 포스트를 참고하자. 

더 읽을 거리:


리뷰