context
는 전역적인 데이터를 컴포넌트 트리 내에서 임의의 깊이를 가진 컴포넌트들에게 전달하는 방법이다. (쉽게 말해서 깊이에 관계없이 타겟 컴포넌트에 필요한 데이터를 전달할 수 있는 방법)
일반적으로 props를 사용해서 데이터를 전달할 때는 부모 컴포넌트에서 자식 컴포넌트로만 데이터를 전달할 수 있지만, context를 사용하면 컴포넌트 트리의 깊은 곳에 있는 컴포넌트들에도 간편하게 데이터를 전달할 수 있다.
컴포넌트에서 context를 읽고 사용할 수 있도록 도와주는 React Hook이다.
const value = useContext(SomeContext)
useContext(SomeContext)
someContext
: createContext
로 생성한 context이다.
컴포넌트에서 제공하거나 읽을 수 있는 정보의 종류를 나타낸다.
useContext는 호출하는 컴포넌트에 대한 context 값
을 반환한다.
이 값은 트리에서 호출하는 컴포넌트 상위의 가장 가까운 SomeContext.Provider
에 전달된 값으로 결정된다.
Provider가 없으면 반환된 값은 해당 context에 대해 createContext에 전달한 defaultValue가 된다.
Context가 변경되면 React는 자동으로 해당 Context를 읽는 컴포넌트를 다시 렌더링한다.
상위
에 있어야 한다.컴포넌트의 최상위 레벨에서 useContext를 호출하여 Context를 읽고 사용한다.
import { useContext } from 'react';
function Button() {
const theme = useContext(ThemeContext);
// ...
useContext를 전달한 context에 대한 context value를 반환하다.
Context 값을 결정하기 위해 React는 컴포넌트 트리를 탐색하고 특정 Context에 대해 상위에서 가장 가까운 Context Provider를 찾는다.
Context를 Button에 전달하려면 해당 버튼 또는 상위 컴포넌트 중 하나를 해당 Context Provider로 감싼다.
function MyPage() {
return (
<ThemeContext.Provider value="dark">
<Form />
</ThemeContext.Provider>
);
}
function Form() {
// ... 내부에서 버튼을 렌더링합니다. ...
}
주의사항
useContext는 현재 컴포넌트에서 호출된 위치보다 상위에 있는 Context.Provider를 찾는다. 쉽게 말해서, useContext는 자신이 호출된 컴포넌트보다 위쪽에 있는 Provider에서 값을 가져오고, 자신의 컴포넌트 내부에 있는 Provider는 무시한다.
import { createContext, useContext } from 'react';
const ThemeContext = createContext(null);
export default function MyApp() {
return (
<ThemeContext.Provider value="dark">
<Form />
</ThemeContext.Provider>
)
}
function Form() {
return (
<Panel title="Welcome">
<Button>Sign up</Button>
<Button>Log in</Button>
</Panel>
);
}
function Panel({ title, children }) {
const theme = useContext(ThemeContext);
const className = 'panel-' + theme;
return (
<section className={className}>
<h1>{title}</h1>
{children}
</section>
)
}
function Button({ children }) {
const theme = useContext(ThemeContext);
const className = 'button-' + theme;
return (
<button className={className}>
{children}
</button>
);
}
context를 업데이트하고 싶은 경우 context와 state
를 결합하면 된다.
function MyPage() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={theme}>
<Form />
<Button onClick={() => {
setTheme('light');
}}>
Switch to light theme
</Button>
</ThemeContext.Provider>
);
}
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext(null);
export default function MyApp() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<Form />
<label>
<input
type="checkbox"
checked={theme === 'dark'}
onChange={(e) => {
setTheme(e.target.checked ? 'dark' : 'light')
}}
/>
Use dark mode
</label>
</ThemeContext.Provider>
)
}
function Form({ children }) {
return (
<Panel title="Welcome">
<Button>Sign up</Button>
<Button>Log in</Button>
</Panel>
);
}
function Panel({ title, children }) {
const theme = useContext(ThemeContext);
const className = 'panel-' + theme;
return (
<section className={className}>
<h1>{title}</h1>
{children}
</section>
)
}
function Button({ children }) {
const theme = useContext(ThemeContext);
const className = 'button-' + theme;
return (
<button className={className}>
{children}
</button>
);
}
{ currentUser, setCurrentUser }
를 하나의 객체로 결합하여 value={} 내부의 Context를 통해 전달한다.import { createContext, useContext, useState } from 'react';
const CurrentUserContext = createContext(null);
export default function MyApp() {
const [currentUser, setCurrentUser] = useState(null);
return (
<CurrentUserContext.Provider
value={{
currentUser,
setCurrentUser
}}
>
<Form />
</CurrentUserContext.Provider>
);
}
function Form({ children }) {
return (
<Panel title="Welcome">
<LoginButton />
</Panel>
);
}
function LoginButton() {
const {
currentUser,
setCurrentUser
} = useContext(CurrentUserContext);
if (currentUser !== null) {
return <p>You logged in as {currentUser.name}.</p>;
}
return (
<Button onClick={() => {
setCurrentUser({ name: 'Advika' })
}}>Log in as Advika</Button>
);
}
function Panel({ title, children }) {
return (
<section className="panel">
<h1>{title}</h1>
{children}
</section>
)
}
function Button({ children, onClick }) {
return (
<button className="button" onClick={onClick}>
{children}
</button>
);
}
context, reducer 정의
import { createContext, useContext, useReducer } from 'react';
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
}
case 'changed': {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
const initialTasks = [
{ id: 0, text: 'Philosopher’s Path', done: true },
{ id: 1, text: 'Visit the temple', done: false },
{ id: 2, text: 'Drink matcha', done: false }
];
state쓰고 dispatch하기
import { useState, useContext } from 'react';
import { useTasks, useTasksDispatch } from './TasksContext.js';
export default function TaskList() {
const tasks = useTasks();
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<Task task={task} />
</li>
))}
</ul>
);
}
function Task({ task }) {
const [isEditing, setIsEditing] = useState(false);
const dispatch = useTasksDispatch();
let taskContent;
if (isEditing) {
taskContent = (
<>
<input
value={task.text}
onChange={e => {
dispatch({
type: 'changed',
task: {
...task,
text: e.target.value
}
});
}} />
<button onClick={() => setIsEditing(false)}>
Save
</button>
</>
);
} else {
taskContent = (
<>
{task.text}
<button onClick={() => setIsEditing(true)}>
Edit
</button>
</>
);
}
return (
<label>
<input
type="checkbox"
checked={task.done}
onChange={e => {
dispatch({
type: 'changed',
task: {
...task,
done: e.target.checked
}
});
}}
/>
{taskContent}
<button onClick={() => {
dispatch({
type: 'deleted',
id: task.id
});
}}>
Delete
</button>
</label>
);
}
React가 부모 트리에서 특정 Context Provider를 찾을 수 없는 경우, useContext()가 반환하는 Context 값은 해당 Context를 생성할 때 지정한 기본값과 동일하다.
따라서 아래와 같은 경우 null이 반환된다.
const ThemeContext = createContext(null);
기본값은 변경할 수 없으므로 context를 업데이트하려면 state와 함께 사용해야 한다.
null 대신 의미있는 기본값을 하는 경우는 아래와 같다.
const ThemeContext = createContext('light');
이렇게 하면 실수로 해당 Provider 없이 일부 컴포넌트를 렌더링해도 깨지지 않는다.
또한 테스트 환경에서 많은 Provider를 설정하지 않고도 컴포넌트가 테스트 환경에서 잘 작동하는 데 도움이 된다.
트리의 일부분을 다른 값의 Provider로 감싸서 해당 부분에 대한 Context를 오버라이딩 할 수 있다.
아래의 경우 Footer 부분은 light로 나온다.
<ThemeContext.Provider value="dark">
...
<ThemeContext.Provider value="light">
<Footer />
</ThemeContext.Provider>
...
</ThemeContext.Provider>
여기서 Footer 내부의 버튼은 외부의 버튼("dark")과 다른 Context 값("light")을 받는다.
import { createContext, useContext } from 'react';
const ThemeContext = createContext(null);
export default function MyApp() {
return (
<ThemeContext.Provider value="dark">
<Form />
</ThemeContext.Provider>
)
}
function Form() {
return (
<Panel title="Welcome">
<Button>Sign up</Button>
<Button>Log in</Button>
<ThemeContext.Provider value="light">
<Footer />
</ThemeContext.Provider>
</Panel>
);
}
function Footer() {
return (
<footer>
<Button>Settings</Button>
</footer>
);
}
function Panel({ title, children }) {
const theme = useContext(ThemeContext);
const className = 'panel-' + theme;
return (
<section className={className}>
{title && <h1>{title}</h1>}
{children}
</section>
)
}
function Button({ children }) {
const theme = useContext(ThemeContext);
const className = 'button-' + theme;
return (
<button className={className}>
{children}
</button>
);
}
context Provider를 중첩할 때 정보를 “누적”할 수 있다.
아래 예시에서 Section 컴포넌트는 섹션 중첩의 깊이를 지정하는 LevelContext를 추적한다.
이 컴포넌트는 부모 섹션에서 LevelContext를 읽은 다음 1씩 증가한 LevelContext 숫자를 자식에게 제공한다.
그 결과 Heading 컴포넌트는 얼마나 많은 Section 컴포넌트가 중첩되어 있는지에 따라 <h1>
, <h2>
, <h3>
, …, 태그 중 어떤 태그를 사용할지 자동으로 결정할 수 있다.
import { createContext } from 'react';
export const LevelContext = createContext(0);
import Heading from './Heading.js';
import Section from './Section.js';
export default function Page() {
return (
<Section>
<Heading>Title</Heading>
<Section>
<Heading>Heading</Heading>
<Heading>Heading</Heading>
<Heading>Heading</Heading>
<Section>
<Heading>Sub-heading</Heading>
<Heading>Sub-heading</Heading>
<Heading>Sub-heading</Heading>
<Section>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>
</Section>
</Section>
</Section>
);
}
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext.Provider value={level + 1}>
{children}
</LevelContext.Provider>
</section>
);
}
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Heading({ children }) {
const level = useContext(LevelContext);
switch (level) {
case 0:
throw Error('Heading must be inside a Section!');
case 1:
return <h1>{children}</h1>;
case 2:
return <h2>{children}</h2>;
case 3:
return <h3>{children}</h3>;
case 4:
return <h4>{children}</h4>;
case 5:
return <h5>{children}</h5>;
case 6:
return <h6>{children}</h6>;
default:
throw Error('Unknown level: ' + level);
}
}
다음과 같이 객체와 함수를 context value로 전달할 때 MyApp이 리렌더링 될 때마다(ex. 경로 이동 시) 함수와 객체가 업데이트되어 React는 useContext(AuthContext)를 호출하는 트리 깊숙한 곳에 있는 모든 컴포넌트들을 다시 렌더링해야 한다.
function MyApp() {
const [currentUser, setCurrentUser] = useState(null);
function login(response) {
storeCredentials(response.credentials);
setCurrentUser(response.user);
}
return (
<AuthContext.Provider value={{ currentUser, login }}>
<Page />
</AuthContext.Provider>
);
}
이때 currentUser가 변경되지 않았다면 리렌더링을 할 필요가 없으므로 React가 이 사실을 알게끔 객체 생성을 useMemo로 최적화하고, 함수를 useCallback으로 최적화할 수 있다.
이렇게 변경하면 MyApp이 다시 렌더링해야 하는 경우에도 currentUser가 변경되지 않는 한 useContext(AuthContext)를 호출하는 컴포넌트는 다시 렌더링할 필요가 없다.
import { useCallback, useMemo } from 'react';
function MyApp() {
const [currentUser, setCurrentUser] = useState(null);
const login = useCallback((response) => {
storeCredentials(response.credentials);
setCurrentUser(response.user);
}, []);
const contextValue = useMemo(() => ({
currentUser,
login
}), [currentUser, login]);
return (
<AuthContext.Provider value={contextValue}>
<Page />
</AuthContext.Provider>
);
}
이런 일이 발생하는 케이스는 다음과 같다.
// 🚩 Doesn't work: no value prop
<ThemeContext.Provider>
<Button />
</ThemeContext.Provider>
이때 createContext(defaultValue)의 기본값이 쓰이지 않고 undefined가 전달되는 이유
createContext(defaultValue)
호출의 기본값은 위에 일치하는 Provider가 전혀 없는 경우에만 사용된다.<SomeContext.Provider value={undefined}>
컴포넌트가 있는 경우,useContext(SomeContext)
를 호출하는 컴포넌트는 undefined를 Context 값으로 받는다.
// 🚩 Doesn't work: prop should be called "value"
<ThemeContext.Provider theme={theme}>
<Button />
</ThemeContext.Provider>
다음과 같이 수정한다.
// ✅ Passing the value prop
<ThemeContext.Provider value={theme}>
<Button />
</ThemeContext.Provider>
createContext를 사용하면 컴포넌트가 Context를 제공하거나 읽을 수 있다.
const SomeContext = createContext(defaultValue)
createContext(defaultValue)
컴포넌트 외부에서 createContext를 호출하여 컨텍스트를 생성한다.
import { createContext } from 'react';
const ThemeContext = createContext('light');
defaultValue
: 컴포넌트가 context를 읽을 때 상위에 일치하는 provider가 없는 경우 컨텍스트가 가져야 할 값이다. 의미 있는 기본값이 없으면 null을 지정한다. 기본값은 “최후의 수단”으로 사용된다. 이 값은 정적이며 시간이 지나도 변경되지 않는다.
context 객체를 반환한다. (context 객체 자체는 어떠한 정보도 갖고 있지 않다.)
일반적으로 상위 컴포넌트에서 컨텍스트 값을 지정하기 위해 SomeContext.Provider
를 사용하고, 하위 컴포넌트에서 읽기 위해 useContext(SomeContext)
를 호출한다.
context 객체가 가진 속성은 다음과 같다.
SomeContext.Provider
: 컴포넌트에 컨텍스트 값을 제공한다.SomeContext.Consumer
: 컨텍스트 값을 읽는 대안책이고 거의 안 쓴다. (예전에 썼던 기능이고 요즘은 useContext로 읽음)value
: prodiver 내부의 컨텍스트를 읽는 모든 컴포넌트에 전달하려는 값이다. 컨텍스트 값은 어떤 타입이든 상관 없다. provider 내부에서 useContext(SomeContext)를 호출하는 컴포넌트는 그 위의 가장 가까운 컨텍스트 prodiver의 value를 받게 된다.
SomeContext.Provider
컴포넌트를 컨텍스트 provider로 감싸서 이 컨텍스트의 값을 모든 내부 컴포넌트에 지정한다.
function App() {
const [theme, setTheme] = useState('light');
// ...
return (
<ThemeContext.Provider value={theme}>
<Page />
</ThemeContext.Provider>
);
}
SomeContext.Consumer
useContext가 등장하기 전에 컨텍스트를 읽는 방식으로 쓰였다.
function Button() {
// 🟡 이전 방식 (권장하지 않음)
return (
<ThemeContext.Consumer>
{theme => (
<button className={theme} />
)}
</ThemeContext.Consumer>
);
}
위의 방식은 여전히 동작하긴 하지만 아래 방식을 권장한다.
function Button() {
// ✅ 권장하는 방법
const theme = useContext(ThemeContext);
return <button className={theme} />;
}
컴포넌트 외부에서 createContext를 호출하여 하나 이상의 컨텍스트를 생성한다.
import { createContext } from 'react';
const ThemeContext = createContext('light');
const AuthContext = createContext(null);
createContext는 컨텍스트 객체를 반환한다.
컴포넌트는 이를 useContext()에 전달하여 컨텍스트를 읽을 수 있다.
function Button() {
const theme = useContext(ThemeContext);
// ...
}
function Profile() {
const currentUser = useContext(AuthContext);
// ...
}
기본적으로 value는 context를 생성할 때 지정한 기본값이 되지만 기본값은 변경할 수 없기 때문에 유용하지 않다.
context를 변경하기 위해서는 다음과 같이 state와 결합해주면 된다.
function App() {
const [theme, setTheme] = useState('dark');
const [currentUser, setCurrentUser] = useState({ name: 'Taylor' });
// ...
return (
<ThemeContext.Provider value={theme}>
<AuthContext.Provider value={currentUser}>
<Page />
</AuthContext.Provider>
</ThemeContext.Provider>
);
}
context는 보통 별도 파일에서 선언하는데, 그 이유는 서로 다른 파일에 있는 컴포넌트에서 컨텍스트에 접근해야 하는 경우가 발생하기 때문이다.
// Contexts.js
import { createContext } from 'react';
export const ThemeContext = createContext('light');
export const AuthContext = createContext(null);
이렇게 별도 파일에서 선언된 context를 읽기 위해서는 import를 하면 된다.
// Button.js
import { ThemeContext } from './Contexts.js';
function Button() {
const theme = useContext(ThemeContext);
// ...
}
depth가 깊어진다고 해서 무조건 context를 사용해야 하는 것은 아님.
context를 사용하기 전 다음을 고려해보아야 한다.
props로 전달하기
: 사소한 컴포넌트가 아닌 이상 컴포넌트에 여러 props를 전달하는 것은 이상한게 아니다. 어떤 컴포넌트가 어떤 데이터를 사용하는지 명확히 나타낼 수 있고, 유지보수에도 좋다.컴포넌트를 추출하고 JSX를 children으로 전달하기
: 데이터를 사용하지 않는 중간 컴포넌트 층을 통해 데이터를 보내기만 하는 경우에는 컴포넌트를 추출하는 것이 낫다.<Layout posts={posts}/>
로 쓰는 것 보다 <Layout><Posts posts={posts}/></Layout>
과 같이 children을 prop으로 받아서 전달하는 것이 낫다. 이렇게 하면 데이터를 지정하는 컴포넌트와 데이터가 필요한 컴포넌트 사이의 계층이 줄어든다.위의 방법을 먼저 시도해보고 잘 해결되지 않는다면 context를 고려해보는 것이 좋다.
테마
: 다크 모드/라이트 모드와 같이 사용자가 테마를 지정할 수 있는 경우에 context provider를 앱 최상단에 두고 시각적으로 조정이 필요한 곳에서 context를 사용할 수 있다.현재 로그인된 계정 정보
: 현재 로그인 한 유저의 정보를 알아야 하는 컴포넌트가 많은 경우에 현재 계정 정보를 context에 저장해두면 좋다. 라우팅
: 대부분의 라우팅 솔루션은 현재 라우트를 유지하기 위해 context를 사용한다. 상태 관리
: 애플리케이션 규모가 커지면 수많은 state가 생기게 되고 다양한 하위 컴포넌트에서 이 값을 변경하고 싶은 니즈가 있을 수 있다. reducer를 context와 함께 사용하면 복잡한 state를 관리하며 멀리 있는 컴포넌트까지 값을 전달할 수 있다.useContext를 이용해 prop drilling 제거하기
App.js
import { useState } from 'react';
import { places } from './data.js';
import { getImageUrl } from './utils.js';
export default function App() {
const [isLarge, setIsLarge] = useState(false);
const imageSize = isLarge ? 150 : 100;
return (
<>
<label>
<input
type="checkbox"
checked={isLarge}
onChange={e => {
setIsLarge(e.target.checked);
}}
/>
Use large images
</label>
<hr />
<List imageSize={imageSize} />
</>
)
}
function List({ imageSize }) {
const listItems = places.map(place =>
<li key={place.id}>
<Place
place={place}
imageSize={imageSize}
/>
</li>
);
return <ul>{listItems}</ul>;
}
function Place({ place, imageSize }) {
return (
<>
<PlaceImage
place={place}
imageSize={imageSize}
/>
<p>
<b>{place.name}</b>
{': ' + place.description}
</p>
</>
);
}
function PlaceImage({ place, imageSize }) {
return (
<img
src={getImageUrl(place)}
alt={place.name}
width={imageSize}
height={imageSize}
/>
);
}
Context.js
import { createContext } from 'react';
export const ImageContext = createContext(null);
App.js
import { useState, useContext } from 'react';
import { places } from './data.js';
import { getImageUrl } from './utils.js';
import { ImageContext } from './Context.js'
export default function App() {
const [isLarge, setIsLarge] = useState(false);
const imageSize = isLarge ? 150 : 100;
return (
<ImageContext.Provider value={imageSize}>
<label>
<input
type="checkbox"
checked={isLarge}
onChange={e => {
setIsLarge(e.target.checked);
}}
/>
Use large images
</label>
<hr />
<List />
</ImageContext.Provider>
)
}
function List() {
const listItems = places.map(place =>
<li key={place.id}>
<Place
place={place}
/>
</li>
);
return <ul>{listItems}</ul>;
}
function Place({ place }) {
return (
<>
<PlaceImage
place={place}
/>
<p>
<b>{place.name}</b>
{': ' + place.description}
</p>
</>
);
}
function PlaceImage({ place }) {
const imageSize = useContext(ImageContext);
return (
<img
src={getImageUrl(place)}
alt={place.name}
width={imageSize}
height={imageSize}
/>
);
}
자료
https://react.dev/reference/react/useContext
https://react.dev/reference/react/createContext
https://react.dev/learn/passing-data-deeply-with-context