[리액트 공식문서 읽기] MANAGING STATE - Passing Data Deeply with Context

JaeHong Jeong·2023년 10월 23일
post-thumbnail

Overview

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

The problem with passing props

props 전달은 UI 트리를 통해 데이터를 사용하는 컴포넌트로 데이터를 명시적으로 파이프하는 좋은 방법이다.

그러나 트리 전체에 걸쳐 일부 prop을 전달해야 하거나 많은 컴포넌트에 동일한 prop 전달은 장황하고 불편해질 수 있다. 가장 가까운 공통 조상은 데이터가 필요한 컴포넌트에서 멀리 떨여저 있을 수 있으며, 상태를 그 높이로 끌어올리면 “prop drilling”이라는 상황이 발생할 수 있다.

props를 전달하지 않고도 데이터가 필요한 트리의 컴포넌트로 데이터를 “순간 이동”할 수 있는 방법이 있다면 좋지 않을까? 리액트의 컨텍스트 기능이 있다.

Context: an alternative to passing 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만으로는 할 수 없다. 이것이 바로 컨텍스트가 작용하는 곳이다. 이 작업은 세 단계로 수행된다.

  1. 컨텍스트 생성 (제목 수준에 대한 것이므로 LevelContext 라고 부를 수 있다.)
  2. 데이터가 필요한 컴포넌트의 해당 컨텍스트를 사용 (HeadingLevelContext 를 사용한다.)
  3. 데이터를 지정하는 컴포넌트에서 해당 컨텍스트를 제공 (Section 에서는 LevelContext 를 제공한다.)

컨텍스트를 사용하면 부모(멀리 떨어져 있는 부모라도)가 내부 트리 전체에 일부 데이터를 제공할 수 있다.

Step 1: Create the Context

먼저 컨텍스트를 만들어야 한다. 컴포넌트에서 사용할 수 있도록 파일에서 내보내야 한다.

// LevelContext.js

import { createContext } from 'react';

export const LevelContext = createContext(1);

createContext 에 대한 유일한 인수는 기본값이다. 여기서 1 은 가장 큰 제목 수준을 의미하지만 모든 종류의 값(객체 포함)을 전달할 수 있다. 다음 단계에서 기본값의 중요성을 확인하게 된다.

Step 2: Use the Context

리액트와 컨텍스트에서 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 는 훅이다. useStateuseReducer 와 마찬가지로 리액트 컴포넌트 내에서만 즉시 훅을 호출할 수 있다.(루프나 조건 내에서는 호출할 수 없다.) useContextHeading 컴포넌트가 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 이 고유한 컨텍스트를 제공하도록 하여 이 문제를 해결해 보겠다.

Step 3: Provide the Context

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 을 요청하여 제목 레벨을 “파악”해야한다.

  1. <Section>level prop을 전달한다.
  2. Section 의 하위 항목을 <LevelContext.Provider value={level}> 로 래핑한다.
  3. HeadinguseContext(LevelContext) 를 사용하여 위의 LevelContext 에 가장 가까운 값을 묻는다.

Using and providing context from the same component

현재는 여전히 각 섹션의 level 을 수동으로 지정해야 한다.

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

컨텍스트를 통해 위 컴포넌트의 정보를 읽을 수 있으므로 각 Section의 위 Sectionlevel 을 읽고 자동으로 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> 에 전달할 필요가 없다.

이제 HeadingSection 모두 LevelContext 를 읽어서 얼마나 "깊은”지 파악한다. 그리고 Section 은 내부의 모든 항목이 “더 깊은” 수준에 있음을 지정하기 위해 하위 항목을 LevelContext 로 래핑한다.

💡 Note

이 예에서는 중첩된 컴포넌트가 컨텍스트를 재정의하는 방법을 시각적으로 보여주기 때문에 제목 수준으로 사용한다. 그러나 컨텍스트는 다른 많은 사용 사례에도 유용하다. 현재 색상 테마, 현재 로그인한 사용자 등 전체 하위 트리에 필요한 모든 정보를 전달할 수 있다.

Context passes through intermediate components

컨텍스트를 제공하는 컴포넌트와 이를 사용하는 컴포넌트 사이에 원하는 만큼 컴포넌트를 삽입할 수 있다. 여기에는 <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에서는 colorbackground-color 과 같은 서로 다른 속성이 서로 재정의되지 않는다. background-color 에 영향을 주지 않고 모든 <div>color 를 빨간색으로 설정할 수 있다. 마찬가지로, 서로다른 리액트 컨텍스트는 서로를 재정의하지 않는다. createContext() 를 사용하여 만드는 각 컨텍스트는 다른 컨텍스트와 완전히 분리되어 있으며 특정 컨텍스트를 사용하고 제공하는 컴포넌트를 함께 연결합니다. 하나의 컴포넌트는 문제 없이 다양한 컨텍스트를 사용하거나 제공할 수 있다.

Before you use context

컨텍스트는 사용하기에 매우 유혹적이다. 그러나 이는 또한 남용하기가 너무 쉽다는 것을 의미한다. 몇 가지 수준의 세부 정보를 전달해야 한다고 해서 해당 정보를 컨텍스트에 포함해야 한다는 의미는 아니다.

컨텍스트를 사용하기 전에 고려해야할 몇 가지 대안은 다음과 같다.

  1. props를 전달하여 시작해라. 컴포넌트가 사소하지 않은 경우 12개의 컴포넌트를 통해 12개의 prop을 전달하는 것은 드문 일이 아니다. 진부하게 느껴질 수도 있지만 어떤 컴포넌트가 어떤 데이터를 사용하는지 매우 명확하게 알 수 있다. 코드를 유지 관리하는 사람은 prop을 사용하여 데이터 흐름을 명시적으로 만든 것을 기뻐할 것이다.
  2. 컴포넌트를 추출하고 JSX를 항목으로 전달한다. 해당 데이터를 사용하지 않고 더 아래로만 전달하는 중간 구성요소의 여러 테이러를 통해 일부 데이터를 전달하는 경우 이는 종종 도중에 일부 컴포넌트를 추출하는 것을 잊어버렸다는 의미한다. 예를 들어, <Layout posts={posts} /> 와 같이 직접 사용하지 않는 시각적 컴포넌트에 posts 와 같은 데이터 prop을 전달할 수 있다. 대신 Layoutchildren 을 prop으로 받아들이고 <Layout><Posts posts={posts} /></Layout> 을 렌더링하도록 만들어라. 이렇게 하면 데이터를 지정하는 컴포넌트와 해당 데이터가 필요한 컴포넌트 사이의 레이어 수가 줄어든다.

이러한 접근 방ㅂ식 중 어느 것도 적합하지 않은 경우 상황을 고려해라.

Use cases for context

  • 테마 설정: 앱에서 사용자가 모양을 변경할 수 있는 경우(예: 다크모드) 컨텍스트 제공자를 앱 상단에 배치하고 시각적 모양을 조정해야 하는 컴포넌트에서 해당 컨텍스트를 사용할 수 있다.
  • 현재 계정: 많은 컴포넌트에서 현재 로그인된 사용자를 알아야할 수 있다. 컨텍스트에 넣으면 트리의 어느 곳에서나 편리하게 읽을 수 있다. 일부 앱에서는 동시에 여러 계정을 운영할 수도 있다.(예: 다른 사용자로 댓글 남기기). 이러한 경우 UI의 일부를 다른 현재 계정 값을 사용하여 중첩된 공급자로 래핑하는 것이 편리할 수 있다.
  • 라우팅: 대부분의 라우팅 솔루션은 내부적으로 컨텍스트를 사용하여 현재 경로를 유지한다. 이것이 모든 링크가 활성화되어 있는지 여부를 “아는” 방법이다. 자신만의 라우터를 구축한다면 그렇게 하고 싶을 수 있다.
  • 상태 관리: 앱이 성장함에 따라 앱 상단에 더 많은 상태가 생길 수 있다. 아래에 있는 많은 컴포넌트가 이를 변경하려고 할 수 있다. 복잡한 상태를 관리하고 큰 번거로움 없이 먼 컴포넌트에 전달하기 위해 컨텍스트와 함께 리듀서를 사용하는 것이 일반적이다.

컨텍스트는 정적 값으로 제한되지 않는다. 다음 렌더링에서 다른 값을 전달하면 리액트는 아래에서 이를 읽는 모든 컴포넌트를 업데이트한다. 이것이 바로 컨텍스트가 종종 상태와 결합하여 사용되는 이유이다.

일반적으로 트리의 서로 다른 부분에 있는 멀리 있는 컴포넌트에 일부 정보가 필요한 경우 컨텍스트가 도움이 될 것이라는 좋은 표시다.

Recap

  • 컨텍스트를 사용하면 컴포넌트가 아래에 있는 전체 트리에 일부 정보를 제공할 수 있다
  • 컨텍스트를 전달하려면 다음을 수행해라.
    1. export const MyContext = createContext(defaultValue) 를 사용하여 만들고 내보낸다.
    2. 깊이에 상관없이 모든 하위 컴포넌트에서 읽으려면 useContext(MyContext) 훅을 전달해라.
    3. 하위 항목을 <MyContext.Provider value={...}> 로 묶어 상위 항목에서 제공한다.
  • 컨텍스트는 중간이 있는 모든 컴포넌트를 통과한다.
  • 컨텍스트를 사용하면 “주변 환경에 적응하는” 컴포넌트를 작성할 수 있다.
  • 컨텍스트를 사용하기 전에 props를 전달하거나 JSX를 하위 항목으로 전달해봐라.
profile
반갑습니다.

0개의 댓글