본문 바로가기
React/Documentation

리액트 공식 문서 번역-21. Context를 사용해 깊게 데이터 전달하기

by 똘이토스 2023. 7. 16.

보통 부모 컴포넌트에서 자식 컴포넌트로 정보를 props를 통해 전달합니다. 하지만 중간에 많은 컴포넌트를 통해 데이터를 전달해야 할 때나 앱 내의 많은 컴포넌트가 동일한 정보를 필요로 할 때 props를 전달하는 것이 번거롭고 불편해집니다. Context는 부모 컴포넌트가 트리 하위의 모든 컴포넌트에게 명시적으로 props를 통해 전달하지 않고도 일부 정보를 사용할 수 있게 합니다.

 

Props 전달의 문제

Props를 전달하는 것은 데이터를 사용하는 컴포넌트에 UI 트리를 명시적으로 연결하는 좋은 방법입니다.

 

그러나 트리를 깊게 전달해야 하거나 많은 컴포넌트가 같은 props가 필요한 경우, props 전달이 번거롭고 불편해질 수 있습니다. 가장 가까운 공통 조상은 데이터가 필요한 컴포넌트로부터 멀리 떨어져 있을 수 있으며, 상태를 그만큼 끌어올리는 것은 "prop 드릴링"이라는 상황을 초래할 수 있습니다.

 

 

Props를 전달하지 않고 트리의 컴포넌트에 데이터를 "전달"하는 방법이 있다면 얼마나 좋을까요? React의 context 기능을 사용하면 가능합니다!

 

Context: props 전달의 대안

Context는 부모 컴포넌트가 그 하위 트리 전체에 데이터를 제공할 수 있도록 합니다. Context에는 여러 가지 용도가 있습니다. 예를 들어, 크기에 대한 level을 받아들이는 Heading 컴포넌트를 고려해 보세요

 

https://codesandbox.io/s/l5glye?file=/App.js&utm_medium=sandpack

 

polished-currying-l5glye - CodeSandbox

polished-currying-l5glye using react, react-dom, react-scripts

codesandbox.io

 

같은 Section 내의 여러 Heading이 항상 같은 크기를 가지도록 하고 싶다고 가정해 봅시다

 

https://codesandbox.io/s/36blul?file=/App.js&utm_medium=sandpack

 

cocky-roentgen-36blul - CodeSandbox

cocky-roentgen-36blul using react, react-dom, react-scripts

codesandbox.io

 

현재는 각 <Heading>level prop을 별도로 전달합니다

 

<Section>
  <Heading level={3}>About</Heading>
  <Heading level={3}>Photos</Heading>
  <Heading level={3}>Videos</Heading>
</Section>

 

<Section> 컴포넌트에 level prop을 전달하고 <Heading>에서 제거할 수 있다면 좋겠습니다. 이렇게 하면 같은 섹션 내의 모든 헤딩이 같은 크기를 갖도록 할 수 있습니다

 

<Section level={3}>
  <Heading>About</Heading>
  <Heading>Photos</Heading>
  <Heading>Videos</Heading>
</Section>

 

그러나 <Heading> 컴포넌트는 가장 가까운 <Section>의 레벨을 어떻게 알 수 있을까요? 이렇게 하려면 트리의 상위에서 어떤 데이터를 "요청"하는 방법이 필요합니다.

 

단순히 props만으로는 이를 수행할 수 없습니다. 이 때 Context가 등장합니다. 이를 세 단계로 수행할 수 있습니다

 

  1. 컨텍스트를 생성합니다. (헤딩 레벨을 위한 것이므로 LevelContext라고 부릅니다.)
  2. 데이터가 필요한 컴포넌트에서 해당 컨텍스트를 사용합니다. (HeadingLevelContext를 사용합니다.)
  3. 데이터를 지정하는 컴포넌트에서 해당 컨텍스트를 제공합니다. (SectionLevelContext를 제공합니다.)

 

Context를 사용하면 부모 컴포넌트(먼 것도 가능!)가 그 안의 전체 트리에 데이터를 제공할 수 있습니다.

 

Step 1: 컨텍스트 생성

먼저, 컨텍스트를 생성해야 합니다. 컴포넌트에서 사용할 수 있도록 파일에서 내보내야 합니다

 

https://codesandbox.io/s/0ewru9?file=/LevelContext.js&utm_medium=sandpack

 

adoring-wing-0ewru9 - CodeSandbox

adoring-wing-0ewru9 using react, react-dom, react-scripts

codesandbox.io

 

createContext의 유일한 인수는 기본값입니다. 여기서 1은 가장 큰 헤딩 레벨을 나타내지만, 어떤 종류의 값(심지어 객체)도 전달할 수 있습니다. 기본값의 중요성은 다음 단계에서 확인할 수 있습니다.

 

Step 2: 컨텍스트 사용

React에서 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입니다. useStateuseReducer와 마찬가지로, React 컴포넌트 내에서만 바로 Hook을 호출할 수 있습니다(루프나 조건문 안에선 사용할 수 없음). useContextHeading 컴포넌트가 LevelContext를 읽고 싶어한다는 것을 React에 알려줍니다.

 

이제 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>

 

참고로, 작동하도록 만들려고 했던 마크업은 다음과 같습니다

 

https://codesandbox.io/s/jydicj?file=/App.js&utm_medium=sandpack

 

clever-wright-jydicj - CodeSandbox

clever-wright-jydicj using react, react-dom, react-scripts

codesandbox.io

 

이 예제는 아직 완전히 작동하지 않습니다! 모든 헤딩의 크기가 같은데, 이는 컨텍스트를 사용하고 있지만 아직 제공하지 않았기 때문입니다. React는 어디서 가져와야 할지 모릅니다!

 

컨텍스트를 제공하지 않으면 React는 이전 단계에서 지정한 기본값을 사용합니다. 이 예제에서는 createContext에 인수로 1을 지정했으므로 useContext(LevelContext)1을 반환하고, 모든 헤딩을 <h1>로 설정합니다. 각 Section이 자체 컨텍스트를 제공하도록 이 문제를 해결해봅시다.

 

Step 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>
  );
}

 

이것은 React에게 이렇게 말합니다: "이 <Section> 안의 어떤 컴포넌트가 LevelContext를 요청하면 이 level을 제공해라." 컴포넌트는 UI 트리에서 자신 위에 있는 가장 가까운 <LevelContext.Provider>의 값을 사용합니다.

 

https://codesandbox.io/s/ehit2d?file=/App.js&utm_medium=sandpack

 

dark-microservice-ehit2d - CodeSandbox

dark-microservice-ehit2d using react, react-dom, react-scripts

codesandbox.io

 

이것은 원래 코드와 같은 결과지만, 각 Heading 컴포넌트에게 level prop을 전달할 필요가 없습니다! 대신 가장 가까운 상위 Section에게 물어봄으로써 헤딩 레벨을 "알아낼" 수 있습니다

 

  1. <Section>에 level prop을 전달합니다.
  2. Section은 자식들을 <LevelContext.Provider value={level}>로 감쌉니다.
  3. Heading은 useContext(LevelContext)를 사용하여 가장 가까운 상위의 LevelContext 값을 요청합니다.

 

같은 컴포넌트에서 컨텍스트 사용 및 제공

현재, 각 섹션의 level을 수동으로 지정해야 합니다

 

export default function Page() {
  return (
    <Section level={1}>
      ...
      <Section level={2}>
        ...
        <Section level={3}>
          ...

 

컨텍스트가 상위 컴포넌트에서 정보를 읽을 수 있게 하므로, 각 섹션은 상위 섹션에서 레벨을 읽고 자동으로 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>
  );
}

 

이 변경으로 인해 <Section>이나 <Heading>에 level prop을 전달할 필요가 없습니다

 

https://codesandbox.io/s/sbhymf?file=/App.js&utm_medium=sandpack

 

serene-albattani-sbhymf - CodeSandbox

serene-albattani-sbhymf using react, react-dom, react-scripts

codesandbox.io

 

이제 HeadingSection 모두 LevelContext를 읽어서 얼마나 "깊게" 있는지 파악합니다. 그리고 Section은 내부의 모든 것이 "더 깊은" 레벨에 있다고 지정하기 위해 자식들을 LevelContext로 감쌉니다.

💡 이 예시는 중첩된 컴포넌트가 컨텍스트를 어떻게 오버라이드할 수 있는지 시각적으로 보여주기 위해 헤딩 레벨을 사용합니다. 그러나 컨텍스트는 많은 다른 사용 사례에도 유용합니다. 전체 하위 트리에 필요한 정보를 내려보낼 수 있습니다: 현재 색상 테마, 현재 로그인한 사용자 등.

 

컨텍스트가 중간 컴포넌트를 통과합니다

컨텍스트를 제공하는 컴포넌트와 사용하는 컴포넌트 사이에 원하는 만큼의 컴포넌트를 삽입할 수 있습니다. 이에는 <div>와 같은 내장 컴포넌트와 직접 만든 컴포넌트 모두 포함됩니다.

 

이 예시에서는 같은 Post 컴포넌트(점선 테두리)가 두 개의 다른 중첩 레벨에서 렌더링됩니다. 그 안에 있는 <Heading>이 가장 가까운 <Section>으로부터 자동으로 레벨을 얻는 것에 주목하세요

 

https://codesandbox.io/s/8t0nb9?file=/App.js&utm_medium=sandpack

 

throbbing-violet-8t0nb9 - CodeSandbox

throbbing-violet-8t0nb9 using react, react-dom, react-scripts

codesandbox.io

 

작동하도록 특별한 작업을 수행하지 않았습니다. Section은 그 안의 트리에 대한 컨텍스트를 지정하므로 어디에서나 <Heading>을 삽입하면 올바른 크기가 됩니다. 위의 샌드박스에서 시도해보세요!

 

컨텍스트를 사용하면 컴포넌트가 "주변 환경에 적응하여" 자신을 렌더링하는 위치(즉, 어떤 컨텍스트에서)에 따라 다르게 표시할 수 있습니다.

 

컨텍스트 작동 방식은 CSS 속성 상속을 연상시킬 수 있습니다. CSS에서 <div>color: blue를 지정하면, 그 안의 어떤 DOM 노드든 상관없이 그 색상을 상속받게 되며, 중간에 다른 DOM 노드가 color: green으로 오버라이드하지 않는 한 그대로 유지됩니다. 마찬가지로 React에서는 상위에서 전달되는 컨텍스트를 오버라이드하는 유일한 방법은 다른 값으로 컨텍스트 프로바이더로 자식을 감싸는 것입니다.

 

CSS에서는 colorbackground-color와 같은 다른 속성이 서로 오버라이드하지 않습니다. 모든 <div>의 색상을 빨간색으로 설정해도 background-color에 영향을 주지 않습니다. 마찬가지로 다른 React 컨텍스트도 서로 오버라이드하지 않습니다. createContext()로 만든 각 컨텍스트는 다른 것들과 완전히 분리되어 있으며, 특정 컨텍스트를 사용하고 제공하는 컴포넌트를 묶어줍니다. 하나의 컴포넌트는 여러 다른 컨텍스트를 사용하거나 제공할 수 있고 문제가 없습니다.

 

컨텍스트를 사용하기 전에

컨텍스트는 사용하기 매우 매력적입니다! 그러나 이는 컨텍스트를 과도하게 사용하기 쉽다는 것을 의미합니다. 몇 단계 깊이의 프로퍼티를 전달해야 한다고 해서 꼭 컨텍스트에 그 정보를 넣어야 하는 것은 아닙니다.

 

컨텍스트를 사용하기 전에 고려해야 할 몇 가지 대안이 있습니다

 

  1. 프로퍼티를 전달하는 것부터 시작하세요. 컴포넌트가 복잡하지 않다면, 십여 개의 프로퍼티를 십여 개의 컴포넌트를 통해 전달하는 것이 흔합니다. 지루해 보일 수 있지만, 어떤 컴포넌트가 어떤 데이터를 사용하는지 명확하게 보여줍니다! 코드를 관리하는 사람은 프로퍼티를 사용하여 데이터 흐름을 명시적으로 만들어 준 것에 감사할 것입니다.
  2. 컴포넌트를 추출하고 JSX를 그들의 자식으로 전달하세요. 많은 중간 컴포넌트 계층을 통해 데이터를 전달하고, 그 데이터를 사용하지 않고 (그저 더 아래로 전달하는 것만) 한다면, 이것은 종종 중간에 컴포넌트를 추출하는 것을 잊은 것을 의미합니다. 예를 들어, <Layout posts={posts} />와 같은 직접적으로 사용하지 않는 시각적 컴포넌트에 데이터 프로퍼티(게시물 등)를 전달할 수 있습니다. 대신에, Layoutchildren을 프로퍼티로 가져가게 하고 <Layout><Posts posts={posts} /></Layout>를 렌더링하세요. 이렇게 하면 데이터를 지정하는 컴포넌트와 그것이 필요한 컴포넌트 사이의 계층 수를 줄일 수 있습니다.

 

이러한 접근 방식이 여러분에게 잘 작동하지 않는다면, 컨텍스트를 고려해보세요.

 

컨텍스트 사용 사례

  • 테마: 앱이 사용자에게 외관을 변경할 수 있게 하려면(예: 다크 모드), 앱 상단에 컨텍스트 프로바이더를 배치하고 컴포넌트에서 시각적 모습을 조절해야 하는 컨텍스트를 사용하세요.
  • 현재 계정: 많은 컴포넌트가 현재 로그인한 사용자를 알아야 할 수 있습니다. 컨텍스트에 넣어 트리의 어디에서나 편리하게 읽을 수 있게 합니다. 일부 앱은 동시에 여러 계정을 사용할 수 있게 하기도 합니다(예: 다른 사용자로 댓글을 남기기). 이러한 경우, 다른 현재 계정 값으로 중첩된 프로바이더로 UI의 일부를 감싸는 것이 편리할 수 있습니다.
  • 라우팅: 대부분의 라우팅 솔루션은 내부적으로 컨텍스트를 사용하여 현재 루트를 유지합니다. 이렇게 하면 모든 링크가 활성화되었는지 아닌지를 알 수 있습니다. 직접 라우터를 만들 경우에도 이렇게 할 수 있습니다.
  • 상태 관리: 앱이 성장하면서 앱 상단에 가까운 많은 상태가 생길 수 있습니다. 아래쪽에 있는 많은 먼 컴포넌트가 그것을 변경하려 할 수 있습니다. 컨텍스트와 함께 리듀서를 사용하여 복잡한 상태를 관리하고, 너무 번거롭지 않게 먼 컴포넌트로 전달하는 것이 일반적입니다.

 

컨텍스트는 정적 값에만 국한되지 않습니다. 다음 렌더링에서 다른 값을 전달하면 React는 아래쪽에서 해당 값을 읽는 모든 컴포넌트를 업데이트합니다! 이 때문에 컨텍스트는 종종 상태와 함께 사용됩니다.

 

일반적으로 트리의 서로 다른 부분에 있는 먼 컴포넌트에게 필요한 정보가 있다면, 컨텍스트가 도움이 될 좋은 지표입니다.

 

요약

  • 컨텍스트는 컴포넌트가 그 아래 전체 트리에 어떤 정보를 제공할 수 있게 합니다.
  • 컨텍스트를 전달하려면:
    1. export const MyContext = createContext(defaultValue)를 사용하여 생성하고 내보냅니다.
    2. useContext(MyContext) 훅을 사용하여 깊이에 상관없이 하위 컴포넌트에서 읽을 수 있도록 전달합니다.
    3. <MyContext.Provider value={...}>를 사용하여 부모로부터 제공되도록 자식 컴포넌트를 감싸줍니다.
  • 컨텍스트는 중간에 있는 모든 컴포넌트를 통과합니다.
  • 컨텍스트는 컴포넌트가 "주변 환경에 적응"하도록 작성할 수 있게 합니다.
  • 컨텍스트를 사용하기 전에, 프로퍼티를 전달하거나 JSX를 자식으로 전달하는 것을 시도해보세요.

 

2023.07.16 - [React/Documentation] - 리액트 공식 문서 번역-22. 리듀서와 컨텍스트를 확장하기

 

리액트 공식 문서 번역-22. 리듀서와 컨텍스트를 확장하기

리듀서를 사용하면 컴포넌트의 상태 업데이트 로직을 통합할 수 있습니다. 컨텍스트를 사용하면 다른 컴포넌트에게 깊이 있는 정보를 전달할 수 있습니다. 리듀서와 컨텍스트를 함께 사용하여

ddor2.tistory.com