안녕하세요! 프론트엔드 개발자를 향해 열심히 나아가고 계신 여러분을 위한 React 공식문서 해설 시간입니다.
대개 부모 컴포넌트에서 자식 컴포넌트로 정보를 전달할 때는 props를 사용하죠. 하지만 중간에 수많은 컴포넌트를 거쳐야 하거나, 앱의 여러 컴포넌트에서 동일한 정보가 필요할 때 props를 일일이 전달하는 건 코드도 길어지고 정말 불편해질 수 있어요. Context를 사용하면 부모 컴포넌트가 props를 통해 명시적으로 전달하지 않아도, 그 아래 트리에 있는 모든 컴포넌트(아무리 깊은 곳에 있어도 상관없어요!)에서 특정 정보를 바로 사용할 수 있게 만들어줍니다.
💡 강사의 팁: 멋진 웹 프로필이나 책을 정리하는 독후감 사이트 같은 포트폴리오를 만드실 때, 컴포넌트 구조가 복잡해지기 시작하면 반드시 마주치게 되는 개념이 바로 이 Context랍니다!
이 문서를 통해 다음 내용들을 배우게 됩니다:
Props 전달하기 (Passing props)는 UI 트리를 따라 데이터를 사용하는 컴포넌트까지 명시적으로 데이터를 꽂아주는(pipe) 아주 훌륭한 방법이에요. 데이터가 어디서 와서 어디로 가는지 한눈에 파악하기 좋죠.
하지만 트리의 아주 깊은 곳으로 어떤 prop을 전달해야 하거나, 여러 컴포넌트가 동일한 prop을 필요로 할 때는 코드가 장황해지고 불편해질 수 있어요. 데이터를 필요로 하는 컴포넌트들의 가장 가까운 공통 조상이 트리 상에서 너무 멀리 떨어져 있을 수 있거든요. 그렇게 높은 곳까지 상태 끌어올리기 (Lifting state up)를 하다 보면, 일명 "prop drilling(프롭 드릴링)"이라는 골치 아픈 상황에 빠지게 됩니다.
💡 강사의 팁: 'Prop Drilling'이란 마치 드릴로 땅을 파고 들어가듯, 데이터를 쓰지도 않는 중간 컴포넌트들을 계속 관통하며 props를 밑으로 내려보내는 현상을 말해요. 유지보수를 굉장히 힘들게 만드는 주범 중 하나죠! 이 개념을 시각적으로 돕기 위해 다이어그램을 하나 볼까요?
상태 끌어올리기 (Lifting state up)
Prop drilling (프롭 드릴링)
props를 일일이 전달하지 않고도, 트리의 필요한 컴포넌트로 데이터를 "순간이동" 시킬 수 있는 방법이 있다면 정말 멋지지 않을까요? React의 context 기능이 바로 그걸 해냅니다!
Context를 사용하면 부모 컴포넌트가 그 아래에 있는 전체 트리에 데이터를 제공할 수 있어요. Context의 활용처는 아주 다양하죠. 여기 한 가지 예시를 볼까요? 크기를 결정하는 level을 prop으로 받는 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>
);
}
// src/Section.js
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
// src/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);
}
}
// styles.css
.section {
padding: 10px;
margin: 5px;
border-radius: 5px;
border: 1px solid #aaa;
}
만약 같은 Section 안에 있는 여러 헤딩(Heading)들이 항상 같은 크기를 가지게 하고 싶다면 어떨까요?
// 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>
);
}
// src/Section.js
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
// src/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);
}
}
// styles.css
.section {
padding: 10px;
margin: 5px;
border-radius: 5px;
border: 1px solid #aaa;
}
현재 코드를 보면, 각 <Heading> 컴포넌트마다 level prop을 개별적으로 따로따로 전달해주고 있어요.
<Section>
<Heading level={3}>About</Heading>
<Heading level={3}>Photos</Heading>
<Heading level={3}>Videos</Heading>
</Section>
이 level prop을 <Heading>에서 빼고, 대신 <Section> 컴포넌트에 한 번만 전달할 수 있다면 훨씬 깔끔하겠죠? 이렇게 하면 같은 섹션 안에 있는 모든 헤딩이 무조건 동일한 크기를 갖도록 강제할 수 있을 테니까요.
<Section level={3}>
<Heading>About</Heading>
<Heading>Photos</Heading>
<Heading>Videos</Heading>
</Section>
하지만, <Heading> 컴포넌트가 도대체 어떻게 자신과 가장 가까운 <Section>의 level 값을 알 수 있을까요? 그렇게 하려면 자식 컴포넌트가 트리 상의 자기보다 위쪽에 있는 어딘가에 데이터를 "요청(ask)"할 수 있는 방법이 필요합니다.
단순히 props만으로는 이 작업을 할 수 없어요. 바로 여기서 Context가 등장합니다! 우리는 이 작업을 3단계에 걸쳐서 해볼 거예요.
LevelContext라고 부르면 좋겠네요.)Heading 컴포넌트가 LevelContext를 사용할 거예요.)Section 컴포넌트가 LevelContext를 제공할 거예요.)Context는 부모가 그 내부의 전체 트리에게 특정 데이터를 제공할 수 있게 해줍니다. 아무리 멀리 떨어져 있는 자식이라도 상관없이 말이죠!
가까운 자식에서 context 사용하기
멀리 떨어진 자식에서 context 사용하기
먼저, Context를 만들어야 해요. 다른 컴포넌트들이 이 Context를 가져다 쓸 수 있도록 파일에서 export(내보내기) 를 해줘야 합니다.
// 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>
);
}
// src/Section.js
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
// src/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);
}
}
// src/LevelContext.js (active)
import { createContext } from 'react';
export const LevelContext = createContext(1);
// styles.css
.section {
padding: 10px;
margin: 5px;
border-radius: 5px;
border: 1px solid #aaa;
}
createContext 함수에 들어가는 유일한 인자는 바로 기본값(default value) 이에요. 여기서는 가장 큰 헤딩 레벨을 의미하는 1을 넣었지만, 객체(object)를 포함해서 어떤 종류의 값이라도 전달할 수 있답니다. 이 기본값이 어떤 의미를 가지는지는 다음 단계에서 자세히 보실 수 있을 거예요.
💡 강사의 팁: createContext를 할 때 초기값을 잘 설정해두면, 나중에 Provider로 감싸지 않은 곳에서 실수로 Context를 호출했을 때 앱이 터지는 걸 방지할 수 있는 좋은 안전망 역할을 해줘요!
React에서 useContext Hook을 불러오고, 방금 만든 Context도 함께 import 해주세요.
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
현재 Heading 컴포넌트는 props에서 level을 읽어오고 있죠.
export default function Heading({ level, children }) {
// ...
}
이걸 수정해봅시다. level prop을 지우고, 대신 아까 import 했던 LevelContext에서 값을 읽어오도록 할게요.
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
useContext는 Hook입니다. useState나 useReducer와 마찬가지로, Hook은 반드시 React 컴포넌트의 최상위(immediately inside)에서만 호출할 수 있어요. (반복문이나 조건문 안에서는 호출하면 안 된다는 거, 프론트엔드 개발자라면 꼭 기억하셔야 해요!) 이 useContext는 React에게 "Heading 컴포넌트가 LevelContext를 읽고 싶어해!"라고 알려주는 역할을 합니다.
자, 이제 Heading 컴포넌트에 level prop이 필요 없어졌으니, JSX에서 <Heading>을 사용할 때 일일이 level prop을 넘겨주던 부분도 지워야겠죠.
<Section>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
</Section>
대신에, 이 값을 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>
);
}
// src/Section.js
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
// src/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);
}
}
// src/LevelContext.js
import { createContext } from 'react';
export const LevelContext = createContext(1);
// styles.css
.section {
padding: 10px;
margin: 5px;
border-radius: 5px;
border: 1px solid #aaa;
}
어라? 결과를 보니 아직 제대로 작동하지 않는 걸 눈치채셨나요? 모든 헤딩의 크기가 다 똑같이 나오고 있어요. 그 이유는 우리가 context를 사용(Use) 하고는 있지만, 아직 제공(Provide) 하지는 않았기 때문이에요. React 입장에서는 어디서 그 값을 가져와야 할지 아직 모르는 거죠!
Context를 제공해주지 않으면, React는 우리가 이전 단계에서 지정했던 기본값(default value)을 사용하게 됩니다. 이 예제에서는 createContext의 인자로 1을 지정했기 때문에, useContext(LevelContext)가 무조건 1을 반환해서 모든 헤딩이 <h1>으로 설정된 거랍니다. 자, 이제 각 Section이 자기만의 Context를 제공하도록 만들어서 이 문제를 해결해봅시다.
현재 Section 컴포넌트는 단순히 자식들(children)을 렌더링하고 있어요.
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
이 자식들에게 LevelContext를 제공하기 위해, 자식들을 Context provider로 감싸주세요.
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext value={level}>
{children}
</LevelContext>
</section>
);
}
💡 강사의 팁: 예전 React 버전이나 서드파티 라이브러리 문서에서는 <LevelContext.Provider value={...}> 형태로 많이 보셨을 텐데, 최신 트렌드와 이 공식문서에서는 깔끔하게 <LevelContext value={...}>로 감싸는 방식을 소개하고 있습니다.
이 코드는 React에게 이렇게 말하는 것과 같아요. "만약 이 <Section> 안에 있는 어떤 컴포넌트라도 LevelContext를 요청하면, 내가 가지고 있는 이 level 값을 줘!" 컴포넌트는 자신보다 트리 위쪽에 있는 가장 가까운 <LevelContext>의 값을 사용하게 됩니다.
// 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>
);
}
// src/Section.js
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext value={level}>
{children}
</LevelContext>
</section>
);
}
// src/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);
}
}
// src/LevelContext.js
import { createContext } from 'react';
export const LevelContext = createContext(1);
// styles.css
.section {
padding: 10px;
margin: 5px;
border-radius: 5px;
border: 1px solid #aaa;
}
짠! 화면을 보면 맨 처음 작성했던 코드와 결과가 완전히 똑같아졌어요. 하지만 이제 우리는 각각의 Heading 컴포넌트마다 level prop을 넘겨줄 필요가 없어졌습니다! 대신에 Heading 컴포넌트는 자기 위쪽에 있는 가장 가까운 Section에게 물어봐서 자신의 헤딩 레벨을 스스로 "알아냅니다(figures out)".
전체 흐름을 정리해볼까요?
1. 여러분이 <Section>에 level prop을 전달합니다.
2. Section은 자식들을 <LevelContext value={level}>로 감싸줍니다.
3. Heading은 useContext(LevelContext)를 사용해서, 자신보다 위쪽에 있는 LevelContext의 가장 가까운 값을 요청합니다.
자, 한 가지 아쉬운 점이 남았네요. 여전히 우리는 각 섹션의 level을 수동으로 일일이 지정해주고 있어요.
export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...
Context는 위쪽 컴포넌트에서 정보를 읽어올 수 있게 해주잖아요? 그렇다면, 각각의 Section이 바로 위쪽의 Section으로부터 level을 읽어오고, 자신의 자식들에게는 자동으로 level + 1을 전달하게 만들 수도 있지 않을까요? 자, 이렇게 코드를 고쳐봅시다.
// src/Section.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext value={level + 1}>
{children}
</LevelContext>
</section>
);
}
이렇게 변경하고 나면, 이제 <Section> 이나 <Heading> 그 어떤 곳에도 level prop을 전달할 필요가 전혀 없어집니다!
// App.js
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>
);
}
// src/Section.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext value={level + 1}>
{children}
</LevelContext>
</section>
);
}
// src/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);
}
}
// src/LevelContext.js
import { createContext } from 'react';
export const LevelContext = createContext(0);
// styles.css
.section {
padding: 10px;
margin: 5px;
border-radius: 5px;
border: 1px solid #aaa;
}
이제 Heading과 Section 모두 LevelContext를 읽어들여서 현재 자신들이 얼마나 "깊은" 곳에 위치해 있는지 알아냅니다. 그리고 Section은 자신의 자식들을 다시 LevelContext로 감싸서 그 안에 있는 모든 것들이 한 단계 더 "깊은" 레벨에 있다는 것을 명시해 주는 거죠.
💡 참고사항: 이번 예제에서는 중첩된 컴포넌트가 Context를 어떻게 덮어쓰는지(override) 시각적으로 잘 보여주기 위해 헤딩 레벨을 사용했어요. 하지만 Context는 이 밖에도 훨씬 다양한 상황에서 아주 유용하게 쓰인답니다. 현재 색상 테마(다크 모드 등), 현재 로그인한 사용자 정보 등 전체 서브 트리에서 필요한 그 어떤 정보라도 아래로 내려보낼 수 있어요.
Context를 제공하는 컴포넌트와 그걸 사용하는 컴포넌트 사이에 여러분이 원하는 만큼 수많은 컴포넌트를 끼워 넣을 수 있어요. <div> 같은 기본 내장 HTML 요소는 물론이고 여러분이 직접 만든 컴포넌트들도 전부 포함해서요.
이 예제를 볼까요? 점선 테두리를 가진 똑같은 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>
);
}
// src/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 value={level + 1}>
{children}
</LevelContext>
</section>
);
}
// src/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);
}
}
// src/LevelContext.js
import { createContext } from 'react';
export const LevelContext = createContext(0);
// styles.css
.section {
padding: 10px;
margin: 5px;
border-radius: 5px;
border: 1px solid #aaa;
}
.fancy {
border: 4px dashed pink;
}
이게 작동하도록 만들기 위해 여러분이 특별히 추가로 한 일은 없어요. Section이 자기 내부의 트리를 위한 Context를 지정해주기 때문에, <Heading>을 아무데나 끼워 넣어도 알아서 올바른 크기를 가지게 되는 거죠. 위의 샌드박스에서 직접 요리조리 바꿔보며 테스트해 보세요!
Context를 사용하면 "주변 환경에 적응하는" 컴포넌트를 만들 수 있어요. 컴포넌트가 어디서 렌더링되느냐(다시 말해, 어떤 context 안에 있느냐)에 따라 자신을 다르게 표현할 수 있게 되는 겁니다.
Context가 작동하는 방식은 웹을 공부하시면서 친숙하게 접하셨을 CSS 속성 상속 (CSS property inheritance)과 매우 비슷해요. CSS에서 어떤 <div>에 color: blue를 지정하면, 그 안에 있는 모든 DOM 노드는 아무리 깊숙이 있어도 파란색을 상속받게 되죠. 중간에 있는 다른 DOM 노드가 color: green으로 덮어쓰지 않는 이상 말이에요. 마찬가지로 React에서도, 위에서 내려오는 Context 값을 덮어쓰는 유일한 방법은 자식들을 다른 값을 가진 Context Provider로 다시 감싸는 것뿐입니다.
또한 CSS에서 color와 background-color 같은 서로 다른 속성들이 서로를 덮어쓰지 않고 독립적으로 작동하는 것처럼, React의 서로 다른 Context들도 서로를 덮어쓰거나 간섭하지 않습니다. createContext()로 만든 각각의 Context는 다른 Context들과 완전히 분리되어 있으며, 오직 그 특정 Context를 제공하고 사용하는 컴포넌트들끼리만 연결해 줘요. 하나의 컴포넌트가 여러 개의 서로 다른 Context를 동시에 사용하거나 제공하는 것도 전혀 문제가 되지 않는답니다.
Context는 정말 편리해서 마구 쓰고 싶은 유혹에 빠지기 쉬워요! 하지만 그만큼 남용하기도 너무 쉽다는 뜻이기도 하죠. 단지 props를 몇 단계 아래로 넘겨줘야 한다고 해서, 무조건 그 정보를 Context에 집어넣어야 하는 건 절대 아닙니다.
Context를 도입하기 전에, 먼저 다음의 대안들을 고려해 보는 걸 강력히 추천해 드려요.
children으로 전달해 보세요. 만약 어떤 데이터를 직접 쓰지도 않는 중간 컴포넌트 계층을 통해 그저 통과만 시키고 있다면, 중간에 컴포넌트를 제대로 추출하지 않았다는 뜻일 수 있어요. 예를 들어, 시각적인 레이아웃만 잡아주는 컴포넌트에 데이터 prop을 넘겨서 <Layout posts={posts} /> 처럼 쓰고 있진 않나요? 대신에 Layout이 children을 prop으로 받게 만들고, <Layout><Posts posts={posts} /></Layout> 처럼 렌더링해 보세요. 이렇게 하면 데이터를 지정하는 컴포넌트와 데이터를 필요로 하는 컴포넌트 사이의 층(layer)을 확 줄일 수 있습니다.이 두 가지 접근법을 모두 시도해봤는데도 코드가 깔끔해지지 않는다면, 그때 Context를 도입하는 게 가장 좋습니다.
실제 현업의 프론트엔드 프로젝트에서 Context가 어떻게 쓰이는지 몇 가지 대표적인 사례를 살펴볼까요?
Context는 꼭 고정된 정적(static) 값에만 쓸 수 있는 게 아니에요. 다음 렌더링 때 여러분이 다른 값을 전달해주면, React는 그 Context를 읽고 있는 아래쪽의 모든 컴포넌트들을 싹 다 업데이트(리렌더링) 해준답니다! 바로 이런 이유 때문에 Context가 state(상태)와 짝꿍으로 자주 쓰이는 거예요.
결론적으로, 트리 상의 서로 다른 위치에 있는 멀리 떨어진 컴포넌트들이 어떤 동일한 정보를 필요로 한다면, 거기가 바로 Context가 활약할 좋은 타이밍이라는 신호입니다.
요약해 볼까요?
export const MyContext = createContext(defaultValue) 로 Context를 만들고 외부에서 쓸 수 있게 export 합니다.useContext(MyContext) Hook을 호출해서 그 값을 읽어옵니다.<MyContext value={...}> 로 감싸서 값을 제공합니다.children으로 넘기는 방법을 먼저 시도해 보는 걸 잊지 마세요!이 예제에서, 체크박스를 토글(toggle)하면 각각의 <PlaceImage>에 전달되는 imageSize prop이 변경됩니다. 체크박스의 상태(state)는 최상위인 App 컴포넌트가 가지고 있지만, 저 아래에 있는 각각의 <PlaceImage>가 이 값을 알아야만 해요.
현재 코드를 보면, App이 imageSize를 List에게 전달하고, List는 그걸 Place에게, Place는 다시 그걸 PlaceImage에게 넘겨주고 있습니다. 일일이 전달하고 있죠? 자, 이제 imageSize prop 전달을 다 지워버리세요! 그리고 대신에 App 컴포넌트에서 PlaceImage로 그 값을 직접 전달하도록 Context를 사용해 보세요.
Context 선언은 Context.js 파일에서 하시면 됩니다.
// src/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}
/>
);
}
// src/Context.js
// src/data.js
export const places = [{
id: 0,
name: 'Bo-Kaap in Cape Town, South Africa',
description: 'The tradition of choosing bright colors for houses began in the late 20th century.',
imageId: 'K9HVAGH'
}, {
id: 1,
name: 'Rainbow Village in Taichung, Taiwan',
description: 'To save the houses from demolition, Huang Yung-Fu, a local resident, painted all 1,200 of them in 1924.',
imageId: '9EAYZrt'
}, {
id: 2,
name: 'Macromural de Pachuca, Mexico',
description: 'One of the largest murals in the world covering homes in a hillside neighborhood.',
imageId: 'DgXHVwu'
}, {
id: 3,
name: 'Selarón Staircase in Rio de Janeiro, Brazil',
description: 'This landmark was created by Jorge Selarón, a Chilean-born artist, as a "tribute to the Brazilian people."',
imageId: 'aeO3rpI'
}, {
id: 4,
name: 'Burano, Italy',
description: 'The houses are painted following a specific color system dating back to 16th century.',
imageId: 'kxsph5C'
}, {
id: 5,
name: 'Chefchaouen, Marocco',
description: 'There are a few theories on why the houses are painted blue, including that the color repels mosquitos or that it symbolizes sky and heaven.',
imageId: 'rTqKo46'
}, {
id: 6,
name: 'Gamcheon Culture Village in Busan, South Korea',
description: 'In 2009, the village was converted into a cultural hub by painting the houses and featuring exhibitions and art installations.',
imageId: 'ZfQOOzf'
}];
// src/utils.js
export function getImageUrl(place) {
return (
'[https://i.imgur.com/](https://i.imgur.com/)' +
place.imageId +
'l.jpg'
);
}
/* styles.css */
ul { list-style-type: none; padding: 0px 10px; }
li {
margin-bottom: 10px;
display: grid;
grid-template-columns: auto 1fr;
gap: 20px;
align-items: center;
}
모든 컴포넌트에서 거추장스러운 imageSize prop을 완전히 제거해 줍니다.
Context.js 파일에서 ImageSizeContext를 만들고 export 해주세요. 그런 다음 App.js에서 List를 <ImageSizeContext value={imageSize}>로 감싸서 값을 아래로 제공하고, 저 아래에 있는 PlaceImage 컴포넌트에서는 useContext(ImageSizeContext)를 써서 그 값을 쏙 뽑아 읽으면 완성입니다!
// src/App.js
import { useState, useContext } from 'react';
import { places } from './data.js';
import { getImageUrl } from './utils.js';
import { ImageSizeContext } from './Context.js';
export default function App() {
const [isLarge, setIsLarge] = useState(false);
const imageSize = isLarge ? 150 : 100;
return (
<ImageSizeContext
value={imageSize}
>
<label>
<input
type="checkbox"
checked={isLarge}
onChange={e => {
setIsLarge(e.target.checked);
}}
/>
Use large images
</label>
<hr />
<List />
</ImageSizeContext>
)
}
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(ImageSizeContext);
return (
<img
src={getImageUrl(place)}
alt={place.name}
width={imageSize}
height={imageSize}
/>
);
}
// src/Context.js
import { createContext } from 'react';
export const ImageSizeContext = createContext(500);
// src/data.js
export const places = [{
id: 0,
name: 'Bo-Kaap in Cape Town, South Africa',
description: 'The tradition of choosing bright colors for houses began in the late 20th century.',
imageId: 'K9HVAGH'
}, {
id: 1,
name: 'Rainbow Village in Taichung, Taiwan',
description: 'To save the houses from demolition, Huang Yung-Fu, a local resident, painted all 1,200 of them in 1924.',
imageId: '9EAYZrt'
}, {
id: 2,
name: 'Macromural de Pachuca, Mexico',
description: 'One of the largest murals in the world covering homes in a hillside neighborhood.',
imageId: 'DgXHVwu'
}, {
id: 3,
name: 'Selarón Staircase in Rio de Janeiro, Brazil',
description: 'This landmark was created by Jorge Selarón, a Chilean-born artist, as a "tribute to the Brazilian people".',
imageId: 'aeO3rpI'
}, {
id: 4,
name: 'Burano, Italy',
description: 'The houses are painted following a specific color system dating back to 16th century.',
imageId: 'kxsph5C'
}, {
id: 5,
name: 'Chefchaouen, Marocco',
description: 'There are a few theories on why the houses are painted blue, including that the color repels mosquitos or that it symbolizes sky and heaven.',
imageId: 'rTqKo46'
}, {
id: 6,
name: 'Gamcheon Culture Village in Busan, South Korea',
description: 'In 2009, the village was converted into a cultural hub by painting the houses and featuring exhibitions and art installations.',
imageId: 'ZfQOOzf'
}];
// src/utils.js
export function getImageUrl(place) {
return (
'[https://i.imgur.com/](https://i.imgur.com/)' +
place.imageId +
'l.jpg'
);
}
/* styles.css */
ul { list-style-type: none; padding: 0px 10px; }
li {
margin-bottom: 10px;
display: grid;
grid-template-columns: auto 1fr;
gap: 20px;
align-items: center;
}
중간에 있는 컴포넌트들(List나 Place 같은 녀석들)이 더 이상 억지로 imageSize를 받아서 넘겨줄 필요가 사라진 걸 확인하셨나요? 코드가 훨씬 깔끔해졌죠!
모든 문서 페이지 개요 (Overview of all docs pages)
강의자료 번역이 완료되었습니다! React Context 파트는 처음엔 낯설 수 있지만, 계속 코드를 만져보다 보면 그 강력함에 푹 빠지게 되실 거예요. 공부하시다가 막히는 부분이나 다른 공식 문서 번역이 필요하시면 언제든 또 말씀해주세요! 더 도와드릴 게 있을까요?