이 글은 김민준(velopert)님의 리액트를 다루는 기술을 참조하였습니다.
목차
1. What is Context API?
2. Context
3. useContext
1. What is Context API?
비단 리액트 뿐만 아니라 기초 언어 공부를 할 때도 전역 변수에 대해서 배웠을 것이다.
함수의 매개변수로 데이터를 주고 받을 수도 있긴 하지만, 굉장히 자주 사용되는 데이터의 경우에는 보통 전역으로 설정해버린다.
Context API란 그런 개념에서 출발한다.
각각의 사각형은 하나의 Component라고 생각하자.
React에서 컴포넌트 간 데이터 교환은 props와 state를 통하여 부모 컴포넌트에서 자식 컴포넌트로 단방향으로 이루어진다. (굳이 알고리즘 측면으로 바라보자면 Top-down으로 흘러간다.)
그런데 한 쪽에서 흐르고 있는 데이터를 다른 컴포넌트에서 필요로 하는 경우에 어떤 일이 발생할까?
아마 보통은 root 컴포넌트에 state를 만들고 데이터를 props로 주고받게 하면 된다고 생각할 것이다.
하지만 이 방법도 I와 E-F 컴포넌트 간의 데이터 공유를 위해 매번 공통 부모 컴포넌트를 수정하고
하위 컴포넌트에 데이터를 props에 전달하는 것은 유지·보수하기 상당히 비효율적임을 직감할 수 있다.
그래서 다양한 레벨에 중첩(nesting)된 많은 컴포넌트들에 데이터 전달을 유용하게 하기 위해 개발된 것이 Context API다.
Context API는 무모 자식 간의 데이터 흐름과 관계없이 전역적으로 다루어지는 데이터를 관리한다.
전역 데이터를 Context에 저장하면 필요한 컴포넌트에서 불러와 사용할 수 있게 된다.
React에서는 Context를 사용하기 위해 Provider와 Consumer을 사용해야 한다.
공통 부모 컴포넌트(Not Root Componet!)에 Provider을 사용하여 데이터를 제공하고
데이터를 사용하려는 자식 컴포넌트에서 Consumer을 사용해 읽어들일 수 있다.
💡 개발 목적을 알면 주의할 점이 보인다.
전역으로 변수를 할당하는 것은 언제나 리스크가 따른다.
그래서 초반에 언어를 배우면 전역 변수 할당은 되도록 지양하라는 말들을 많이 듣게 된다.
리액트에서 Context란 위에서도 말했다시피 많은 컴포넌트에 데이터를 전달하는 것인데,
이건 원래 컴포넌트 간에 해야할 작업이지만 너무 번거로우니까 Context가 처리해주는 의미이지
Context에 의존하다보면 컴포넌트를 재사용하기가 상당히 어렵고 복잡해진다.
그래서 때로는 여러 단계에 걸쳐 props를 넘길 땐, 컴포넌트 합성이 더 간단한 방법일 수 있다.
제어의 역전과, 자식 컴포넌트와 직속 부모 분리를 통해 해결 가능한 문제가 아니라
같은 데이터를 트리 안에서도 여러 레벨이 있는 많은 컴포넌트에 주어야할 때 사용해야 한다.
예를 들어, 테마나 데이터 캐시 등을 관리하는데 있어서는 Context를 사용하는 것이 가장 편리하다.
🤔 Context API를 사용하는 때?
- 자주 변하지 않는 간단한 상태 정보만 전달하는 경우
- application의 극히 일부의 state나 함수를 전달하지만, props로 넘기는 단계가 너무 많을 때
- 추가적인 라이브러리 없이 리액트만으로 구현할 때
- # 상태관리까지 필요하다면?
- 여러 레벨에 다수의 Consumer가 존재하는 경우
- State가 시간에 따라 빈번하게 Update가 발생하는 경우에
- 기존의 상태 관리 로직이 복잡한 경우에 전역으로 빼버릴 수도 있음.
언제나 도구를 적재적소에 활용한다는 게 제일 어렵다.
2. Context
1. create Context
따로 설치가 필요한 기능인 줄 알았는데 요샌 React 자체적으로 지원하는 기능이 많이 개선되어서 그냥 이걸 쓴다고 한다.
아무 프로젝트나 시작해서 src 디렉토리에 Contexts 디렉토리를 생성하자.
기본 구조
const MyContext = React.createContext(defaultValue);
React는 트리 상위부터 가장 가까이 적절한 Provider로부터 현재값을 읽어 Context 객체에 넘겨준다.
defaultValue는 트리 안에서 적절한 Provider를 찾지 못할 때 사용하는 값이다.
createContext 함수를 사용하면 Provider와 Consumer 컴포넌트를 리턴값으로 준다.
contexts/color.js
import { createContext } from 'react';
const ColorContext = createContext({ color: 'black' });
export default ColorContext;
createContext의 파라미터는 해당 Context의 기본 상태를 지정한다.
2. Consumer
<MyContext.Consumer>
{value => {/* context 값을 이용해 렌더링 */} }
</MyContext.Consumer>
context 변화를 구독하는 React의 컴포넌트이다. 이 컴포넌트를 쓰면 함수 컴포넌트 내에서 Context를 구독할 수 있게 된다.
이 때, Consumer의 자식은 함수여야 한다. 이 함수는 context 현재 값을 받아 React 노드를 반환한다.
함수가 받는 value 값은 상위 트리에서 가장 가까운 Provider의 value prop과 동일하다. (없다면 defaultValue)
components / ColorBox.js
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;
Context의 데이터를 가져올 때는 props가 아니라 ColorContext안의 Consumer라는 컴포넌트를 사용한다.
위처럼 컴포넌트 사이에 중괄호를 열어 함수를 정의하는 패턴을 Function as child, 혹은 Render Props라고 한다.
컴포넌트 자식이 위치할 자리에 일반 JSX나 문자열이 아닌 함수를 전달하는 것이다.
부모 컴포넌트에게 children props로 정의한 함수를 넘겨주면, 부모 컴포넌트에서 children(값)을 사용해 해당 함수를 사용할 수 있다는 의미다.
검정색 정사각형이 나타나면 무사히 진행되고 있는 것.
3. Provider
기본 구조
<MyContext.Provider value={/* someValue */}>
(...)
</MyContext.Provider>
예시
import ColorBox from './components/ColorBox';
import ColorContext from './contexts/color';
function App() {
return (
<div>
<ColorContext.Provider value={{ color: 'red'} }>
<ColorBox />
</ColorContext.Provider>
</div>
);
}
export default App;
Provider는 Context를 구독하는 컴포넌트들에게 값의 변화를 알린다.
value prop를 받아서 하위 컴포넌트에 전달하는데, 값을 전달받을 수 있는 컴포넌트 수에 제한은 따로 없다.
Provider 하위에 Provider가 나오면 하위 Provider의 값이 우선시 된다. (적어도 하위 Consumer에게는)
또한 Provider 하위에서 Context를 구독하는 모든 컴포넌트는 value prop가 바뀔 때마다 리렌더링된다.
이는 상위 컴포넌트가 업데이트를 건너 뛰더라도 Consumer가 Update가 되는 것에 유의하자.
💡 StateUpdate & Context & reRendering
리렌더링 여부를 정할 때는 reference(참조)를 확인하는데 Provider의 부모가 렌더링 되면
하위 컴포넌트가 리렌더링 되는 문제가 발생할 수 있다.
내부 로직을 살펴보면 다음과 같다.
· setState()를 호출하면 Component rendering을 Queue에 집어 넣는다.
· React는 재귀적(recursive)으로 하위 Component를 렌더링한다.
· Context provider는 컴포넌트에 의해 렌더링할 값을 전달받는다.
· 보통 위에서 언급한 값은 부모 컴포넌트의 state에 기반한다.
즉, Context Provider를 구성하는 부모 컴포넌트의 state 업데이트는 모든 자식 컴포넌드들의 실제 Context value 구독 여부와 관계없이 리렌더링을 발생시켜버린다.
A - B - C의 depth가 3인 컴포넌트에서 C가 리렌더링되는 이유가 A에서 Context 갱신했기 때문이 아니라
B가 리렌더링 되는 이유만으로 C까지 리렌더링될 수 있음을 의미한다.
이것들을 방지하기 위해서는 React.memo 혹은 {props.children}을 사용해야 한다.
추가적으로 Provider의 value 값으로 위의 코드처럼 던져주면 매번 새로운 객체가 생성되어 Context를 구독하고 있는 모든 Consumer가 리렌더링되는 사태가 벌어질 수 있다.
이를 해결하기 위해서 Providr Component에서 state를 관리하거나, React.memo()를 쓸 수 있지만 근본적인 원인은 Context가 너무 정적이어서 발생하는 문제다!!
이런 이유로 리액트 16.3부터는 Context 내에서 자체적으로 state를 관리하여 줄 수도 있다. (좀 더 다루기 쉬워졌다고 보는 게 맞다.)
다만, 이 과정에서 내가 구독하고 있지 않은 state 변화로 인해 엉뚱한 Consumer이 죄다 리렌더링 되는 사태를 방지하기 위해서 context를 분할 관리 해주어야하는 것은 여전하다 ㅎ. 덕분에 코드가 상당히 길어진다.
4. 동적 Context 관리
contexts/color.js
import { createContext, useState } from 'react';
const ColorContext = createContext({
state: { color: 'black', subcolor: 'red' },
action: {
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;
export { ColorProvider, ColorConsumer };
export default ColorContext;
이제 여기서 ColorProvider와 ColorConsumer을 호출하면 어떤 일이 발생할까.
- 'ColorProvider'가 렌더링 됨. (value props로 값의 수정 가능)
- 새로운 value가 세팅됨.
- React에서 ColorContext.Provider에 새로운 값이 들어옴을 인지하고, 해당 Context를 사용하는 Consumer들에게 업데이트가 필요함을 공지함.
- 'Consumer'가 렌더링 됨. (Context에 접근하여 setState 함수인 action 값도 사용 가능하다.)
- Provider가 props로 Context를 보내줌.
마저 코딩을 해보자.
App.js
import ColorBox from './components/ColorBox';
import {ColorProvider} from './contexts/color';
function App() {
return (
<div>
<ColorProvider>
<div>
<ColorBox />
</div>
</ColorProvider>
</div>
);
}
export default App;
components / ColorBox.js
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에서 value값으로 설정한 action 함수도 사용해보자.
components / SelectColors.js
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>
<hr />
</div>
)
}
export default SelectColors;
그리고 App.js에서 ColorBox 컴포넌트 위에 추가해주면 브라우저 상단에 알록달록한 정사각형들이 나올 것이다.
마우스 왼쪽 버튼은 큰 정사각형의 색상을 바꾸고 우측 버튼은 작은 정사각형의 색상을 변경하기 위해서
action으로 받은 함수를 활용하자.
<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>
SelectColors 컴포넌트에서 Consumer을 import하여 actions로 setState 함수를 사용하자.
무사히 작동하는 것을 볼 수 있다.
3. useContext
Context를 관리하는 Hook도 존재한다. (별 게 다 있다.)
사용법은 굉장히 간단하다. useContext의 인자로 Context를 던져주면 된다.
ColorBox.js
import { 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;
Cosumer 컴포넌트를 모두 날리고 state만 불러왔다. 책에는 나와있지 않지만 actions 함수도 불러올 수 있다.
그걸 이용해서 SelectColors.js도 똑같이 수정 가능하다.
static contextType이란 것도 있는데 얘는 클래스형 컴포넌트를 위한방식이다.
난 다루지 않겠다.