Context로 깊게 데이터 넘겨주기 - NEW 리액트 공식문서

hongregii·2023년 3월 13일
0

보통 부모에서 자식 컴포넌트로 정보를 넘겨줄 때는 prop을 사용한다. 그러나 props는 때로 귀찮아진다. 부모 - 자식 컴포넌트 사이에 3개 - 4개 컴포넌트가 끼어 있는 경우 (고조할아버지 컴포넌트 ?), 같은 정보를 아주 많은 컴포넌트가 공유하고 있어야 할 경우 등.

Context 는 자식, 손주 컴포넌트들이 props로 안받아도 특정 정보를 부모, 조상에서 받아올 수 있게 해준다. (얼마나 먼 조상이든 상관없이!)

이 문서에서는..

  • "prop drilling" 이 뭔지
  • 반복되는 prop 넘겨주기를 context로 바꾸는 법
  • context의 주요 사용 사례
  • context의 흔한 대체재
    를 배워보겠다.

props, 뭐가 문제임?

  • 아주 깊게 prop을 전달해주고 싶을 때

  • 많은 컴포넌트가 같은 prop을 가지고 있어야 할 때

  • 이런 경우 상태 끌어올리기를 사용해야 하는데, 정작 필요한 컴포넌트로부터 너무 멀리에서 선언돼야 할 수 있다. 이러면 Prop Drilling 상황이 생김.

    이게 바로 Prop Drilling

이게 먼짓거리임. 필요한 컴포넌트보다 훨~~씬 위에서 선언하고 엄청 깊게 내려주는것 보다, 필요한 컴포넌트에 "텔레포트" 하는 방법이 있지 않을까?
그럴 때 Context를 써보자, 이말입니다.

Props 대신 Context

Context는 부모 컴포넌트가 자기 하위의 전체 트리에 데이터를 제공하게 해준다.

아래 예시는 Heading 컴포넌트가 level을 받아서 싸이즈로 사용하는 경우.

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 컴포넌트 아래에 있는 Heading들은 모두 같은 싸이즈를 가지고 싶다고 해 보자.

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 이라는 prop으로 각 Heading에 따로따로 내려준다.

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

level = {3} 을 세번 쓰지 말고, Section 컴포넌트에서 선언해주면 어떨까.

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

다 좋은데, <Heading> 컴포넌트 입장에서 가장 가까운 부모 <Section>을 어떻게 알 수 있냐는 말이다.

이걸 위해서는 자식이 부모 트리 어딘가에 "데이터를 요청하는" 방법이 있어야 한다!

여기서 Context 등장. 이 3단계를 따라하시라 :

  1. Context를 만드시오.
    (헤딩 level이니까 LevelContext라고 부르자.)
  2. 데이터가 필요한 컴포넌트에서 그 Context를 쓰시오.
    (HeadingLevelContext를 사용)
  3. 부모에서 Context를 제공하시오.
    (SectionLevelContext를 제공)

이것이 바로 Context.

1단계 : Context를 만드시오.

새로운 파일 만들어서 export 해주는 게 좋음. (관심사 분리)

// LevelContext.js
   // createContext 훅 !
import { createContext } from 'react';

export const LevelContext = createContext(1);

createContext 훅이 받는 유일한 인자는 default 값이다. 객체같이 아무거나 넣어도 됨.

2단계 : Context를 쓰시오.

useContext 훅이랑 아까 만든 Context를 임포트하자.

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

지금 Heading 컨텍스트는 level 값을 props로부터 받고 있다. 이렇게 :

// Heading.js
export default function Heading({ level, children }) {
  // ...
}

이렇게 하지 말고, level prop을 없애고 아까 import 한 Context (LevelContext)에서 받아오자. 즉 :

// Heading.js
export default function Heading({ children }) {
  const level = useContext(LevelContext);
  // ...
}

useContext 는 훅이다. useStateuseReducer처럼, 훅은 리액트 컴포넌트의 최상단에서만 호출 가능함. useContext는 리액트한테 Heading 컴포넌트가 LevelContext를 읽어오고 싶다고 말해주는 것임.

이제 Heading 컴포넌트가 level prop을 아지고 있지 않으니, 이렇게 prop을 넘겨줄 필요가 없다 :

// 이 코드가

// App.js
<Section>
  <Heading level={4}>Sub-sub-heading</Heading>
  <Heading level={4}>Sub-sub-heading</Heading>
  <Heading level={4}>Sub-sub-heading</Heading>
</Section>
// 이렇게

// App.js
<Section level={4}>
  <Heading>Sub-sub-heading</Heading>
  <Heading>Sub-sub-heading</Heading>
  <Heading>Sub-sub-heading</Heading>
</Section>

Section에서 level을 받으면 됨.

여기까지 읽었을 때 이상함을 느껴야 정상이다. 왜냐하면 Heading에서 Context를 사용하긴 했지만, Section에서 아직 제공하고 있지 않기 때문! 리액트가 Context를 어디서 가져와야 할지 아직 알지 못함.

이렇게 Context를 제공하지 않으면, 리액트는 맨 처음에 선언된 default 값 을 사용함. 이 예시에서는 createContext의 인자로 1을 줬다. 그래서 useContext(LevelContext)1을 리턴. 즉 모든 heading들의 level 이 1 이 되니까 사실상 모든 컴포넌트가 <h1> 태그가 될 것이다.

이제 고쳐보자. 어떻게?
Section이 자신의 Context를 자식들에게 제공하면 됨.

3단계 : Context를 제공하시오.

소외됐던 Section 컴포넌트를 다시 보면 :

export default function Section({ children }) {
  return (
    <section className="section">
      {children}
    </section>
  );
}

children을 <section> 태그 안에 그대로 렌더링하는 녀석.

context provider로 감싸서 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을 줘. "
라고 말해주는 것이다! 그 컴포넌트는 그럼 가장 가까운 <LevelContext.Provider>value를 사용할 것이다.

이 경우 level을 인자 prop으로 받았고, Providervalue값에 level을 넘겨줬으니, Heading 들은 가장 가까운 SectionProvider, 그 중에서도 value 를 찾아올 것이다.

// 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 컴포넌트가 상위의 Section 컴포넌트에 요청해서 "자기가 알아서 찾아간다". 다시 정리 :

  1. <Section>에게 level prop 넘겨주기
  2. <Section>은 자식들을 <LevelContext.Provider value={level}> 로 감싼다.
  3. HeadinguseContext(LevelContext)를 통해 가장 가까운 상위 LevelContext 값을 찾아감.

한 컴포넌트에서 Context 제공하고 사용하기

지금은 각 section의 level을 어찌 됐건 수동으로 지정해줘야 한다.

// Page.js 또는 App.js
export default function Page() {
  return (
    <Section level={1}>
      ...
      <Section level={2}>
        ...
        <Section level={3}>
          ...

Context가 상위 컴포넌트의 정보를 읽게끔 하기 때문에, 각 Section은 상위 Section에서 level을 읽어올 수 있고, 자동으로 level + 1을 내려줄 수 있다. 이렇게 하면 된다 :

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

export default function Section({ children }) {
  const level = useContext(LevelContext); // Section 에서도 상위 level을 가져오자!
  return (
    <section className="section">
      <LevelContext.Provider value={level + 1}> // 아래로 내려주기 위한 LevelContext를 내려주자!
        {children}
      </LevelContext.Provider>
    </section>
  );
}

이러면 level prop을 <Section>, <Heading> 둘 모두에게 내려주지 않아도 됨!

자동으로
상위 level 가져와서 자기가 쓰고, 자식한테는 level + 값을 내려줬기 때문.

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading>Title</Heading>
      <Section>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Section>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Section>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

CSS 상속과 비슷합니다

  • CSS에서 <div>color:blue 속성을 붙이면, 하위 DOM은 깊이에 상관 없이 모두 그 속성을 상속하게 된다. 중간에 color:green을 부여하지 않는 한!
  • 리액트 Context에서도 state값을 상위로 찾는데, 중간에 overriding 되지 않으면 그 값이 그 값이 된다.
  • CSS에서 colorbackground-color가 서로 다른 속성이라 서로 override 하지 못하는 것과 같이, Context도 다른 변수는 서로 섞이지 않음 (너무당연하잔슴)

Context, 막 쓰기 전에

overuse 하지 말자. props가 더 간단한 것은 팩트임. 다음은 Context 도입 전에 고려할 대체재들이다.

  1. props로 시작해라. 보통은 3개, 4개, 5개 넘는 하위 컴포넌트로 넘겨줄 일이 없으니깐.. 문서에 따르면 별것도 아닌데 Context 떡칠해놓으면 유지보수하는 개발자가 화낼 수 있다고 한다.. ㅋㅋ

  2. 컴포넌트를 추출하고, children prop 을 잘 써라. Props로 내려줄 때 중간에 거쳐가는 컴포넌트가 많다면(=중간에 안쓰고 내려주기만 하는 컴포넌트들), 높은 확률로 중간에 컴포넌트를 추출 extract 하지 않았을 수 있다.

    • <App>에서 <Layout> 안에 있는 <Posts> 컴포넌트로 posts라는 prop을 내려주고 싶다.
    • 이렇게 할 수도 있지만 : <Layout posts={posts} />
    • 그러면 Layoutposts prop을 쓰지도 않는데 받아서 다시 Posts로 내려줘야 하는 것. 이러지 말고
    • <Layout> <Posts posts={posts} /> </Layout> 요렇게 짠 다음, Layout이 posts 말고 children 을 받게 해라! 데이터가 필요한 컴포넌트를 보다 직관적으로 볼 수 있음.

Context, 이럴 때 쓰자

  • Theming : 다크 모드 등. 앱 최상단에 context provider 놓고 필요한 데서 마구 가져다 쓰자.
  • 로그인한 계정 : 현재 로그인한 사람이 누군지 알아야 하는 컴포넌트들이 있다!
  • Routing : 대부분의 라우팅 패키지가 이미 Context를 사용하고 있다고 함. 패키지를 안쓰고 스스로 라우팅을 만들고 싶다면 context를 사용하자.
  • 상태관리 : reducer 와 같이 사용하면 더욱 좋다! 이것은 다음 문서에서..

다섯줄 요약

  • Context는 자식 트리가 어떤 정보에 접근하게 제공해줌.

  • context를 내려주려면 :

    1. export const MyContext = createContext(defaultValue) 로 생성, 추출
    2. useContext(MyContext)훅에 인자로 넘겨준다.
    3. <MyContext.Provider value= {...}> 로 childern을 감싼다. 이제 children 안의 컴포넌트는 value 에 접근 가능.
  • 중간에 컴포넌트가 있어도 Context는 그대로 통과

  • Context를 쓰면 컴포넌트가 그때그때 주변에 맞게 적응하게 할 수 있음. 위에서 한 컴포넌트에서 Context 제공하고 사용하기 참조

  • Context 쓰기 전에 props로 넘겨주기 or children으로 JSX 넘겨주기 해 볼 것.

profile
잡식성 누렁이 개발자

0개의 댓글