리액트를 다루는 기술 ( Context API )

Context API : 리액트 프로젝트에서 전역적으로 사용할 데이터가 있을 때 유용한 기능

  • 종류 : 사용자 로그인 정보 , 애플리케이션 환경 설정, 테마 등
  • 리액트 관련 라이브러리에서 많이 사용
    • 예) 리덕스 , 리액트 라우터 , styled-components 등

실습 흐름

  1. Context API를 사용한 전역 상태 관리 흐름 이해
  2. 기본적인 사용법
  3. 동적 Context 사용
  4. Consumer 대신 Hook 또는 static contextType 사용

15-1. Context API를 사용한 전역 상태 관리 흐름 이해

G 컴포넌트는 전역 상태를 업데이트 시키고 , F 와 J 컴포넌트는 업데이트 된 상태를 렌더링한다.

Root 컴포넌트의 state에는 value라는 값이 있고, 이 값을 변경시키는 handleSetValue라는 함수가 있다고 가정한다.

이러한 상황에선 , value 값과 handleSetValue 함수를 props 로 하위 컴포넌트한테 전달해주게 된다.

예를 들면 ,

value 값은 Root → A > B > F , Root → H → J 와 같은 형식으로

handleSetValue 값은 Root → A → B → E → G 와 같이 진행

실제 프로젝트에서는 컴포넌트의 깊이가 더 깊어 질 수도 있고, 다루게 되는 데이터들도 훨씬 많아 질 수도 있어서 이렇게 하면 유지보수성이 낮아질 가능성이 존재한다.

그렇기 때문에 Redux 나 MobX 같은 라이브러리를 사용한다고 한다. 하지만 Context API를 통하여 글로벌 상태 관리를 편하게 할 수 있다.

더 이상 여러 컴포넌트를 거쳐서 값을 전달해주는 것이 아니라, Context를 통해 원하는 값이나 함수를 바로 값을 받아와 사용할 수 있다.

15-2. Context API 사용법 익히기

yarn create react-app context-tutorial

15-2-1. 새 Context 만들기

src/contexts/color.js

import { createContext } from "react";

const ColorContext = createContext({ color: "black" });

export default ColorContext;

새 Context를 만들 떄는 createContext 함수를 사용한다. 파라미터에는 해당 Context의 기본 상태를 지정한다.

15-2-2. Consumer 사용하기

src/components/ColorBox.js

import React from "react";
import ColorContext from "../contexts/color";

const ColorBox = () => {
  return (
    <ColorContext.Consumer>
      {(value) => (
        <div
          style={{
            width: "64px",
            height: "64px",
            background: value.color,
          }}
        />
      )}
    </ColorContext.Consumer>
  );
};

export default ColorBox;

ColorBox라는 컴포넌트를 만들어서 ColorContext 안에 들어 있는 색상을 보여준다. 이때 색상을 props로 받아 오는 것이 아니라 ColorContext 안에 들어있는 Consumer라는 컴포넌트를 통해 색상을 조회한다.

Consumer 사이에 중괄호를 열어서 그 안에 함수를 넣어 준다. 이러한 패턴을 Function as a child 또는 Render Props 라고 한다. ( 컴포넌트의 children이 있어야 할 자리에 일반 JSX 혹은 문자열이 아닌 함수를 전달하는 것 )

App.js 렌더링

import React from "react";
import ColorBox from "./components/ColorBox";

const App = () => {
  return (
    <div>
      <ColorBox />
    </div>
  );
};

export default App;

15-2-3. Provider

Provider를 사용하면 Context의 value를 변경할 수 있다.

App.js

import React from "react";
import ColorBox from "./components/ColorBox";
import ColorContext from "./contexts/color";

const App = () => {
  return (
    <ColorContext.Provider value={{ color: "red" }}>
      <div>
        <ColorBox />
      </div>
    </ColorContext.Provider>
  );
};

export default App;

기존 createContext 함수를 사용할 때 파라미터로 Context의 기본값을 넣어 주었다. ( color: 'black' )

이 기본값은 Provider를 사용하지 않았을 때만 사용된다. 만약 Provider는 사용했는데 value를 명시하지 않았다면, 기본값을 사용하지 않기 때문에 오류가 발생한다.

15-3. 동적 Context 사용하기

지금까지는 고정적인 값만 사용할 수 있었지만, 이번에는 Context 값을 업데이트해야 하는 경우

15-3-1. Context 파일 수정하기

Context의 value에는 무조건 상태 값만 있어야 하는 것은 아니다. 함수를 전달 할 수도 있다.

contexts/color.js 수정

import { createContext, useState } from "react";

const ColorContext = createContext({
  state: { color: "black", subcolor: "red" },
  actions: {
    setColor: () => {},
    setSubColor: () => {},
  },
});

const ColorProvider = ({ children }) => {
  const [color, setColor] = useState("black");
  const [subcolor, setSubColor] = useState("red");

  const value = {
    state: { color, subcolor },
    actions: { setColor, setSubColor },
  };

  return (
    <ColorContext.Provider value={value}>{children}</ColorContext.Provider>
  );
};

// const ColorConsumer = ColorContext.Consumer와 같은 의미
const { Consumer: ColorConsumer } = ColorContext;

// ColorProvider와 ColorConsumer 내보내기
export { ColorProvider, ColorConsumer };

export default ColorContext;

ColorProvider라는 컴포넌트를 새로 작성해 주었다. 이 컴포넌트에서는 ColorContext.Provier를 렌더링하고 있다. 이 Provider의 value에 상태는 state로, 업데이트는 actions로 묶어서 전달하고 있다. 값을 동적으로 사용할 때 반드시 묶어서 줄 필요는 없지만, 객체를 따로 분리해 주면 나중에 다른 컴포넌트에서 Context의 값을 사용할 때 편하다.

createContext를 사용할 때 기본값으로 사용할 객체도 수정했다. createContext의 기본 값은 실제 Provider의 value에 넣는 객체의 형태와 일치시켜 주는 것이 좋다. 그렇게 하면 Context 코드를 볼 때 내부 값이 어떻게 구성되어 있는지 파악하기 쉽고, 실수로 Provider를 사용하지 않았을 때 리액트 애플리케이션에서 에러가 발생하지 않는다.

15-3-2. 새로워진 Context를 프로젝트에 반영하기

App 컴포넌트에 ColorContext.Provider를 ColorProvider로 대체

import React from "react";
import ColorBox from "./components/ColorBox";
import { ColorProvider } from "./contexts/color";

const App = () => {
  return (
    <ColorProvider>
      <div>
        <ColorBox />
      </div>
    </ColorProvider>
  );
};

export default App;

ColorBox도 마찬가지로 ColorContext.Consumer를 ColorConsumer로 변경 , 사용할 value의 형태도 바뀌었으니 이에 따른 변화를 반영

import React from "react";
import { ColorConsumer } from "../contexts/color";

const ColorBox = () => {
  return (
    <ColorConsumer>
      {(value) => (
        <>
          <div
            style={{
              width: "64px",
              height: "64px",
              background: value.state.color,
            }}
          />
          <div
            style={{
              width: "64px",
              height: "64px",
              background: value.state.subcolor,
            }}
          />
        </>
      )}
    </ColorConsumer>
  );
};

export default ColorBox;

객체 비구조화 할당 문법을 사용하면 value를 조회하는 것을 생략 가능

import React from "react";
import { ColorConsumer } from "../contexts/color";

const ColorBox = () => {
  return (
    <ColorConsumer>
      {({ state }) => (
        <>
          <div
            style={{
              width: "64px",
              height: "64px",
              background: state.color,
            }}
          />
          <div
            style={{
              width: "32px",
              height: "32px",
              background: state.subcolor,
            }}
          />
        </>
      )}
    </ColorConsumer>
  );
};

export default ColorBox;

15-3-3. 색상 선택 컴포넌트 만들기

Context의 actions에 넣어 준 함수를 호출하는 컴포넌트 생성

components/SelectColors.js

import React from "react";

const colors = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"];

const SelectColors = () => {
  return (
    <div>
      <h2>색상을 선택하세요.</h2>
      <div style={{ display: "flex" }}>
        {colors.map((color) => (
          <div
            key={color}
            style={{
              background: color,
              width: 24,
              height: 24,
              cursor: "pointer",
            }}
          />
        ))}
      </div>
      <hr />
    </div>
  );
};

export default SelectColors;

App 컴포넌트에 렌더링

import React from "react";
import ColorBox from "./components/ColorBox";
import { ColorProvider } from "./contexts/color";
import SelectColors from "./contexts/SelectColors";

const App = () => {
  return (
    <ColorProvider>
      <div>
        <SelectColors />
        <ColorBox />
      </div>
    </ColorProvider>
  );
};

export default App;

이제 해당 SelectColors에서 마우스 왼쪽 버튼을 클릭하면 큰 정사각형의 색상을 변경, 오른쪽 버튼을 클릭하면 작은 정사각형의 색상을 변경하도록 구현

import React from "react";
import { ColorConsumer } from "../contexts/color";

const colors = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"];

const SelectColors = () => {
  return (
    <div>
      <h2>색상을 선택하세요.</h2>
      <ColorConsumer>
        {({ actions }) => (
          <div style={{ display: "flex" }}>
            {colors.map((color) => (
              <div
                key={color}
                style={{
                  background: color,
                  width: 24,
                  height: 24,
                  cursor: "pointer",
                }}
                onClick={() => actions.setColor(color)}
                onContextMenu={(e) => {
                  e.preventDefault(); // 오른쪽 버튼 클릭 시 메뉴가 뜨는 것을 무시
                  actions.setSubColor(color);
                }}
              />
            ))}
          </div>
        )}
      </ColorConsumer>
      <hr />
    </div>
  );
};

export default SelectColors;

마우스 오른쪽 버튼 클식 이벤트는 onContextMenu를 사용하면 된다. 원래 브라우저 메뉴가 나타나지만, 여기서 e.preventDefault()를 호출하면 메뉴가 뜨지 않는다.

15-4. Consumer 대신 Hook 또는 static contextType 사용

Context에 있는 값을 사용할 때 Consumer 대신 다른 방식을 사용하여 값을 받아 오는 방법

15-4-1. useContext Hook 사용

useContext( 리액트 내장 Hooks )라는 Hook을 사용하면 함수형 컴포넌트에서 COntext를 편하게 사용할 수 있다.

components/ColorBox.js

import React, { useContext } from "react";
import ColorContext from "../contexts/color";

const ColorBox = () => {
  const { state } = useContext(ColorContext);
  return (
    <>
      <div
        style={{
          width: "64px",
          height: "64px",
          background: state.color,
        }}
      />
      <div
        style={{
          width: "32px",
          height: "32px",
          background: state.subcolor,
        }}
      />
    </>
  );
};

export default ColorBox;

children에 함수를 전달하는 Render Props 패턴이 불편하다면 , useContext Hook을 사용하여 편하게 Context 값을 조회할 수 있다.

단! Hook은 함수형 컴포넌트에서만 사용할 수 있다는 점에 주의!

15-4-2. static contextType 사용

이번엔 클래스형 컴포넌트로 알아볼텐데, Context를 쉽게 사용하고 싶다면 static contextType을 정의하는 방법이 있다. SelectColors 컴포넌트를 클래스형으로 변환하고, Consumer 코드는 일단 제거한다.

import React, { Component } from "react";

const colors = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"];

class SelectColors extends Component {
  render() {
    return (
      <div>
        <h2>색상을 선택하세요.</h2>
        <div style={{ display: "flex" }}>
          {colors.map((color) => (
            <div
              key={color}
              style={{
                background: color,
                width: 24,
                height: 24,
                cursor: "pointer",
              }}
            />
          ))}
        </div>
        <hr />
      </div>
    );
  }
}

export default SelectColors;

클래스 상단에 static contextType 값을 지정

import React, { Component } from "react";
import ColorContext from "../contexts/color";

const colors = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"];

class SelectColors extends Component {
  static contextType = ColorContext;
  render() {
    return (
      <div>
        <h2>색상을 선택하세요.</h2>
        <div style={{ display: "flex" }}>
          {colors.map((color) => (
            <div
              key={color}
              style={{
                background: color,
                width: 24,
                height: 24,
                cursor: "pointer",
              }}
            />
          ))}
        </div>
        <hr />
      </div>
    );
  }
}

export default SelectColors;

이렇게 하면 this.context를 조회했을 떄 현재 Context의 value 값을 가리키게 된다.

만약, setColor를 호출하고 싶다면 this.context.actions.setColor를 호출하면 된다.

import React, { Component } from "react";
import ColorContext from "../contexts/color";

const colors = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"];

class SelectColors extends Component {
  static contextType = ColorContext;

  handleSetColor = (color) => {
    this.context.actions.setColor(color);
  };
  handleSetSubcolor = (subcolor) => {
    this.context.actions.setSubColor(subcolor);
  };

  render() {
    return (
      <div>
        <h2>색상을 선택하세요.</h2>
        <div style={{ display: "flex" }}>
          {colors.map((color) => (
            <div
              key={color}
              style={{
                background: color,
                width: 24,
                height: 24,
                cursor: "pointer",
              }}
              onClick={() => this.handleSetColor(color)}
              onContextMenu={(e) => {
                e.preventDefault();
                this.handleSetSubcolor(color);
              }}
            />
          ))}
        </div>
        <hr />
      </div>
    );
  }
}

export default SelectColors;

static contextType을 정의하면

  • 장점 : 클래스 메서드에서도 Context에 넣어 둔 함수를 호출할 수 있다.
  • 단점 : 한 클래스에서 하나의 Context밖에 사용하지 못한다.

그러나 앞으로 새로운 컴포넌트를 작성할 때 클래스형으로 작성하는 일은 많지 않기 때문에 useContext를 사용하는 쪽을 권한다.

좋은 웹페이지 즐겨찾기