Context: 데이터 깊이 전달
- Published on
- 이 글은 리액트 컨텍스트를 사용하여 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하는 방법에 대한 내용을 다루고 있습니다.
Table of Contents
일반적으로 부모 컴포넌트에서 자식 컴포넌트로 정보를 전달할 때는 props를 사용합니다. 그러나 많은 컴포넌트를 거쳐서 props를 전달해야 하거나 앱 내에서 많은 컴포넌트가 동일한 정보를 필요로 할 때는 props 전달이 장황하고 불편해질 수 있습니다. 컨텍스트를 사용하면 부모 컴포넌트가 아래 트리의 모든 컴포넌트에게 일부 정보를 제공할 수 있으며, 명시적으로 props를 전달하지 않아도 됩니다.
배울 내용
- "prop drilling"이란 무엇인지
- 반복되는 prop 전달을 컨텍스트로 대체하는 방법
- 컨텍스트의 일반적인 사용 사례
- 컨텍스트의 대체재
props 전달의 문제점
props 전달은 데이터를 UI 트리를 통해 명시적으로 파이프로 전달하여 해당 데이터를 사용하는 컴포넌트에 전달하는 좋은 방법입니다.
그러나 props 전달은 데이터를 트리를 따라 깊게 전달해야 하는 경우나 많은 컴포넌트가 동일한 props를 필요로 하는 경우에 장황하고 불편해질 수 있습니다. 가장 가까운 공통 조상은 데이터가 필요한 컴포넌트와 상당히 멀리 떨어져 있을 수 있으며, 해당 높이까지 상태를 "끌어올리는" 것은 "prop drilling"이라는 상황을 초래할 수 있습니다.
상태를 상위로 끌어올리기
Prop 드릴링
props를 전달하지 않고도 데이터를 필요로 하는 컴포넌트로 데이터를 "전송"할 수 있는 방법이 있으면 좋지 않을까요? 리액트의 컨텍스트 기능을 사용하면 이를 할 수 있습니다!
컨텍스트: props 전달의 대체재
컨텍스트를 사용하면 부모 컴포넌트가 해당 트리 아래의 전체 트리에 데이터를 제공할 수 있습니다. 컨텍스트에는 여러 가지 사용 사례가 있습니다. 여기에 한 가지 예가 있습니다. 크기를 위한 레벨을 인수로 받는 이 Heading 컴포넌트를 고려해보세요:
동일한 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만으로는 할 수 없습니다. 여기서 컨텍스트가 필요해집니다. 다음 세 가지 단계를 거쳐 수행할 것입니다.
- 컨텍스트를 생성합니다. (Heading의 크기를 나타내는 컨텍스트이므로 LevelContext라고 부르겠습니다.)
- 데이터를 필요로 하는 컴포넌트에서 해당 컨텍스트를 사용합니다. (Heading은 LevelContext를 사용할 것입니다.)
- 데이터를 지정하는 컴포넌트에서 해당 컨텍스트를 제공합니다. (Section은 LevelContext를 제공할 것입니다.)
컨텍스트를 사용하면 부모 - 심지어 먼 곳에 위치한 부모 - 가 해당 트리 안의 모든 컴포넌트에 일부 데이터를 제공할 수 있습니다.
가까운 자식에서 컨텍스트 사용하기
먼 자식에서 컨텍스트 사용하기
단계 1: 컨텍스트 생성
먼저 컨텍스트를 생성해야 합니다. 컴포넌트에서 사용할 수 있도록 컨텍스트를 파일에서 내보내야 합니다:
createContext의 유일한 인수는 기본 값입니다. 여기에서는 1이 가장 큰 제목 레벨을 나타내지만 어떤 종류의 값(객체조차도)을 전달할 수 있습니다. 다음 단계에서 기본 값의 의미를 알게될 것입니다.
단계 2: 컨텍스트 사용
리액트에서 useContext Hook과 컨텍스트를 가져옵니다.
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는 Hook입니다. useState와 useReducer와 마찬가지로 Hook은 리액트 컴포넌트 내에서만 즉시 호출할 수 있습니다(루프나 조건문 내에서는 호출할 수 없습니다). useContext는 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>
대신, JSX를 업데이트하여 Section이 level prop을 전달받도록 변경합니다.
<Section level={4}>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>
알림: 이 예제는 중첩된 컴포넌트가 컨텍스트를 재정의하는 방식을 시각적으로 보여주기 위해 제목 레벨을 사용합니다. 그러나 컨텍스트는 시각적인 표시 외에도 많은 다른 사용 사례에 유용합니다. 전체 하위 트리가 필요로하는 모든 정보를 전달할 수 있습니다. 예를 들어, 현재의 색상 테마, 현재 로그인한 사용자 등을 전달할 수 있습니다.
단계 3: 컨텍스트 제공
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을 전달할 필요가 없었습니다! 대신, Heading은 그 위에 있는 가장 가까운 Section에게 레벨을 "알아내기" 위해 요청했습니다.
- level prop을
<Section>
에 전달합니다. - Section은 자식을
<LevelContext.Provider value={level}>
로 감쌉니다. - Heading은
useContext(LevelContext)
를 사용하여 위로부터 가장 가까운 LevelContext 값을 요청합니다.
동일한 컴포넌트에서 컨텍스트 사용과 제공
현재는 각 섹션의 레벨을 수동으로 지정해야 합니다.
export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...
컨텍스트를 사용하면 각 Section이 이전 Section에서 레벨을 읽고 레벨 + 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>
);
}
이 변경 사항으로 <Section>
또는 <Heading>
에 level prop을 전달할 필요가 없어졌습니다.
이제 Heading과 Section은 자신의 주변 상황에 따라 어떻게 표시되는지 조정하기 위해 LevelContext를 읽습니다. 그리고 Section은 자식을 LevelContext로 감싸 어떤 것이 "더 깊은" 레벨인지를 지정합니다.
참고
이 예제에서는 중첩된 컴포넌트가 컨텍스트를 재정의하는 방식을 시각적으로 보여주기 위해 제목 레벨을 사용합니다. 그러나 컨텍스트는 테마 지정, 현재 로그인한 사용자 등 많은 다른 사용 사례에 유용합니다. 전체 트리의 다른 부분에서 필요로 하는 정보가 있는 경우 컨텍스트가 도움이 될 것입니다.
컨텍스트는 중간 컴포넌트를 통과합니다
컨텍스트를 제공하는 컴포넌트와 사용하는 컴포넌트 사이에 원하는 만큼 많은 컴포넌트를 삽입할 수 있습니다. 이에는 <div>
와 같은 내장 컴포넌트와 직접 작성한 컴포넌트 모두 포함됩니다.
이 예에서는 동일한 Post 컴포넌트(점선 테두리)가 서로 다른 중첩 레벨에서 렌더링됩니다. 그 내부의 <Heading>
은 가장 가까운 <Section>
에서 자동으로 레벨을 얻습니다.
이것을 작동시키기 위해 특별히 할 것은 없습니다. Section은 내부 트리에 대한 컨텍스트를 지정하므로 <Heading>
을 어디에든 삽입할 수 있고 올바른 크기를 가집니다. 상단의 샌드박스에서 확인해보세요!
컨텍스트를 사용하면 컴포넌트를 "주변에 맞게 조정"하여 렌더링되는 위치(다시 말해, 어떤 컨텍스트에서 렌더링되는지)에 따라 다르게 표시할 수 있습니다.
컨텍스트가 동작 방식이 CSS 속성 상속을 연상시킬 수 있습니다. CSS에서는 <div>
에 color: blue
를 지정하면 얼마든지 깊게 위치한 DOM 노드도 다른 DOM 노드가 중간에 color: green
으로 재정의하지 않는 한 해당 색상을 상속합니다. 마찬가지로 리액트에서도 위에서 내려오는 컨텍스트를 재정의하는 유일한 방법은 다른 값을 가진 컨텍스트 제공자로 자식을 감싸는 것입니다.
CSS에서 색상(color)과 배경색(background-color)과 같은 다른 속성은 서로를 덮어쓰지 않습니다. 모든 <div>
의 색상을 red로 설정하면 배경색에는 영향을 주지 않습니다. 마찬가지로 다른 리액트 컨텍스트도 서로를 덮어쓰지 않습니다. createContext()
로 만든 각 컨텍스트는 완전히 다른 것으로 간주되며 해당 컨텍스트를 사용하거나 제공하는 컴포넌트를 연결시킵니다. 한 컴포넌트가 문제없이 여러 다른 컨텍스트를 사용하거나 제공할 수 있습니다.
컨텍스트를 사용하기 전에
컨텍스트는 사용하면 매우 유혹적입니다! 그러나 이것은 동시에 너무 쉽게 과용할 수 있는 것을 의미합니다. 일부 props를 여러 수준으로 전달해야 한다고 해서 그 정보를 컨텍스트에 넣어야 하는 것은 아닙니다.
컨텍스트를 사용하기 전에 고려해야 할 몇 가지 대안이 있습니다.
- 우선은 props를 전달해 보세요. 컴포넌트가 간단하지 않은 경우 수십 개의 props를 수십 개의 컴포넌트를 통해 전달하는 것은 보통입니다. 번거롭게 느껴질 수 있지만, 어떤 데이터를 사용하는지를 명확히 표시하여 코드를 유지보수하는 사람에게 큰 도움이 될 것입니다. 3. children을
<MyContext.Provider value={...}>
로 감싸서 상위에서 제공합니다. 3. children을<MyContext.Provider value={...}>
로 감싸서 상위에서 제공합니다. - 컴포넌트를 추출하고 JSX를 자식으로 전달하세요. 데이터를 사용하지 않는 중간 컴포넌트의 많은 수준을 통해 데이터를 전달하는 경우, 그 중간에 컴포넌트를 추출하는 것을 잊은 경우가 많습니다. 예를 들어,
<Layout posts={posts} />
와 같이 posts와 같은 데이터 props를 직접 사용하지 않는 시각적인 컴포넌트에 데이터 props를 전달할 수 있습니다. 대신, Layout이 children을 하나의 prop으로 받도록 만들고,<Layout><Posts posts={posts} /></Layout>
를 렌더링합니다. 이렇게 하면 데이터를 지정하는 컴포넌트와 해당 데이터가 필요한 컴포넌트 간의 계층 수가 줄어듭니다.
위 두 가지 방법이 잘 작동하지 않는 경우 컨텍스트를 고려하세요.
컨텍스트의 사용 사례
- 테마 지정: 사용자가 앱의 외관을 변경할 수 있는 경우(예: 다크 모드), 앱의 맨 위에 컨텍스트 제공자를 놓고 시각적인 모양을 조정해야 하는 컴포넌트에서 해당 컨텍스트를 사용할 수 있습니다.
- 현재 계정: 많은 컴포넌트가 현재 로그인한 사용자를 알아야 할 수도 있습니다. 컨텍스트에 넣어 어디에서든 해당 정보를 편리하게 읽을 수 있습니다. 일부 앱은 동시에 여러 계정(예: 다른 사용자로 댓글 남기기)을 사용할 수도 있습니다. 이러한 경우 일부 UI를 다른 현재 계정 값으로 감싸는 중첩된 프로바이더를 만드는 것이 편리할 수 있습니다.
- 라우팅: 대부분의 라우팅 솔루션은 현재 경로를 저장하기 위해 내부적으로 컨텍스트를 사용합니다. 이것은 링크가 활성화되었는지 여부를 알기 위해 필요합니다. 직접 라우터를 작성하는 경우 이와 같이 사용할 수 있습니다.
- 상태 관리: 앱이 커지면 상태가 앱의 상위에 가까이 있게 될 수 있습니다. 그 아래에 있는 많은 먼 컴포넌트가 상태를 변경해야 할 수도 있습니다. 복잡한 상태를 관리하고 너무 많은 문제 없이 먼 컴포넌트에 전달하기 위해 일반적으로 리듀서와 컨텍스트를 함께 사용합니다.
컨텍스트는 정적인 값에만 제한되지 않습니다. 다음 렌더링에 다른 값을 전달하면 리액트가 아래에서 해당 값을 읽는 모든 컴포넌트를 업데이트합니다. 이러한 이유로 컨텍스트는 종종 상태와 결합하여 사용됩니다.
일반적으로 트리의 여러 부분에서 필요로 하는 정보가 있다면 컨텍스트가 도움이 될 수 있습니다.
요약
- 컨텍스트를 사용하면 부모 컴포넌트가 해당 트리 전체에 일부 정보를 제공할 수 있습니다.
- 컨텍스트를 전달하려면:
export const MyContext = createContext(defaultValue)
와 같이 생성하고 내보냅니다.useContext(MyContext)
Hook을 사용하여 자식 컴포넌트에서 읽습니다(깊이에 상관없이).- children을
<MyContext.Provider value={...}>
로 감싸서 상위에서 제공합니다.
- 컨텍스트는 중간 컴포넌트를 통과합니다.
- 컨텍스트를 사용하여 "주변에 맞게 조정"되는 컴포넌트를 작성할 수 있습니다.
- 컨텍스트를 사용하기 전에 props를 전달하거나 JSX를 children으로 전달하는 것을 시도해보세요.