Context API 제대로 이해하기

이은지·2022년 6월 30일
3
post-thumbnail

👩🏻‍💻 언제 사용하나요?

✅ React Component Tree의 서로 다른 부분에서 동일한 데이터를 필요로 할 때

위 상황에서 보통은 props를 사용해 데이터를 전달하게 됩니다.
그런데 컴포넌트 간의 거리가 먼 경우, 특정 컴포넌트로 데이터를 전달하기 위해 여러 개의 컴포넌트를 거쳐야 하는 상황이 발생합니다.

A
|
B
|
C
|
D

위와 같은 구조로 이뤄진 React Component Tree를 가정해봅시다.

A: useState로 데이터를 관리하는 컴포넌트
D: A의 데이터를 바탕으로 UI를 그리는 컴포넌트

라고 가정했을 때, D에게 A의 데이터를 넘겨주려면 어떻게 해야 할까요?

B,C가 props로 부모 컴포넌트(각각 A,B)로부터 데이터를 전달 받고, 다시 자식 컴포넌트(각각 C,D)에게 props를 통해 해당 데이터를 전달해줘야겠죠.

심지어 B, C는 데이터를 필요로 하지 않는데도, 단지 D에게 데이터를 전달해주기 위해 props를 받아 전달하는 작업을 해야 합니다.

이때, D가 A의 데이터에 곧바로(B,C를 거치지 않고) 접근하도록 해주는 게 바로 Context API라고 할 수 있습니다.

📋 구성 요소

Context를 사용하기 위해서는 총 세 가지의 구성요소를 알아야 합니다!

  1. CreateContext: Context를 생성 하는 것
  2. Consumer Component: Context에 저장된 데이터를 읽어와서 사용 하는 컴포넌트
  3. Provider Component: Context에 데이터를 저장 하는 컴포넌트

Context는 우리가 원하는 데이터를 저장하는 공간이라고 할 수 있습니다.
Context를 사용하기 위해선 우선 이 공간을 만들어줘야겠죠! 이게 바로 1번 createContext입니다. createContext로 공간을 만들었다면, 이제 이 공간에 데이터를 저장하고, 저장된 데이터를 읽어와서 사용할 수 있게 됩니다 👏🏻👏🏻👏🏻

위의 예시에서는 A가 Provider Component가 되고, D가 Consumer Component가 되겠네요!
그럼 실제로 Context를 어떻게 만드는지, Provider Component와 Consumer Component에서 각각 어떤 방법을 통해 데이터를 저장하고 읽어오는지를 알아보겠습니당.

📓 사용 방법

예시 설명

쉬운 이해를 위해 예시를 한 번 들어보겠습니다!
아래 사진처럼, 상단의 체크박스를 클릭하면 이미지의 사이즈가 커지는 기능이 구현되어 있다고 해봅시다.


짠 이렇게요

위의 기능을 구현하기 위해 작성한 코드를 한 번 살펴볼게요.

import { useState } from 'react';
import { places } from './data.js';
import { getImageUrl } from './utils.js';

export default function App() {
  const [isLarge, setIsLarge] = useState(false);
  const imageSize = isLarge ? 150 : 100;
  return (
    <>
      <label>
        <input
          type="checkbox"
          checked={isLarge}
          onChange={e => {
            setIsLarge(e.target.checked);
          }}
        />
        Use large images
      </label>
      <hr />
      <List imageSize={imageSize} />
    </>
  )
}

function List({ imageSize }) {
  const listItems = places.map(place =>
    <li key={place.id}>
      <Place
        place={place}
        imageSize={imageSize}
      />
    </li>
  );
  return <ul>{listItems}</ul>;
}

function Place({ place, imageSize }) {
  return (
    <>
      <PlaceImage
        place={place}
        imageSize={imageSize}
      />
      <p>
        <b>{place.name}</b>
        {': ' + place.description}
      </p>
    </>
  );
}

function PlaceImage({ place, imageSize }) {
  return (
    <img
      src={getImageUrl(place)}
      alt={place.name}
      width={imageSize}
      height={imageSize}
    />
  );
}

해당 링크에서 위의 코드를 확인해보실 수 있습니다

지금 보시면 App-List-Place-PlageImage 구조로 Component Tree가 구성되어 있네요. 사용자의 체크박스 클릭 이벤트를 감지해서 imageSize를 변경하는 건 App 컴포넌트에서 이뤄지고 있고, PlageImage 컴포넌트에서 해당 값을 사용해 UI를 그리고 있어요.

App에서 PlageImage까지 imageSize 값을 전달하기 위해, imageSize 값을 사용하지도 않는 ListPlace 컴포넌트에 props로 해당 값을 전달하고 있는 상황입니다. 아주 번거롭죠 🤦🏻‍♀️

이럴 때 Context를 사용한다면, 여러 컴포넌트를 거칠 필요 없이 App에서 저장(혹은 변경)한 imageSize값을 PlaceImage 컴포넌트가 읽어올 수 있을거예요! 한 번 사용해봅시다.

Context 생성하기

새로운 Context의 생성은 주로 별도의 파일에서 작성합니다.
Context.js라는 파일을 생성해서, 안에 아래의 코드를 작성해줄게요.

import { createContext } from 'react';

export const ImageSizeContext = createContext(500);

ImageSizeContext라는 이름의 새로운 context를 생성하겠다는 뜻입니다.
이제 다른 컴포넌트에서 ImageSizeContext를 import하면 이 context안에 담긴 데이터를 읽고, 변경할 수 있게 됩니다. createContext() 괄호 안에 값을 적어주면, Context의 기본값을 설정해줄 수 있어요. 단순 숫자나 string이 아닌 객체로 설정할 수도 있답니다!

Use(consume) the Context

Context에 저장된 값을 읽어오기 위해서는, useContext 라는 Hook을 사용합니다.
imageSize 값을 필요로 하는 컴포넌트는 어디죠? PlaceImage 컴포넌트죠.
값을 필요로 하는 컴포넌트 내부에서 useContext를 사용하면 Context를 읽어올 수 있습니다.

useContext(MyContext) 라는 코드는, React에게 "나 MyContext의 정보를 읽어오고 싶어!"하고 이야기하는 것과 같습니다.

import { ImageSizeContext } from './Context.js';

function PlaceImage({ place }) {
  const imageSize = useContext(ImageSizeContext);
  return (
    <img
      src={getImageUrl(place)}
      alt={place.name}
      width={imageSize}
      height={imageSize}
    />
  );
}

이렇게 하면 아까 createContext에 저장해둔 500이란 값이 imageSize 변수에 담기게 됩니다.

Provide the Context

그러나 저희는 지금 이미지 사이즈를 500으로 설정하고 싶은 게 아니라, 사용자가 체크박스를 클릭했는지 아닌지에 따라 이미지 사이즈를 바꾸고 싶은거죠. 그러기 위해선 App 컴포넌트에서 Context 내의 imageSize값을 업데이트 해줘야 합니다.

Conetext 안에 들어있는 Provider라는 컴포넌트를 통해 Context의 value를 변경할 수 있습니다.

<ImageContext.Provider value={imageSize}> 태그로 자식 컴포넌트를 감싸주면, 자식 컴포넌트들은 useContext를 통해 위 태그에서 지정한 value를 읽어오게 됩니다.

export default function App() {
  const [isLarge, setIsLarge] = useState(false);
  const imageSize = isLarge ? 150 : 100;
  return (
    <ImageSizeContext.Provider
      value={imageSize}
    >
      <label>
        <input
          type="checkbox"
          checked={isLarge}
          onChange={e => {
            setIsLarge(e.target.checked);
          }}
        />
        Use large images
      </label>
      <hr />
      <List />
    </ImageSizeContext.Provider>
  )
}

이렇게 하면 이제 사용자가 체크박스를 클릭할 때마다 ImageSizeContext 내의 value가 업데이트 될거고, PlaceImage 컴포넌트는 업데이트 된 value를 읽어와 이미지의 사이즈를 변경하겠죠! 번거로운 props 전달 없이 동일한 기능을 구현해냈어요 👏🏻👏🏻

(추가) Nested Provider

모든 컴포넌트들은 자신과 가장 가까운(React Component 트리 상에서 수직-위 방향으로) <Context.Provider> 에서 지정한 value를 읽어오게 됩니다.
가령 위 코드와 동일하게 작성하되, 아래처럼 PlageImage 컴포넌트의 직계 부모 컴포넌트인 Place 컴포넌트에서 <Context.Provider>로 자식 컴포넌트를 감싼다면?

function Place({ place }) {
  return (
    <ImageSizeContext.Provider value={500}>
      <PlaceImage place={place} />
      <p>
        <b>{place.name}</b>
        {': ' + place.description}
      </p>
    </ImageSizeContext.Provider>
  );
}

이미지 크기는 500이 될겁니다. App 컴포넌트의 <ImageContext.Provider> 보다 Place 컴포넌트의 <ImgaeContext.Provider>가 PlaceImage와 더 가깝기 때문이죠.

각 Context는 개별적이며, 한 컴포넌트에서 여러 개의 Context를 사용할 수도 있지만 동일한 Context의 경우 얼마든지 override가 가능합니다.

Context를 사용한 코드 전문은 여기서 확인하실 수 있습니다


Reference
https://beta.reactjs.org/learn/passing-data-deeply-with-context

profile
교육학과 출신 서타터업 프론트 개발자 👩🏻‍🏫

0개의 댓글