그동안 useMemo, useCallback이 어떤 차이점을 가지고 있는지에 대해서 탐구하느라 조금 오랜시간이 걸렸습니다.. useReducer도 왜 사용하는지에 대해서도 탐구하느라 .. 그럼 이제 Context API를 배워볼까요?
● Context API 왜 쓸까요?
우리는 App.js라는 가장 부모 컴포넌트에서 state를 만들고, 이 state를 변경하기 위해서는 state끌어올리기라는 행동을 해왔습니다. 이 state를 변경하는 컴포넌트가 부모 컴포넌트의 자식 컴포넌트 또 그 자식컴포넌트라도 그에게 꼭 state를 변경할 수 있는 함수를 넘겨주곤 했씁니다.. 그럼 중간에 끼어있는 컴포넌트들은 그저 props를 전달해주는 역할밖에 하지 않음에도 props들을 전달해준 것이죠.
이를 props chain 이라고 합니다. 저는 사실 이 context API를 배우기전에 이런것들이 매우 비효율적이라고 생각했는데, context API로 쉽게 해결될일이었읍니다.
실제로 이런식으로 데이터를 주고 받고 하는 식은 프로젝트에서 유지 보수성이 낮아질 가능성이 있습니다.
따라서 리덕스, MobX같은 상태 관리 라이브러리를 사용해서 전역 상태 관리 작업을 편하게 처리하기도 합니다.
하지만 리액트 v16.3버전 이후에는 Context API가 많이 개선되어서 작은 프로젝트에서도 쉽게 전역상태관리를 할 수 있게 되었습니다.
위의 사진에서도 보다시피 header.js는 그저 navigation.js라는 컴포넌트에 props를 전달해주는 역할이되어버리겠죠?
● 새 Context 만들기
새로운 프로젝트를 생성하고, src 폴더에 contexts 폴더를 만들어 color.js 파일을 만들어주었습니다.
경로 : src > contexts > color.js
color.js
import { createContext } from 'react';
const ColorContext = createContext({
color : 'black'
});
export default ColorContext;
createContext를 import 해오고 새로운 context를 만들어 주었습니다.
● Consumer 사용하기
src 폴더에 components 폴더를 만들어 ColorBox.jsx 파일을 만들어주었습니다.
경로 : src > components > ColorBox.jsx
ColorBox.js
import ColorContext from '../contexts/color'
const ColorBox = ()=>{
return(
<ColorContext.Consumer>
{()=>{}}
</ColorContext.Consumer>
)
}
export default ColorBox;
먼저 위와 같이 ColorContext를 작성해주었습니다. Consumer란 이제 앞으로 context를 사용할 컴포넌트를 이야기합니다. 그리고 나서, 항상 안쪽은 { } 자바스크립트로 처리해주고, 내부는 꼭 콜백함수여야합니다. { ( ) => { } } 이렇게.
다음 아래와 같이 적어줍니다.
ColorBox.js
import ColorContext from '../contexts/color'
const ColorBox = ()=>{
return(
<ColorContext.Consumer>
{(value)=>
(<div style={{ width: '64px', height : '64px', background : value.color}}></div>)
}
</ColorContext.Consumer>
)
}
export default ColorBox;
항상 콜백함수는 인자를 받아오는데, 이는 context 객체입니다. 이후 컴포넌트처럼 똑같이 jsx를 return 을 해줍니다. 위의 코드에서 div를 리턴하고 있고, div의 background로 value.color를 적어주었습니다. 이는 context에서 아까 적어준 color입니다.
이를 이제 App에서 렌더링 해보겠습니다.
App.jsx
import ColorBox from './components/ColorBox'
const App = ()=>{
return (
<>
<ColorBox/>
</>
)
}
export default App;
● Provider 사용하기
provider를 사용하면 Context의 value를 수정할 수 있습니다.
App.jsx
import ColorBox from './components/ColorBox'
import ColorContext from './contexts/color';
const App = ()=>{
return (
<ColorContext.Provider value={{color : "red"}}>
<ColorBox/>
</ColorContext.Provider>
)
}
export default App;
위와 같이 ColorContext 컴포넌트를 불러오고 <ColorBox/> 컴포넌트를 <ColorContext.Provider>로 감싸줍니다. 이렇게 되면 fragment를 쓰지 않아도 되겠죠? 그리고 value의 값으로 원하는 color의 값을 넣어줍니다.
- 꼭 value 라는 값으로 넘겨주어야합니다.
그럼 위와 같이 렌더링 되는 것을 볼 수 있습니다. 그렇다면 Provider에 값을 넘겨주지 않으면 defaul값인 black으로 설정되지 않을까요?
아닙니다. Provider를 사용할 때는 꼭 value를 설정해주어야합니다.
- Provider는 꼭 value를 설정해주어야한다.
- 그리고 꼭 value로 값을 설정해주어야한다.
● 동적 Context 사용하기
context에는 꼭 값만 있을 필요는 없습니다. 함수가 있을 수도 있습니다!
경로 : src > contexts > color.js
color.js
import React, { 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;
// ColorProvider와 ColorConsumer 내보내기
export { ColorProvider, ColorConsumer };
export default ColorContext;
위와 같이 context는부분은 default 값을 설정해주고 setColor, setSubcolor는 그냥 화살표 함수로 설정해줍니다.
그리고 안에서 ColorProvider 컴포넌트안에서 ColorContexet.Provider를 렌더링해주고 있습니다. 그리고 안에서 state를 정의해주고, value안을 context와 똑같은 구조로 설정해주었습니다.
그리고 난뒤, ColorContext.Provider의 value로 넘겨주었습니다.
● 새로워진 Context를 프로젝트에 반영하기
App.jsx
import ColorBox from './components/ColorBox'
import {ColorProvider} from './contexts/color';
const App = ()=>{
return (
<ColorProvider>
<ColorBox/>
</ColorProvider>
)
}
export default App;
ColorProvider를 불러와 수정해주었습니다.
ColorBox.jsx
import {ColorConsumer} from '../contexts/color'
const ColorBox = ()=>{
return(
<ColorConsumer>
{(value)=>
(<>
<div style={{ width: '64px', height : '64px', background : value.state.color}} />
<div style={{ width : '32px', height : '32px', background : value.state.subcolor}}/>
</>)
}
</ColorConsumer>
)
}
export default ColorBox;
ColorConsumer도 불러와 수정하고, value도 수정해주었습니다.
만약 value를 구조 분해 할당으로 수정하고 싶다면
const ColorBox = ()=>{
return(
<ColorConsumer>
{({state})=>
(<>
<div style={{ width: '64px', height : '64px', background : state.color}} />
<div style={{ width : '32px', height : '32px', background : state.subcolor}}/>
</>)
}
</ColorConsumer>
)
}
위와 같이 수정할 수 있습니다.
● 색상 선택 컴포넌트 만들기
components 디렉토리에 SelectColors.jsx라는 파일을 만들어주었습니다.
경로 : src > components > SelectColors.jsx
SelectColors.jsx
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'];
const SelectColors = ()=>{
return (
<div>
<h2>색상을 선택하세요.</h2>
<div style={{display : 'flex'}}>
{
colors.map(color =>
<div
key={color}
style={{background : color , width : '24px', height : '24px', cursor: 'pointer'}}/>
)
}
</div>
</div>
)
}
export default SelectColors;
다음 App.jsx에서 렌더링해주었습니다.
App.jsx
import ColorBox from './components/ColorBox'
import SelectColors from './components/SelectColors';
import {ColorProvider} from './contexts/color';
const App = ()=>{
return (
<ColorProvider>
<SelectColors />
<ColorBox/>
</ColorProvider>
)
}
export default App;
구분하기 쉽게 아래처럼 구분선을 넣어주었습니다. <hr>태그로!
이제 위의 색상들을 클릭 할때마다, 아래 컴포넌트들의 색상을 바꿔보도록 하겠습니다.
SelectColors.jsx
import { ColorConsumer } from '../contexts/color';
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'];
const SelectColors = ()=>{
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>
</div>
)
}
export default SelectColors;
onContextMenu는 마우스 오른쪽 클릭시 발동하는 이벤트입니다.
e.preventDefault()를 통해서 우클릭시 창이 뜨지 않도록하고, 다음 subColor를 수정하게 해주었습니다.
오 신기해!
● Consumer 대신 Hook 또는 static contextType 사용하기
위에서 Consumer를 사용해서 context객체를 가져올 수 있었지만, 컴포넌트 리턴안에 또 리턴하는 문법이 솔직히.. 저는 처음에 보고 좀 별로라고 생각했습니다. 그리고 매번 객체를 불러와야하는 것이 너무 귀찮았읍니다.. 하지만!! useContext 훅을 사용한다면, 매우 편리해집니다. :)
ColorBox.jsx
import ColorContext from '../contexts/color'
import { useContext } from 'react';
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;
우와.. 너무 편해졌습니다..!! Consumer을 호출하고 또 그 안에서 콜백함수를 만들어 context객체를 받아 사용해야했지만,
useContext를 사용하면 아주 아주 간편해집니다.
● 그렇다면 언제 이 context API를 사용해야할까?
상태끌어올리기를 할 때 state를 변경할 수 있는 setter함수를 아래로 쭈르륵 옮겨갔다가••• 변경되는 사항들이 너무 귀찮았지만, 더이상 props로 전달해주는 행위를 하지 않아도 되었습니다...! 넘나 편리!! 하지만, 그렇다고 매번 context API를 사용해야할까요?
아닙니다. 아주 자주 바뀌는 값같은 경우에는 context API로 사용하는걸 권장치 않는다고 합니다. 실제 리액트 개발자가 context API는 자주 바뀌는 값을 관리하는 의도가 사용하는 게 아니라고 합니다. 또한 직접 부모 컴포넌트가 state와 state의 setter함수를 직접적으로 자식 컴포넌트에게 물려줄 수 있을 때에는 사용하지 않는다고 하네요.