https://react.dev/learn/passing-data-deeply-with-context를 읽고 정리한다.

 

보통 부모 컴포넌트에서 자식 컴포넌트로 props를 통해 정보를 전달한다. 하지만 그 중간에 많은 컴포넌트를 거쳐야 하는 경우, 코드가 장황해지고 불편해진다.

Context를 사용하면 부모 컴포넌트가 props를 통해 명시적으로 전달하지 않고도 하위 트리의 모든 컴포넌트에서 일부 정보를 사용할 수 있도록 한다.

Props 전달할때의 문제 : Prop driling

tree를 통해 일부 prop을 깊게 전달해야하거나 많은 컴포넌트에 동일한 prop이 필요한 경우, 상태를 높이 올려서(lifting state up) 해결한다. 이 방법을 "prop driling"이라고 한다. 이럴 경우, 반복적으로 props를 전달해야 한다는 단점이 있다.

prop driling을 사용하면 prop을 실제로 사용하지 않는 중간 컴포넌트도 거쳐야한다.

Context : 반복적인 prop 전달을 대체하는 방법

context를 사용하면 상위 컴포넌트가 그 아래의 전체 트리에 데이터를 제공할 수 있다.

context를 사용하면 그 아래의 전체 트리에 데이터가 제공되므로, 실제로 prop이 필요한 곳에서만 사용한다.

여기서 사용되는 예는 다음과 같다. 이를 context를 사용해 개선해보자.

  • Section 내부에 여러 개의 Heading이 존재한다.
  • 동일한 Section 내에는 동일한 Level의 Heading만 존재할 수 있다.
  • 기존 코드는 Heading에서 prop으로 Level을 입력받고 있다.

 

1. context를 만든다.

context용 파일을 생성하고, 컴포넌트에서 사용할 수 있도록 export 한다.

  • createContext의 인수로 디폴트 값(개체 포함 모든 타입 가능)을 설정할 수 있다.
// LevelContext.js

import { createContext } from 'react';

export const LevelContext = createContext(1);

2. data가 필요한 컴포넌트에서 context를 사용한다.

2-1. useContext Hook과 아까 생성한 context를 import 한다.

import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

2-2. 기존 컴포넌트(여기서는 Heading)의 propscontext로 대체될 것은 제거하고, 제거된 prop를 가져오는 곳에는 context를 사용한다.

  • useContext HookHeading 컴포넌트가 LevelContext를 읽기를 원하다는 것을 React에 알리는 역할을 한다.
  • context를 제공하지않으면 React는 지정된 기본값을 사용한다.
// 기존 컴포넌트: level이 컨텍스트로 대체될 예정
export default function Heading({ level, children }) {
  // ...
}

// 컴포넌트가 context를 사용하도록 변경
export default function Heading({ children }) {
  const level = useContext(LevelContext);
  // ...
}

2-3. 기존 컴포넌트를 사용하는 곳에서 context로 대체되어 제거된 prop에 해당하는 부분을 제거한다.

2-4. 그 대신 제거된 propcontext를 제공할 컴포넌트에 전달한다.(여기서는 Section)

 

3. 데이터를 설정하는 컴포넌트에서 context를 제공한다.

context provider로 래핑 하여 LevelContext에게 값을 제공한다.

  • 이는 Section 내의 컴포넌트가 LevelContext를 요청하면 여기에 주어진 level을 제공하라고 React에게 말하는 것과 같다.
  • 단, 컴포넌트는 자신보다 위인 것 중 가장 가까운 <LevelContext.Provider>를 사용한다.
// 기존 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>
  );
}

 

결과

위의 3단계를 거치면 아래와 같이 코드가 완성된다.

각각의 Heading 컴포넌트 에 level prop을 사용하지 않는다. 대신 가장 가까운 Section에 아래와 같이 요청해서 파악한다.

  1. <Section>에서 level prop을 전달한다.
  2. Section에서 자식 요소를 <LevelContext.Provider value={level}>로 묶는다.
  3. HeadinguseContext(LevelContext)를 사용해 가장 가까운 LevelContext의 값을 요청한다.

 

 

동일한 컴포넌트에서 context 사용 및 제공

context를 사용하면 위의 컴포넌트를 통해 정보를 읽을 수 있기 때문에, 각각의 Section은 그 위의 Section으로부터 level을 읽고, 자동적으로 level + 1을 전달할 수 있다.

  • Heading, Section 에서는 LevelContext을 읽고 얼마나 깊이 있는지(level)를 파악한다.
  • Section LevelContextchildren를 래핑 하여 children가 "더 깊은" level(level + 1)에 있도록 지정한다.
// 기존 코드
export default function Page() {
  return (
    <Section level={1}>
      ...
      <Section level={2}>
        ...
        <Section level={3}>
          ...

// context를 사용
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>
  );
}

 

 

이 예제는 중첩된 컴포넌트가 context를 재정의할 수 있는 방법을 시각적으로 보여주기 위해 heading level을 사용한다.

 

context를 사용하면 "주변에 적응"해서 렌더링 되는 위치 (또는 컨텍스트)에 따라 다르게 표시되는 컴포넌트를 작성할 수 있다.

context를 제공하는 컴포넌트와 context를 사용하는 컴포넌트 사이에 얼마든지 컴포넌트를 삽입할 수 있다.

  • 삽입 가능 컴포넌트 : built-in 컴포넌트(`div`...) 또는 직접 만든 컴포넌트

따라서 동일한 컴포넌트라도 다른 중첩 레벨에 넣으면 자동적으로 가까운 context의 정보를 이용해 렌더링 된다.

아래 예제에서는 Post 컴포넌트가 어디에 위치하느냐에 따라 자동으로 <Heading> level이 적용된다.(App.js 참고)

 

 

다른 React context는 서로를 재정의하지 않는다. createContext()를 사용해 만들어지는 각 context는 다른 context와 완전히 분리되어 있으며 특정 context를  사용 및 제공하는 컴포넌트를 함께 묶는다. 따라서 하나의 컴포넌트는 문제없이 많은 다른 context를 사용하거나 제공할 수 있다.

 

context를 사용하기 전에

context를 사용하기 쉬운만큼 남용하기 쉽다. 일부 props를 여러 level의 깊이로 전달해야 한다고 해서 항상 이 정보를 컨텍스트에 넣어야 하는 것은 아니다.

context를 사용하기 전에 고려할 수 있는 대안이 몇 가지 있다.

  1. props를 전달하기.
    • 중요한 컴포넌트라면 수십 개의 컴포넌트를 통해 수십개의 prop을 전달하는 방법은 자주 사용된다.
    • 유지 보수 측면에서 props를 사용해 데이터 흐름(어떤 컴포넌트가 어떤 데이터를 사용하는지)을 명시적으로 만드는 것이 좋을 수 있다.
  2. 컴포넌트를 추출하고 JSX를 children으로 전달하기.
    • 데이터를 전달할 때 해당 데이터를 사용하지 않는 중간 컴포넌트들이 많을 경우(또 더 아래로만 전달할 경우), 이는 종종 도중에 일부 컴포넌트 추출을 제대로 하지 않았다는 의미일 수 있다.
      • 잘못된 예 : posts와 같은 data props을 직접 사용하지 않는 컴포넌트에 즉시 전달.
        • `<Layout posts={posts} />`
      • 올바른 예 : Layoutchildren을 porp으로 두고, 아래처럼 렌더링. => 이렇게 하면 데이터를 지정하는 컴포넌트와 이를 필요로 하는 컴포넌트 사이의 계층 수가 줄어든다.
        • `<Layout><Posts posts={posts} /></Layout>`

context를 위한 유즈 케이스

  • Theming: 앱에서 사용자가 모양을 변경할 수 있는 경우 (e.g. dark mode), context provider를 앱 상단에 배치하고, 시각적인 요소를 조정해야 하는 컴포넌트에서 해당 컨텍스트를 사용할 수 있다.
  • Current account: 많은 컴포넌트가 현재 로그인한 사용자를 알아야 할 수 있다. context 사용하면 트리의 어느 곳에서 나 쉽게 데이터를 읽을 수 있다. 일부 앱에서는 동시에 여러 계정을 운영할 수 도 있다. (예 : 다른 사용자로 댓글 남기기). 이때, UI 일부를 nested provider로 (현재 계정 값과) 다른 값을 래핑 하는 것이 편리하다.
  • Routing:  대부분의 라우팅 솔루션은 현재 경로를 유지하기 위해 내부적으로 context 를 사용한다. 이것이 모든 링크의 활성 여부를 "알 수 있는" 방법이다. 자신의 라우터를 구축하는 경우에도 그렇게 하고 싶을 수 있다.
  • Managing state: 앱이 성장함에 따라 앱 상단에 더 많은 상태가 생길 수 있다. 아래에 거리가 먼 많은 컴포넌트가 상태를 변경하려고 할 수 있다. 복잡한 상태를 관리하고 너무 많은 번거로움 없이 멀리 떨어진 구성 요소로 전달하기 위해 context와 함께 reducer를 사용하는 것이 일반적이다.

context의 값은 변경될 수 있다. 다음 렌더에서 다른 값을 전달하면, React는 아래에서 읽는 모든 구성 요소를 업데이트한다! 이것이 context가 종종 state와 함께 사용되는 이유이다.

일반적으로 트리의 다른 부분에 있는 멀리 떨어진 구성 요소에 일부 정보가 필요한 경우 컨텍스트가 도움이 될 것이라는 좋은 표시이다.

 

 

+ Recent posts