
일반적으로 props를 통해 상위 컴포넌트의 정보를 하위 컴포넌트로 전달한다. 그러나 props를 전달하는 것은 중간에 많은 컴포넌트를 통해 전달해야 하거나 앱의 많은 컴포넌트에 동일한 정보가 필요한 경우 장황하고 불편할 수 있다. 컨텍스트를 사용하면 부모 컴포넌트는 props를 통해 명시적으로 정보를 전달하지 않고도 하위 트리의 모든 컴포넌트에서 일부 정보를 사용할 수 있도록 한다.

props 전달은 UI 트리를 통해 데이터를 사용하는 컴포넌트로 데이터를 명시적으로 파이프하는 좋은 방법이다.
그러나 트리 전체에 걸쳐 일부 prop을 전달해야 하거나 많은 컴포넌트에 동일한 prop 전달은 장황하고 불편해질 수 있다. 가장 가까운 공통 조상은 데이터가 필요한 컴포넌트에서 멀리 떨여저 있을 수 있으며, 상태를 그 높이로 끌어올리면 “prop drilling”이라는 상황이 발생할 수 있다.
props를 전달하지 않고도 데이터가 필요한 트리의 컴포넌트로 데이터를 “순간 이동”할 수 있는 방법이 있다면 좋지 않을까? 리액트의 컨텍스트 기능이 있다.
컨텍스트를 사용하면 상위 컴포넌트가 그 아래의 전체 트리에 데이터를 제공할 수 있다. 컨텍스트에는 다양한 용도가 있다. 여기에 한 가지 예가 있다. level 을 허용하는 다음 Heading 컴포넌트를 봐라.
// App.js
import Heading from './Heading.js';
import Section from './Section.js';
export default function Page() {
return (
<Section>
<Heading level={1}>Title</Heading>
<Heading level={2}>Heading</Heading>
<Heading level={3}>Sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={5}>Sub-sub-sub-heading</Heading>
<Heading level={6}>Sub-sub-sub-sub-heading</Heading>
</Section>
);
}
// Section.js
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
// Heading.js
export default function Heading({ level, children }) {
switch (level) {
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);
}
}
동일한 Section 내의 여러 제목이 항상 동일한 크기를 갖기를 원한다고 가정해 보겠다.
// App.js
import Heading from './Heading.js';
import Section from './Section.js';
export default function Page() {
return (
<Section>
<Heading level={1}>Title</Heading>
<Section>
<Heading level={2}>Heading</Heading>
<Heading level={2}>Heading</Heading>
<Heading level={2}>Heading</Heading>
<Section>
<Heading level={3}>Sub-heading</Heading>
<Heading level={3}>Sub-heading</Heading>
<Heading level={3}>Sub-heading</Heading>
<Section>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
</Section>
</Section>
</Section>
</Section>
);
}
현재는 각 <Heading> 에 level prop을 개별적으로 전달한다.
<Section>
<Heading level={3}>About</Heading>
<Heading level={3}>Photos</Heading>
<Heading level={3}>Videos</Heading>
</Section>
대신 level prop을 <Section> 컴포넌트에 전달하고 <Heading> 에서 제거할 수 있다면 좋을 것ㅇ이다. 이렇게 하면 동일한 섹션의 모든 제목이 동일한 크기를 갖도록 강제할 수 있다.
<Section level={3}>
<Heading>About</Heading>
<Heading>Photos</Heading>
<Heading>Videos</Heading>
</Section>
하지만 <Heading> 컴포넌트가 가장 가까운 <Section> 의 수준을 어떻게 알 수 있을까? 이를 위해서는 어린이가 트리의 위 어딘가에서 데이터를 “요청”할 수 있는 방법이 필요하다.
props만으로는 할 수 없다. 이것이 바로 컨텍스트가 작용하는 곳이다. 이 작업은 세 단계로 수행된다.
LevelContext 라고 부를 수 있다.)Heading 은 LevelContext 를 사용한다.)Section 에서는 LevelContext 를 제공한다.)컨텍스트를 사용하면 부모(멀리 떨어져 있는 부모라도)가 내부 트리 전체에 일부 데이터를 제공할 수 있다.
먼저 컨텍스트를 만들어야 한다. 컴포넌트에서 사용할 수 있도록 파일에서 내보내야 한다.
// LevelContext.js
import { createContext } from 'react';
export const LevelContext = createContext(1);
createContext 에 대한 유일한 인수는 기본값이다. 여기서 1 은 가장 큰 제목 수준을 의미하지만 모든 종류의 값(객체 포함)을 전달할 수 있다. 다음 단계에서 기본값의 중요성을 확인하게 된다.
리액트와 컨텍스트에서 useContext 훅을 가져온다.
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
현재 Heading 컴포넌트는 props에서 level 을 읽는다.
export default function Heading({ level, children }) {
// ...
}
대신, level prop을 제거하고 방금 가져온 컨텍스트인 LevelContext 에서 값을 읽어라.
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
useContext 는 훅이다. useState 와 useReducer 와 마찬가지로 리액트 컴포넌트 내에서만 즉시 훅을 호출할 수 있다.(루프나 조건 내에서는 호출할 수 없다.) useContext 는 Heading 컴포넌트가 LevelContext 를 읽고 싶어한다고 리액트에게 알려준다.
이제 Heading 컴포넌트에는 level prop이 없으므로 더 이상 다음과 같이 JSX의 Heading 에 레벨 prop을 전달할 필요가 없다.
<Section>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
</Section>
대신 JSX를 수신하는 Section 이 되도록 JSX를 업데이트해라.
<Section level={4}>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>
참고로 다음은 작업하려고 했던 마크업이다.
// App.js
import Heading from './Heading.js';
import Section from './Section.js';
export default function Page() {
return (
<Section level={1}>
<Heading>Title</Heading>
<Section level={2}>
<Heading>Heading</Heading>
<Heading>Heading</Heading>
<Heading>Heading</Heading>
<Section level={3}>
<Heading>Sub-heading</Heading>
<Heading>Sub-heading</Heading>
<Heading>Sub-heading</Heading>
<Section level={4}>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>
</Section>
</Section>
</Section>
);
}
// Heading.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Heading({ children }) {
const level = useContext(LevelContext);
switch (level) {
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);
}
}
이 예는 아직 제대로 작동하지 않는다. 컨텍스트를 사용하더라도 아직 컨텍스트를 제공하지 않았기 때문에 모든 제목의 크기는 동일하다. 리액트는 어디서 구할 수 있는지 모른다.
컨텍스트를 제공하지 않으면 리액트는 이전 단계에서 지정한 기본값을 사용한다. 이 예에서는 createContext 에 대한 인수로 1 을 지정했으므로 useContext(LevelContext) 는 1 을 반환하고 모든 제목을 <h1> 로 설정한다. 각 Section 이 고유한 컨텍스트를 제공하도록 하여 이 문제를 해결해 보겠다.
Section 컴포넌트는 현재 하위 항목을 렌더링한다.
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
LevelContext 를 제공하기 위해 컨텍스트 공급자로 래핑한다.
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>
{children}
</LevelContext.Provider>
</section>
);
}
리액트에게 “이 <Section> 내부의 컴포넌트가 LevelContext 를 요청하는 경우 이 level 을 제공한다.”라고 알려준다. 컴포넌트는 위의 UI 트리에서 가장 가까운 <LevelContext.Provider> 값을 사용한다.
원본 코드와 결과는 동일하지만 각 Heading 컴포넌트 요소에 level prop을 전달할 필요가 없다. 대신 위의 가장 가까운 Section 을 요청하여 제목 레벨을 “파악”해야한다.
<Section> 에 level prop을 전달한다.Section 의 하위 항목을 <LevelContext.Provider value={level}> 로 래핑한다.Heading 은 useContext(LevelContext) 를 사용하여 위의 LevelContext 에 가장 가까운 값을 묻는다.현재는 여전히 각 섹션의 level 을 수동으로 지정해야 한다.
export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...
컨텍스트를 통해 위 컴포넌트의 정보를 읽을 수 있으므로 각 Section의 위 Section 의 level 을 읽고 자동으로 level + 1 을 아래로 전달할 수 있다. 방법은 다음과 같다.
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>
);
}
이 변경으로 인해 level prop을 <Section> 또는 <Heading> 에 전달할 필요가 없다.
이제 Heading 과 Section 모두 LevelContext 를 읽어서 얼마나 "깊은”지 파악한다. 그리고 Section 은 내부의 모든 항목이 “더 깊은” 수준에 있음을 지정하기 위해 하위 항목을 LevelContext 로 래핑한다.
이 예에서는 중첩된 컴포넌트가 컨텍스트를 재정의하는 방법을 시각적으로 보여주기 때문에 제목 수준으로 사용한다. 그러나 컨텍스트는 다른 많은 사용 사례에도 유용하다. 현재 색상 테마, 현재 로그인한 사용자 등 전체 하위 트리에 필요한 모든 정보를 전달할 수 있다.
컨텍스트를 제공하는 컴포넌트와 이를 사용하는 컴포넌트 사이에 원하는 만큼 컴포넌트를 삽입할 수 있다. 여기에는 <div> 와 같은 내장 컴포넌트와 사용자가 직접 빌드할 수 있는 컴포넌트가 모두 포함된다.
이 예에서는 동일한 Post 컴포넌트(점선 테두리)가 두 개의 서로 다른 중첩 수준에서 렌더링된다. 내부의 <Heading> 은 가장 가까운 <Section> 에서 자동으로 레벨을 가져온다.
// App.js
import Heading from './Heading.js';
import Section from './Section.js';
export default function ProfilePage() {
return (
<Section>
<Heading>My Profile</Heading>
<Post
title="Hello traveller!"
body="Read about my adventures."
/>
<AllPosts />
</Section>
);
}
function AllPosts() {
return (
<Section>
<Heading>Posts</Heading>
<RecentPosts />
</Section>
);
}
function RecentPosts() {
return (
<Section>
<Heading>Recent Posts</Heading>
<Post
title="Flavors of Lisbon"
body="...those pastéis de nata!"
/>
<Post
title="Buenos Aires in the rhythm of tango"
body="I loved it!"
/>
</Section>
);
}
function Post({ title, body }) {
return (
<Section isFancy={true}>
<Heading>
{title}
</Heading>
<p><i>{body}</i></p>
</Section>
);
}
// Section.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children, isFancy }) {
const level = useContext(LevelContext);
return (
<section className={
'section ' +
(isFancy ? 'fancy' : '')
}>
<LevelContext.Provider value={level + 1}>
{children}
</LevelContext.Provider>
</section>
);
}
// Heading.js
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);
}
}
// LevelContext.js
import { createContext } from 'react';
export const LevelContext = createContext(0);
이 작업을 수행하기 위해 특별한 작업을 수행하지 않았다. Section 은 그 안의 트리에 대한 컨텍스트를 지정하므로 어디에나 <Heading> 을 삽입할 수 있으며 올바른 크기를 갖게 된다.
컨텍스트를 사용하면 “주변 환경에 적응”하고 렌더링되는 위치(즉, 어떤 컨텍스트에서)에 따라 다르게 표시되는 컴포넌트를 작성할 수 있다.
컨텍스트가 작동하는 방식은 CSS 속성 상속을 상기시킬 수 있다. CSS에서는 <div> 에 color: blue 를 지정할 수 있으며, 그 안에 있는 모든 DOM 노드는 중간에 있는 다른 DOM 노드가 color: green 으로 재정의 하지 않는 한 그 색상을 상속한다. 마찬가지로, 리액트에서 위에서 오는 일부 컨텍스트를 재정의하는 유일한 방법은 하위 항목을 다른 값을 가진 컨텍스트 공급자로 래핑하는 것이다.
CSS에서는 color 와 background-color 과 같은 서로 다른 속성이 서로 재정의되지 않는다. background-color 에 영향을 주지 않고 모든 <div> 의 color 를 빨간색으로 설정할 수 있다. 마찬가지로, 서로다른 리액트 컨텍스트는 서로를 재정의하지 않는다. createContext() 를 사용하여 만드는 각 컨텍스트는 다른 컨텍스트와 완전히 분리되어 있으며 특정 컨텍스트를 사용하고 제공하는 컴포넌트를 함께 연결합니다. 하나의 컴포넌트는 문제 없이 다양한 컨텍스트를 사용하거나 제공할 수 있다.
컨텍스트는 사용하기에 매우 유혹적이다. 그러나 이는 또한 남용하기가 너무 쉽다는 것을 의미한다. 몇 가지 수준의 세부 정보를 전달해야 한다고 해서 해당 정보를 컨텍스트에 포함해야 한다는 의미는 아니다.
컨텍스트를 사용하기 전에 고려해야할 몇 가지 대안은 다음과 같다.
<Layout posts={posts} /> 와 같이 직접 사용하지 않는 시각적 컴포넌트에 posts 와 같은 데이터 prop을 전달할 수 있다. 대신 Layout 이 children 을 prop으로 받아들이고 <Layout><Posts posts={posts} /></Layout> 을 렌더링하도록 만들어라. 이렇게 하면 데이터를 지정하는 컴포넌트와 해당 데이터가 필요한 컴포넌트 사이의 레이어 수가 줄어든다.이러한 접근 방ㅂ식 중 어느 것도 적합하지 않은 경우 상황을 고려해라.
컨텍스트는 정적 값으로 제한되지 않는다. 다음 렌더링에서 다른 값을 전달하면 리액트는 아래에서 이를 읽는 모든 컴포넌트를 업데이트한다. 이것이 바로 컨텍스트가 종종 상태와 결합하여 사용되는 이유이다.
일반적으로 트리의 서로 다른 부분에 있는 멀리 있는 컴포넌트에 일부 정보가 필요한 경우 컨텍스트가 도움이 될 것이라는 좋은 표시다.
export const MyContext = createContext(defaultValue) 를 사용하여 만들고 내보낸다.useContext(MyContext) 훅을 전달해라.<MyContext.Provider value={...}> 로 묶어 상위 항목에서 제공한다.