Context는 리액트 컴포넌트간에 어떠한 값을 공유할수 있게 해주는 기능이다. 주로 Context는 전역적(global)으로 필요한 값을 다룰 때 사용하는데, 꼭 전역적일 필요는 없다.
"리액트 컴포넌트에서 Props가 아닌 또 다른 방식으로 컴포넌트간에 값을 전달하는 방법이다" 라고 접근 하면 된다.
리액트 애플리케이션에서는 일반적으로 컴포넌트에게 데이터를 전달해주어야 할 때 Props를 통해 전달한다. 하지만 깊숙히 위치한 컴포넌트에 데이터를 전달해야 하는 경우에 여러 컴포넌트를 거쳐 여러번 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 <Message value={value} />;
}
function Message({ value }) {
return <div>Received: {value}</div>;
}
위 코드 같은 경우를 Props Drilling 이라고 부른다. 이렇게 props를 3개 이상 거쳐서 전달하는 것은 매우 불편하고 귀찮은 일이다. 또한 이 value 값이 어디서 오는건지 파악할 때도 상위 컴포넌트를 타고 여러번 거슬러 올라가야 하는 번거로움이 있다. 또는, value 라는 네이밍을 변경을 하려면, 통일성을 맞추기 위해서 또 여러 컴포넌트들을 수정해야 하니까 그것 역시 불편하다.
아래는 다른 예시이다. 우리가 컴포넌트를 만들 때, 여러 종류의 자식 컴포넌트가 특정 값에 의존을 한다고 가정을 해보자
function App() {
return (
<Awesome value="Hello World" />
)
}
function Awesome({ value }) {
return (
<div>
<First value={value} />
<Second value={value} />
<Third value={value} />
</div>
)
}
function First({ value }) {
return (
<div>First says: "{value}"</div>
)
}
function Second({ value }) {
return (
<div>Second says: "{value}"</div>
)
}
function Third({ value }) {
return (
<div>Third says: "{value}"</div>
)
}
위의 경우에는 각 컴포넌트가 하나의 Props만 받아오기 때문에 많이 복잡해보이지 않을 수 있지만, 만약 여러 Props가 들어있고, 컴포넌트가 더 길고 복잡해 진다면, 가독성이 떨어질 수 도 있다.
이럴 때 Context를 사용하면 깔끔하게 해결할 수 있다.
Context는 리액트 패키지에서 createContext 라는 함수를 import 해야한다.
그리고 createContext로 MyContext를 만들어보자
(MyContext가 아니라도 원하는 식별자로 지정 가능하다)
import { createContext } from 'react';
const MyContext = createContext();
Context 객체 안에는 Provider라는 컴포넌트가 들어있는데
이 컴포넌트에 공유하고자 하는 값을 value 라는 Props로 설정하면 자손 컴포넌트들에서 해당 값에 바로 접근을 할 수 있게된다.
function App() {
return (
<MyContext.Provider value="Hello World">
<GrandParent />
</MyContext.Provider>
);
}
이렇게 하면, 원하는 컴포넌트에서 useContext 라는 Hook을 사용하여 Provider의 value 값들에 바로 접근할 수 있게된다. 해당 Hook의 인자에는 createContext로 만든 MyContext를 넣는다.
import { createContext, useContext } from 'react';
function 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 <Message />;
}
function Message() {
const value = useContext(MyContext);
return <div>Received: {value}</div>;
}
export default App;
마찬가지로 두번째 예제도 context를 적용해보자
import { createContext, useContext } from 'react';
const MyContext = createContext();
function App() {
return (
<MyContext.Provider value="Hello World">
<AwesomeComponent />
</MyContext.Provider>
);
}
function AwesomeComponent() {
return (
<div>
<FirstComponent />
<SecondComponent />
<ThirdComponent />
</div>
);
}
function FirstComponent() {
const value = useContext(MyContext);
return <div>First Component says: "{value}"</div>;
}
function SecondComponent() {
const value = useContext(MyContext);
return <div>Second Component says: "{value}"</div>;
}
function ThirdComponent() {
const value = useContext(MyContext);
return <div>Third Component says: "{value}"</div>;
}
export default App;
또한 위 코드처럼 Context가 여러 컴포넌트에서 사용되고 있다면 다음과 같이 커스텀 Hook을 만들어서 사용할 수도 있다.
import { createContext, useContext } from 'react';
const MyContext = createContext();
function useMyContext() {
return useContext(MyContext);
}
function App() {
return (
<MyContext.Provider value="Hello World">
<AwesomeComponent />
</MyContext.Provider>
);
}
function AwesomeComponent() {
return (
<div>
<FirstComponent />
<SecondComponent />
<ThirdComponent />
</div>
);
}
function FirstComponent() {
const value = useMyContext();
return <div>First Component says: "{value}"</div>;
}
function SecondComponent() {
const value = useMyContext();
return <div>Second Component says: "{value}"</div>;
}
function ThirdComponent() {
const value = useMyContext();
return <div>Third Component says: "{value}"</div>;
}
export default App;
이번에는 Context 에서 유동적인 값을 다뤄야 할 때를 알아보자
function App() {
return (
<div>
<Value />
<Buttons />
</div>
);
}
function Value() {
return <h1>1</h1>;
}
function Buttons() {
return (
<div>
<button>+</button>
<button>-</button>
</div>
);
}
export default App;
숫자가 보여지는 UI와 숫자에 변화를 주는 UI가 완전히 다른 컴포넌트로 분리되어 있다.
우선, Context 에서 유동적인 값을 관리하는 경우엔 Provider를 새로 만들어주는 것이 좋다. 또한 위와 같이 하나의 state만 있는 경우라면, useState를 사용하여 만들어진 값과 함수가 들어있는 배열을 통째로 value 로 넣으면 된다.
import { createContext } from 'react';
const CounterContext = createContext();
function CounterProvider({ children }) {
const counterState = useState(1);
return (
<CounterContext.Provider>
{children}
</CounterContext.Provider>
);
}
function App() {
return (
<CounterProvider>
<div>
<Value />
<Buttons />
</div>
</CounterProvider>
);
}
// ...
그 다음은 커스텀 Hook을 만들어 value 컴포넌트와 Buttons에서 다음과 같이 사용할 수 있다.
import { createContext, useContext, useState } from 'react';
// ...
function useCounterState() {
const value = useContext(CounterContext);
if (value === undefined) {
throw new Error('useCounterState should be used within CounterProvider');
}
return value;
}
function Value() {
const [counter] = useCounterState();
return <h1>{counter}</h1>;
}
function Buttons() {
const [, setCounter] = useCounterState();
const increase = () => setCounter(c => c + 1);
const decrease = () => setCounter(c => c - 1);
return (
<div>
<button onClick={increase}>+</button>
<button onClick={decrease}>-</button>
</div>
);
}
export default App;
만약 Context에서 관리하는 상태가 빈번하게 업데이트 된다면, 성능적으로 좋지 않다. 실제로 바뀌는 곳은 Value 컴포넌트뿐인데, Buttons 컴포넌트도 리렌더링되기 때문이다.
이는 우리가 비록 value를 만드는 과정에서 useMemo로 감싸주었긴 하지만, 어쨌든 counter가 바뀔 때 마다 새로운 배열을 만들어서 반환하고 있고 useContext에선 이를 감지하여 리렌더링을 하기 때문이다.
때문에 이럴 때는 Context를 분리하는 것이 좋다.
import { createContext, useContext, useMemo, useState } from 'react';
const CounterValueContext = createContext();
const CounterActionsContext = createContext();
function CounterProvider({ children }) {
const [counter, setCounter] = useState(1);
const actions = useMemo(
() => ({
increase() {
setCounter((prev) => prev + 1);
},
decrease() {
setCounter((prev) => prev - 1);
}
}),
[]
);
return (
<CounterActionsContext.Provider value={actions}>
<CounterValueContext.Provider value={counter}>
{children}
</CounterValueContext.Provider>
</CounterActionsContext.Provider>
);
}
function useCounterValue() {
const value = useContext(CounterValueContext);
// value가 없으면 오류 반환
if (value === undefined) {
throw new Error('useCounterValue should be used within CounterProvider');
}
return value;
}
function useCounterActions() {
const value = useContext(CounterActionsContext);
if (value === undefined) {
throw new Error('useCounterActions should be used within CounterProvider');
}
return value;
}
function App() {
return (
<CounterProvider>
<div>
<Value />
<Buttons />
</div>
</CounterProvider>
);
}
function Value() {
const counter = useCounterValue();
return <h1>{counter}</h1>;
}
function Buttons() {
const actions = useCounterActions();
return (
<div>
<button onClick={actions.increase}>+</button>
<button onClick={actions.decrease}>-</button>
</div>
);
}
export default App;
이제 버튼을 눌러서 상태에 변화가 일어날 때, Value 컴포넌트에서만 리렌더링이 발생한다.
이처럼 여러 업데이트 함수가 들어있는 actions 객체를 선언하여 별도의 Context에 넣어주는 방식으로 구현을 해주었다. 이 방식 외에도, useReducer롤 통해서 상태 업데이트를 하도록 구현하고 dispatch를 별도의 Context로 넣어주는 방식도 있다.
방금 우리가 사용했던 방식은 배열이나 객체를 다룰 때에도 동일하다.
예를 들어서 화면의 중앙에 문구를 띄우는 모달의 상태를 Context로 작성한다면, 다음과 같이 구현할 수 있다.
const ModalValueContext = createContext();
const ModalActionsContext = createContext();
function ModalProvider({ children }) {
const [modal, setModal] = useState({
visible: false,
message: ''
});
const actions = useMemo(
() => ({
open(message) {
setModal({
message,
visible: true
});
},
close() {
setModal((prev) => ({
...prev,
visible: false
}));
}
}),
[]
);
return (
<ModalActionsContext.Provider value={actions}>
<ModalValueContext.Provider value={modal}>
{children}
</ModalValueContext.Provider>
</ModalActionsContext.Provider>
);
}
function useModalValue() {
const value = useContext(ModalValueContext);
if (value === undefined) {
throw new Error('useModalValue should be used within ModalProvider');
}
return value;
}
function useModalActions() {
const value = useContext(ModalActionsContext);
if (value === undefined) {
throw new Error('useModalActions should be used within ModalProvider');
}
return value;
}
이렇게 하면 원하는 곳 어디서든지 모달을 띄울 수 있게된다.
const { open } = useModalActions();
const handleSomething = () => {
open('안녕하세요!');
};
만약 할 일 목록같은 배열을 다룬다면 어떨까?
import { createContext, useContext, useMemo, useRef, useState } from 'react';
const TodosValueContext = createContext();
const TodosActionsContext = createContext();
function TodosProvider({ children }) {
const idRef = useRef(3);
const [todos, setTodos] = useState([
{
id: 1,
text: '밥먹기',
done: true
},
{
id: 2,
text: '잠자기',
done: false
}
]);
const actions = useMemo(
() => ({
add(text) {
const id = idRef.current;
idRef.current += 1;
setTodos((prev) => [
...prev,
{
id,
text,
done: false
}
]);
},
toggle(id) {
setTodos((prev) =>
prev.map((item) =>
item.id === id
? {
...item,
done: !item.done
}
: item
)
);
},
remove(id) {
setTodos((prev) => prev.filter((item) => item.id !== id));
}
}),
[]
);
return (
<TodosActionsContext.Provider value={actions}>
<TodosValueContext.Provider value={todos}>
{children}
</TodosValueContext.Provider>
</TodosActionsContext.Provider>
);
}
function useTodosValue() {
const value = useContext(TodosValueContext);
if (value === undefined) {
throw new Error('useTodosValue should be used within TodosProvider');
}
return value;
}
function useTodosActions() {
const value = useContext(TodosActionsContext);
if (value === undefined) {
throw new Error('useTodosActions should be used within TodosProvider');
}
return value;
}
이렇게 하면 할 일 항목을 추가할때는
const { add } = useTodosActions();
const handleSubmit = () => {
add(text);
}
그리고 각 항목을 보여주는 컴포넌트에서는
const { toggle, remove } = useTodosActions()
const handleToggle = () => {
toggle(id);
};
const handleRemove = () => {
remove(id);
};
이런 식으로 구현해줄 수 있다.
Context에서 다루는 값은 꼭 전역적일 필요가 없다.
Context는 재사용성이 높은 컴포넌트를 만들때도 매우 유용하다.
import { useState } from 'react';
function Item({ active, children, onClick }) {
const activeStyle = {
backgroundColor: 'black',
color: 'white'
};
const style = {
cursor: 'pointer',
padding: '1rem'
};
return (
<div
style={active ? { ...style, ...activeStyle } : style}
onClick={onClick}
>
{children}
</div>
);
}
function App() {
const [activeId, setActiveId] = useState(1);
return (
<div>
<Item active={activeId === 1} onClick={() => setActiveId(1)}>
Hello
</Item>
<Item active={activeId === 2} onClick={() => setActiveId(2)}>
World
</Item>
<Item active={activeId === 3} onClick={() => setActiveId(3)}>
React
</Item>
</div>
);
}
export default App;
위 이미지처럼 항목을 선택할 수 있는 기능을 만들었다.
하지만 이 방법은 사용할때마다 activeId, onSelect Props를 반복적으로 넣어줘야 하는게 불편할 수 있다. 물론 다음과 같이 배열의 map 내장함수를 사용한다면 반복되는 코드는 사라지겠지만, 항목들을 JSX로 명료하게 표현해내지 못한다는 점이 아쉬울 수 있다.
import { useState } from 'react';
function Item({ activeId, children, onSelect, id }) {
const activeStyle = {
backgroundColor: 'black',
color: 'white'
};
const style = {
cursor: 'pointer',
padding: '1rem'
};
const active = activeId === id;
const onClick = () => onSelect(id);
return (
<div
style={active ? { ...style, ...activeStyle } : style}
onClick={onClick}
>
{children}
</div>
);
}
function App() {
const [activeId, setActiveId] = useState(1);
const items = [
{ id: 1, text: 'Hello' },
{ id: 2, text: 'World' },
{ id: 3, text: 'React' }
];
return (
<div>
{items.map((item) => (
<Item
key={item.id}
id={item.id}
activeId={activeId}
onSelect={setActiveId}
>
{item.text}
</Item>
))}
</div>
);
}
이럴때 항목들을 JSX로 표현하고 싶고, 반복되는 코드들을 정리해주고 싶다면, 이 또한 Context를 사용하여 쉽게 해결할 수 있다.
import { createContext, useContext, useMemo, useState } from 'react';
const ItemGroupContext = createContext();
function ItemGroup({ children, activeId, onSelect }) {
const value = useMemo(
() => ({
activeId,
onSelect
}),
[activeId, onSelect]
);
return (
<ItemGroupContext.Provider value={value}>
{children}
</ItemGroupContext.Provider>
);
}
function useItemGroup() {
const value = useContext(ItemGroupContext);
if (value === undefined) {
throw new Error('Item component should be used within ItemGroup');
}
return value;
}
function Item({ children, id }) {
const activeStyle = {
backgroundColor: 'black',
color: 'white'
};
const style = {
cursor: 'pointer',
padding: '1rem'
};
const { activeId, onSelect } = useItemGroup();
const active = activeId === id;
const onClick = () => onSelect(id);
return (
<div
style={active ? { ...style, ...activeStyle } : style}
onClick={onClick}
>
{children}
</div>
);
}
function App() {
const [activeId, setActiveId] = useState(1);
const [anotherActiveId, setAnotherActiveId] = useState(4);
return (
<div>
<ItemGroup activeId={activeId} onSelect={setActiveId}>
<Item id={1}>Hello</Item>
<Item id={2}>World</Item>
<Item id={3}>React</Item>
</ItemGroup>
<hr />
<ItemGroup activeId={anotherActiveId} onSelect={setAnotherActiveId}>
<Item id={4}>Bye</Item>
<Item id={5}>World</Item>
<Item id={6}>Context</Item>
</ItemGroup>
</div>
);
}
export default App;
필요한 값과 함수를 매번 Props로 넣어주는 대신, ItemGroup 이라는 Provider 컴포넌트를 만들어서 해당 컴포넌트에만 한번 넣어주고 Item 에서 Context를 읽어와서 값을 사용하도록 만들어주었다.
비록 작성해야 할 전체적인 코드는 늘어났지만, Item 코드를 사용하는 쪽에서는 훨씬 가독성 높고 편하게 작성할 수 있고 재사용성 또한 좋아졌다.
다른 ver.
import { createContext, useContext, useMemo, useState } from 'react';
const ItemGroupContext = createContext();
function Item({ children, id }) {
const { activeId, setActiveId } = useContext(ItemGroupContext);
const activeStyle = {
backgroundColor: 'black', color: 'white'
};
const style = {
cursor: 'pointer', padding: '1rem'
};
const active = (activeId === id);
const onClick = ;
return (
<div
style={active ? { ...style, ...activeStyle } : style}
onClick={() => setActiveId(id)}
>
{children}
</div>
);
}
function App() {
const [activeId, setActiveId] = useState(1);
return (
<ItemGroupContext.Provider value={activeId,setActiveId}>
<Item id={1}>Hello</Item>
<Item id={2}>World</Item>
<Item id={3}>React</Item>
</ItemGroupContext.Provider>
);
}
export default App;