컨텍스트와 함께 깊이 있는 데이터 전달
일반적으로 부모 컴포넌트에서 자식 컴포넌트로 정보를 전달할 때는 props를 통해 전달합니다. 하지만 중간에 여러 컴포넌트를 거쳐야 하거나 앱의 여러 컴포넌트가 동일한 정보를 필요로 하는 경우 props 전달은 장황하고 불편할 수 있습니다. Context를 사용하면 부모 컴포넌트가 props를 통해 명시적으로 전달하지 않고도 그 아래 트리에 있는 모든 컴포넌트가 일부 정보를 사용할 수 있습니다.
학습 내용
- 'props 드릴링'이란 무엇인가요?
- 반복적인 props 전달을 context로 대체하는 방법
- context의 일반적인 사용 사례
- context에 대한 일반적인 대안
props 전달의 문제점
props 전달은 UI 트리를 통해 props를 사용하는 컴포넌트로 데이터를 명시적으로 연결할 수 있는 좋은 방법입니다.
하지만 일부 props를 트리 깊숙이 전달해야 하거나 여러 컴포넌트에 동일한 prop이 필요한 경우 props 전달은 장황하고 불편할 수 있습니다. 가장 가까운 공통 부모는 데이터가 필요한 컴포넌트에서 멀리 떨어져 있을 수 있으며, 상태를 그렇게 높이 올리면 "props 드릴링"이라고도 하는 상황이 발생할 수 있습니다.
props를 전달하지 않고도 트리에서 데이터를 필요한 컴포넌트로 '텔레포트'할 수 있는 방법이 있다면 좋지 않을까요? React의 컨텍스트 기능을 사용하면 가능합니다!
Context: props 전달의 대안
컨텍스트는 부모 컴포넌트가 그 아래 전체 트리에 데이터를 제공할 수 있게 해줍니다. 컨텍스트에는 많은 용도가 있습니다. 한 가지 예를 들어보겠습니다. 크기에 따라 레벨을 받아들이는 Heading 컴포넌트를 생각해 봅시다:
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>
);
}
동일한 섹션 내의 여러 제목이 항상 같은 크기를 갖도록 하려고 한다고 가정해 보겠습니다:
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>
);
}
현재 level props를 각 <Heading>에 개별적으로 전달합니다:
<Section>
<Heading level={3}>About</Heading>
<Heading level={3}>Photos</Heading>
<Heading level={3}>Videos</Heading>
</Section>
대신 level props를 <Section> 컴포넌트로 전달하고 <Heading>에서 제거할 수 있다면 좋을 것입니다. 이렇게 하면 같은 섹션의 모든 제목이 같은 크기를 갖도록 강제할 수 있습니다:
<Section level={3}>
<Heading>About</Heading>
<Heading>Photos</Heading>
<Heading>Videos</Heading>
</Section>
하지만 <Heading> 컴포넌트가 가장 가까운 <Section>의 레벨을 어떻게 알 수 있을까요? 그러려면 자식이 트리 위 어딘가에서 데이터를 '요청'할 수 있는 방법이 필요합니다.
props만으로는 불가능합니다. 이때 Context가 중요한 역할을 합니다. 이 작업은 세 단계로 진행됩니다:
- 컨텍스트를 생성합니다. (제목 레벨에 대한 컨텍스트이므로 LevelContext라고 부를 수 있습니다.)
- 데이터가 필요한 컴포넌트에서 해당 컨텍스트를 사용합니다. (헤딩은 LevelContext를 사용합니다.)
- 데이터를 지정하는 컴포넌트에서 해당 컨텍스트를 제공합니다. (섹션은 LevelContext를 제공합니다.)
컨텍스트를 사용하면 부모(멀리 떨어져 있는 부모라도!)가 그 안에 있는 전체 트리에 일부 데이터를 제공할 수 있습니다.
1단계: 컨텍스트 만들기
먼저 컨텍스트를 만들어야 합니다. 컴포넌트에서 사용할 수 있도록 파일에서 컨텍스트를 내보내야 합니다:
import { createContext } from 'react';
export const LevelContext = createContext(1);
createContext의 유일한 인수는 기본값입니다. 여기서 1은 가장 큰 제목 수준을 의미하지만 모든 종류의 값(객체 포함)을 전달할 수 있습니다. 기본값의 중요성은 다음 단계에서 확인할 수 있습니다.
2단계: 컨텍스트 사용
React와 컨텍스트에서 사용 컨텍스트 훅을 가져옵니다:
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
현재 제목 컴포넌트는 level props를 받습니다:
export default function Heading({ level, children }) {
// ...
}
대신 level props를 제거하고 방금 임포트한 컨텍스트인 LevelContext에서 값을 읽습니다:
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
useContext는 Hook입니다. useState 및 useReducer와 마찬가지로 React 컴포넌트의 최상위 레벨에서만 Hook을 호출할 수 있습니다. useContext는 Heading 컴포넌트가 LevelContext를 읽기를 원한다고 React에 알려줍니다.
이제 Heading 컴포넌트에는 level props가 없으므로 더 이상 JSX에서 Heading에 level props를 이런 식으로 전달할 필요가 없습니다:
<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>
다시 한 번 말씀드리지만, 이 마크업이 작동하도록 하려고 했던 마크업입니다:
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>
);
}
이 예제는 아직 제대로 작동하지 않습니다! 컨텍스트를 사용하고 있지만 아직 컨텍스트를 제공하지 않았기 때문에 모든 제목의 크기가 동일합니다. React는 컨텍스트를 어디서 가져와야 할지 모릅니다!
컨텍스트를 제공하지 않으면 React는 이전 단계에서 지정한 기본값을 사용합니다. 이 예제에서는 createContext의 인수로 1을 지정했기 때문에, useContext(LevelContext)는 1을 반환하고 모든 제목을 <h1>으로 설정합니다. 각 섹션이 자체 컨텍스트를 제공하도록 하여 이 문제를 해결해 보겠습니다.
3단계: 컨텍스트 제공
Section 컴포넌트는 현재 그 자식을 렌더링합니다:
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
컨텍스트 프로바이더로 래핑하여 레벨 컨텍스트를 제공하세요:
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>
{children}
</LevelContext.Provider>
</section>
);
}
이는 React에게 "이 <Section> 안에 있는 컴포넌트가 LevelContext를 요청하면 이 레벨을 제공하라"고 지시합니다. 컴포넌트는 그 위에 있는 UI 트리에서 가장 가까운 <LevelContext.Provider>의 값을 사용합니다.
원래 코드와 동일한 결과이지만 각 Heading 컴포넌트에 level props를 전달할 필요가 없습니다! 대신, 위의 가장 가까운 Section에 요청하여 제목 level을 "알아냅니다":
- level props를 <Section>에 전달합니다.
- Section은 그 자식들을 <LevelContext.Provider value={level}>으로 래핑합니다.
- Heading은 useContext(LevelContext)를 사용하여 위의 LevelContext에서 가장 가까운 값을 묻습니다.
동일한 컴포넌트에서 컨텍스트 사용 및 제공
현재는 여전히 각 섹션의 레벨을 수동으로 지정해야 합니다:
export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...
컨텍스트를 사용하면 위의 컴포넌트에서 정보를 읽을 수 있으므로 각 섹션은 위의 섹션에서 레벨을 읽고 레벨 + 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 props를 <Section>이나 <Heading>에 전달할 필요가 없습니다:
이제 Heading과 Section은 모두 LevelContext를 읽어 얼마나 "깊은" 수준인지 파악합니다. 그리고 섹션은 그 자식들을 LevelContext로 래핑하여 그 안에 있는 모든 것이 "더 깊은" 레벨에 있음을 지정합니다.
이 예제에서는 중첩된 컴포넌트가 컨텍스트를 재정의하는 방법을 시각적으로 보여주기 위해 제목 수준을 사용합니다. 하지만 컨텍스트는 다른 많은 사용 사례에도 유용합니다. 컨텍스트를 사용하여 현재 색상 테마, 현재 로그인한 사용자 등 전체 하위 트리에 필요한 모든 정보를 전달할 수 있습니다.
컨텍스트는 중간 컴포넌트를 통과합니다.
컨텍스트를 제공하는 컴포넌트와 이를 사용하는 컴포넌트 사이에 원하는 만큼의 컴포넌트를 삽입할 수 있습니다. 여기에는 <div>와 같은 기본 제공 컴포넌트와 사용자가 직접 작성할 수 있는 컴포넌트가 모두 포함됩니다.
이 예시에서는 동일한 Post 컴포넌트(점선 테두리 포함)가 두 개의 서로 다른 중첩 레벨에서 렌더링됩니다. 그 안의 <Heading>이 가장 가까운 <Section>에서 자동으로 레벨을 가져오는 것을 볼 수 있습니다:
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 은 그 안에 있는 트리의 컨텍스트를 지정하므로 <Heading>을 아무 곳에나 삽입할 수 있으며 올바른 크기를 갖습니다. 위의 샌드박스에서 사용해 보세요!
컨텍스트를 사용하면 "주변 환경에 적응"하고 렌더링되는 위치(즉, 어떤 컨텍스트)에 따라 다르게 표시되는 컴포넌트를 작성할 수 있습니다.
컨텍스트가 작동하는 방식은 CSS 속성 상속을 떠올리게 할 수 있습니다. CSS에서는 <div>에 color:blue 를 지정할 수 있으며, 그 안에 있는 모든 DOM 노드는 중간에 있는 다른 DOM 노드가 color:green 으로 재정의하지 않는 한 아무리 깊어도 해당 색상을 상속받습니다. CSS에서는 색상 및 배경색과 같은 서로 다른 속성이 서로 재정의되지 않습니다. 배경색에 영향을 주지 않고 모든 <div>의 색상을 빨간색으로 설정할 수 있습니다. 마찬가지로, 서로 다른 React 컨텍스트는 서로 재정의되지 않습니다. createContext()로 만드는 각 컨텍스트는 다른 컨텍스트와 완전히 분리되어 있으며, 특정 컨텍스트를 사용하고 제공하는 컴포넌트를 함께 묶어줍니다. 하나의 컴포넌트가 다양한 컨텍스트를 문제 없이 사용하거나 제공할 수 있습니다.
컨텍스트를 사용하기 전에
컨텍스트는 사용하기 매우 유혹적입니다! 그러나 이는 또한 너무 쉽게 남용될 수 있다는 의미이기도 합니다. props를 몇 단계 깊이 전달해야 한다고 해서 해당 정보를 문맥에 넣어야 한다는 의미는 아닙니다.
다음은 문맥을 사용하기 전에 고려해야 할 몇 가지 대안입니다:
- props 전달부터 시작하세요. 컴포넌트가 사소하지 않다면 수십 개의 컴포넌트를 통해 수십 개의 props를 전달해야 하는 경우도 드물지 않습니다. 느리게 느껴질 수도 있지만 어떤 컴포넌트가 어떤 데이터를 사용하는지 매우 명확해집니다! 코드를 유지 관리하는 사람은 props를 사용하여 데이터 흐름을 명확하게 만든 것에 만족할 것입니다.
- 컴포넌트를 추출하고 JSX를 자식으로 전달하세요. 일부 데이터를 해당 데이터를 사용하지 않는 중간 컴포넌트의 여러 계층을 거쳐 전달한다면(그리고 더 아래로만 전달한다면), 이는 종종 그 과정에서 일부 컴포넌트를 추출하는 것을 잊어버렸다는 것을 의미합니다. 예를 들어, 게시물과 같은 데이터 props를 직접 사용하지 않는 시각적 컴포넌트에 <Layout posts={posts} /> 처럼 사용하는것 대신, 레이아웃이 자식을 prop으로 사용하도록 하고 <Layout><Posts posts={posts} /></Layout>을 렌더링합니다. 이렇게 하면 데이터를 지정하는 컴포넌트와 데이터를 필요로 하는 컴포넌트 사이의 레이어 수가 줄어듭니다.
이 두 가지 접근 방식이 모두 적합하지 않은 경우 컨텍스트를 고려하세요.
컨텍스트 사용 사례
- 테마: 앱에서 사용자가 앱의 모양을 변경할 수 있는 경우(예: 다크 모드) 앱 상단에 컨텍스트 제공업체를 배치하고 시각적 모양을 조정해야 하는 컴포넌트에서 해당 컨텍스트를 사용할 수 있습니다.
- 현재 계정: 많은 컴포넌트가 현재 로그인한 사용자를 알아야 할 수 있습니다. 컨텍스트에 넣으면 트리의 어느 곳에서나 편리하게 읽을 수 있습니다. 또한 일부 앱에서는 여러 계정을 동시에 조작할 수 있습니다(예: 다른 사용자로 댓글을 남기는 경우). 이러한 경우 UI의 일부를 다른 현재 계정 값으로 중첩된 공급자로 래핑하는 것이 편리할 수 있습니다.
- 라우팅: 대부분의 라우팅 솔루션은 내부적으로 컨텍스트를 사용하여 현재 경로를 유지합니다. 이것이 모든 링크가 활성 상태인지 아닌지를 "아는" 방식입니다. 자체 라우터를 구축하는 경우에도 이 방식을 사용할 수 있습니다.
- 상태 관리: 앱이 성장함에 따라 앱 상단에 더 많은 상태가 있을 수 있습니다. 아래에 있는 많은 멀리 떨어진 컴포넌트에서 이를 변경하고 싶을 수 있습니다. 컨텍스트와 함께 리듀서를 사용하면 복잡한 상태를 관리하고 멀리 떨어진 컴포넌트에 큰 번거로움 없이 전달할 수 있습니다.
컨텍스트는 정적 값에만 국한되지 않습니다. 다음 렌더링에서 다른 값을 전달하면 React는 아래에서 이를 읽는 모든 컴포넌트를 업데이트합니다! 이것이 컨텍스트가 state와 함께 자주 사용되는 이유입니다.
일반적으로 트리의 다른 부분에 있는 멀리 떨어진 컴포넌트에서 일부 정보가 필요한 경우 컨텍스트가 도움이 될 수 있다는 좋은 신호입니다.
요약
컨텍스트는 컴포넌트가 그 아래 전체 트리에 일부 정보를 제공할 수 있게 해줍니다. 컨텍스트를 전달하려면:
- export를 사용하여 생성하고 내보냅니다. const MyContext = createContext(defaultValue).
이를 useContext(MyContext) Hook에 전달하면 깊이에 상관없이 모든 자식 컴포넌트에서 읽을 수 있습니다. - 부모에서 제공하려면 자식을 <MyContext.Provider value={...}>로 래핑합니다.
- 컨텍스트는 중간에 있는 모든 컴포넌트를 통과합니다.
- 컨텍스트를 사용하면 "주변 환경에 적응"하는 컴포넌트를 작성할 수 있습니다.
- 컨텍스트를 사용하기 전에 소품을 전달하거나 JSX를 자식으로 전달해 보세요.
과제 1 / 1: props 드릴링을 컨텍스트로 바꾸기
이 예제에서 확인란을 토글하면 각 <PlaceImage>에 전달된 imageSize prop이 변경됩니다. 체크박스 상태는 최상위 App 컴포넌트에 유지되지만 각 <PlaceImage>가 이를 인식해야 합니다.
현재 App은 이미지 크기를 List로 전달하고, List는 이를 각 장소로 전달하며, 장소는 이를 PlaceImage로 전달합니다. 이미지사이즈 props를 제거하고 대신 App 컴포넌트에서 PlaceImage로 직접 전달하세요.
컨텍스트는 Context.js에서 선언할 수 있습니다.
https://codesandbox.io/s/replace-prop-drilling-with-context-yne7c1?file=/App.js
발표요약)
<MyContext.Provider value={...}>로 부모에서 래핑
1. props가 깊어지면 컨텍스트를 쓰는것도 방법이다. 그러나 최선은 아니다.
(요즘엔 또 굳이 안쓰는 추세. 렌더 영역이 방대해져서 리코일같은 상태관리 씀)
2. 컨텍스트 사이에있는 컴포넌트 모두 통과한다. 프로바이더 안에 있는 컴포넌트 어디든 사용할수있다.