Context는 리액트 컴포넌트 간에 어떠한 값을 공유할수 있게 해주는 기능이다. 주로 Context는 전역적으로 필요한 값을 다룰 때 사용하는데, 꼭 전역적일 필요는 없다. 리액트 컴포넌트에서 Props가 아닌 또 다른 방식으로 컴포넌트간에 값을 전달하는 방법 이라고 접근하는 것이 좋다.
Props Drilling🪛
프로젝트에서 환경설정, 사용자 정보와 같은 전역적으로 사용할 데이터의 상태 관리는 어떻게 해야할까? 리액트 앱은 컴포넌트 간의 데이터를 props로 전달하기 때문에 컴포넌트 여기저기서 필요한 데이터가 있을 때는 주로 최상위 컴포넌트 App의 state에 넣어 관리한다.
다루어야 하는 데이터가 훨씬 많은 실제 프로젝트에서 props 전달에 많은 컴포넌트를 거치는 것은 유지 보수에 좋지 않다.
function App() {
return <GrandParent value="Hello World!" />;
}
function GrandParent({ value }) {
return <Parent value={value} />;
}
function Parent({ value }) {
return <Child value={value} />;
}
function Child({ value }) {
return <GrandChild value={value} />;
}
function GrandChild({ value }) {
return <Message value={value} />;
}
function Message({ value }) {
return <div>Received: {value}</div>;
}
따라서 Redux
, MobX
같은 상태 관리 라이브러리를 사용해 전역 상태 관리 작업을 편하게 처리하기도 한다. 리액트 v16.3 업데이트 이후 context API
가 많이 개선되면서 별도의 라이브러리를 사용하지 않아도 전역 상태를 쉽게 관리할 수 있다. 참고로 리덕스, 리액트 라우터, styled-components 등의 라이브러리는 context API
를 기반으로 구현되었다.
context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있다. context를 이용하면 트리 단계마다 명시적으로 props를 넘겨주지 않아도 많은 컴포넌트가 이러한 값을 공유하도록 할 수 있다.
Context는 리액트 패키지에서 createContext
함수를 불러와 만들 수 있다. Context 객체 안에는 Provider
라는 컴포넌트가 있다. 그 컴포넌트 간에 공유하고자 하는 값을 value라는 Props로 설정하면 자식 컴포넌트들에서 해당 값에 바로 접근할 수 있다.
import { createContext } from 'react';
const MyContext = createContext();
const App = () => {
return (
<MyContext.Provider value="Hello World">
<GrandParent />
</MyContext.Provider>
);
}
이렇게 하면, 원하는 컴포넌트에서 useContext
Hook을 사용해 Context에 넣은 값에 바로 접근할 수 있다. 해당 useContext
인자에는 createContext
로 만든 MyContext를 넣는다.
const Message = () => {
const value = useContext(MyContext);
return <div>Received: {value}</div>;
}
이렇게 하면 중간 중간 여러 컴포넌트를 거쳐 전달하던 Props를 지울 수 있다.
import { createContext, useContext } from 'react';
const MyContext = createContext();
function App() {
return (
<MyContext.Provider value="Hello World">
<GrandParent />
</MyContext.Provider>
);
}
function GrandParent() {
return <Parent />;
}
function Parent() {
return <Child />;
}
function Child() {
return <GrandChild />;
}
function GrandChild() {
return <Message />;
}
function Message() {
const value = useContext(MyContext);
return <div>Received: {value}</div>;
}
export default App;
만약 자식 컴포넌트에서 useContext
를 사용하고 있는데, Provider 컴포넌트로 감싸는 것을 깜빡하면, value 값을 따로 지정하지 않았기 때문에 value 값이 undefined로 조회되어 해당 값이 보여질 자리에 아무것도 나타나지 않게 된다. 이럴 경우에 기본 값을 설정하고 싶다면, createContext
함수에 인자로 기본 값을 넣어준다.
const MyContext = createContext('default value');
프로젝트 생성 후 src 디렉토리에 contexts 디렉토리를 만들고 그 안에 color.js 파일을 만든다.
$ yarn create react-app context-tutorial
🏷️contexts/color.js
createContext
함수로 ColorContext 변수에 Context 객체를 담는다. 함수의 인자로는 기본값을 전달한다.
import { createContext } from "react";
const ColorContext = createContext({color: 'black'})
export default ColorContext
context 변화를 구독하는 React 컴포넌트이다. Context.Consumer의 children은 함수여야 한다. 이 함수는 context의 현재 값을 받고 React 노드를 반환한다.
ColorContext 안에 들어있는 색상을 보여주는 ColorBox 컴포넌트를 만들어 보자. 이때 색상을 props로 받아오는 것이 아니라 ColorContext 안에 들어 있는 consumer 컴포넌트를 통해 색상을 조회한다.
🏷️components/colorBox.js
consumer 사이에 중괄호를 열어 함수를 넣어주었다. 이런 패턴을 function as a child or Render pops 라고 한다. 컴포넌트의 children 자리에 JSX, 문자열이 아닌 함수를 전달하는 것이다.
import React from "react";
import ColorContext from "../contexts/color";
const ColorBox = () => {
return(
<ColorContext.Consumer>
{value => (
<div style={{
width: '64px',
height: '64px',
background: value.color
}}
/>
)}
</ColorContext.Consumer>
)
}
export default ColorBox
🏷️App.js
import React from 'react';
import ColorBox from './components/colorBox';
const App = () => {
return <ColorBox/>
}
export default App;
Context 객체에 포함된 React 컴포넌트인 Provider는 context를 구독하는 컴포넌트들에게 context의 변화를 알리는 역할을 한다. Provider 컴포넌트는 value prop을 받아 이 값을 하위에 있는 컴포넌트에게 전달한다. 즉, provider를 사용하면 context의 value를 변경할 수 있다.
- Provider 하위에 또 다른 Provider를 배치하는 것도 가능하며, 이 경우 하위 Provider의 값이 우선시된다.
- createContext 함수의 인자로 전달한 context의 기본 상태는 provider를 사용하지 않았을 때만 사용된다.
- Provider 하위에서 context를 구독하는 모든 컴포넌트는 Provider의 value prop가 바뀔 때마다 다시 렌더링 된다.
🏷️App.js
import React from 'react';
import ColorBox from './components/colorBox';
import ColorContext from './contexts/color';
const App = () => {
return (
<ColorContext.Provider value={{color: 'blue'}}>
<div>
<ColorBox/>
</div>
</ColorContext.Provider>
)
}
export default App;
이번에는 Context에서 유동적인 값을 다뤄야 할 때 어떻게 해야 하는지 알아보자.
Context에서 유동적인 값을 관리하는 경우엔 Provider를 새로 만들어주는 것이 좋다.
🏷️contexts/color.js
ColorContext.Provider
를 렌더링하는 ColorProvider라는 컴포넌트를 새로 작성했다. 상태는 state로, 업데이트 함수는 actions로 묶어서 Provider의 value로 전달하고 있다. Context 값을 동적으로 사용할 때 반드시 묶어서 줄 필요는 없지만 이렇게 state, actions 객체를 따로 분리하면 나중에 다른 컴포넌트에서 Context 값을 사용할 때 편하다.
추가로 createContext
를 사용할 때 기본 값으로 사용할 객체를 Provider의 value에 넣는 객체의 형태와 일치시켜 주었다. 이렇게 하면 Context 코드를 볼 때 내부 값이 어떻게 구성되었는지 파악하기 쉽다.
import { createContext, useState } from "react";
const ColorContext = createContext({
state:{color: 'black', subcolor: 'red'},
actions:{
setColor: () => {},
setSubcolor: () => {}
}
})
const ColorProvider = ({children}) => {
const [color, setColor] = useState('black')
const [subcolor, setSubcolor] = useState('red')
const value = {
state: { color, subcolor},
actions: { setColor, setSubcolor }
}
return (
<ColorContext.Provider value={value}>
{children}
</ColorContext.Provider>
)
}
// const ColorConsumer = ColorContext.Consumer와 같은 의미
const {Consumer: ColorConsumer} = ColorContext
export {ColorProvider, ColorConsumer}
export default ColorContext
🏷️App.js
import React from 'react';
import ColorBox from './components/colorBox';
import { ColorProvider } from './contexts/color';
const App = () => {
return (
<ColorProvider>
<div>
<ColorBox/>
</div>
</ColorProvider>
)
}
export default App;
🏷️components/colorBox.js
import React from "react";
import { ColorConsumer } from "../contexts/color";
const ColorBox = () => {
return(
<ColorConsumer>
{({state}) => (
<>
<div style={{
width: '64px',
height: '64px',
background: state.color
}}
/>
<div style={{
width: '32px',
height: '32px',
background: state.subcolor
}}
/>
</>
)}
</ColorConsumer>
)
}
export default ColorBox
이번에는 Context의 actions에 넣어준 함수를 호출하는 컴포넌트를 만들어보자. components 디렉토리에 selectColor.js 파일을 만든다.
🏷️components/selectColor.js
마우스 왼쪽을 클릭하면 큰 정사각형 색이 변하고, 마우스 오른쪽 버튼을 클릭하면 작은 정사각형 색이 변하도록 구현하였다.
DOM Event :
onContextMenu
사용자가 요소를 마우스 오른쪽 단추로 클릭해 메뉴를 열 때 발생한다.
import React from "react";
import { ColorConsumer } from "../contexts/color";
const colors = ['red','orange','yellow','green','blue','indigo','violet']
const SelectColor = () => {
return(
<div>
<h2>색상을 선택하세요.</h2>
<ColorConsumer>
{({actions}) => (
<div style={{display: 'flex'}}>
{colors.map(color => (
<div
key={color}
style={{
background: color,
width: '24px',
height: '24px',
cursor: 'pointer'
}}
onClick={()=>actions.setColor(color)}
onContextMenu={e=>{
e.preventDefault() // 마우스 오른쪽 클릭 시 메뉴 뜨는 것을 무시
actions.setSubcolor(color)
}}
/>
))}
</div>
)}
</ColorConsumer>
<hr/>
</div>
)
}
export default SelectColor
리액트 내장 Hooks 중 useContext
를 사용하면 함수형 컴포넌트에서 context를 편하게 사용할 수 있다.
🏷️components/colorBox.js
import React, { useContext } from "react";
import ColorContext from "../contexts/color";
const ColorBox = () => {
const { state } = useContext(ColorContext)
return(
<>
<div style={{
width: '64px',
height: '64px',
background: state.color
}}
/>
<div style={{
width: '32px',
height: '32px',
background: state.subcolor
}}
/>
</>
)
}
export default ColorBox
🔔클래스형 컴포넌트에서 context를 좀 더 쉽게 사용하고 싶다면 static contextType
을 정의한는 방법이 있다. 클래스형 컴포넌트를 잘 쓰지 않기 때문에 static contextType
사용에 대해선 필요 시 알아보도록 하자.