본문 바로가기

프론트엔드(Front-End)/React

[REACT] Context API

ContextAPI란?

 

리액트에서 컴포넌트간 데이터의 흐름은 단방향 입니다. 이러한 특징은 JavaScript라는 언어의 특성 상 디버깅이 어렵다는 단점을 보완하기 위해 도입된 rule에서 기인하는데요. 그래서 컴포넌트 간에 데이터를 전달하려면, 부모 컴포넌트로부터 props의 형태로 값을 물려 받아야 할 것입니다. 개발을 편하게 하려고 이러한 규칙을 정했다지만, 개발을 하다보면 컴포넌트 간에 데이터를 송수신 해야 할 경우가 무척이나 많겠죠? 아래의 모식도를 보겠습니다.

 

 

리액트로 개발된 어떤 SPA가 위의 모식도와 같이 구성되어 있다고 가정해 봅시다. 개발자가 C라는 컴포넌트에서 이벤트가 발생하면 이웃한 B와 E 컴포넌트에서 어떤 데이터가 반영되고자 한다면 모식도 처럼 최상위 컴포넌트인 App 컴포넌트부터 시작하여 B를 업데이트 하기 위해 App → A → B 순서로 데이터가 이동하고, E를 업데이트 하기 위해 App → D → E 로 데이터를 이동하게 해야 합니다. 리액트의 컴포넌트는 props가 변경되면 리렌더링 이 발생하기 때문에 B와 E만 바꾸고 싶었어도 결과적으로는 A와 D까지 리렌더링이 되게 됩니다. 또한, 이러한 플로우는 프로그램 규모가 커지면 서비스 성능에도 영향을 줄 수 있기 때문에 프로그래밍 언어 Java나 C처럼 일종의 전역변수 처럼 사용할 수 있는 것에 대한 요구가 생기게 되었고, 이러한 요구를 해결하기 위한 방법 중 하나가 바로 Context API 입니다. 다음 챕터에서는 조금 더 고도화된 Redux라는 것을 통해 이를 관리하게 될 테인데요. Redux도 좋지만 Context API도 이러한 데이터를 송수신 하는 데 사용하기 나쁘지 않아서 필요하다면 개발자의 재량으로 얼마든지 활용할 수 있습니다.

Context

Context API를 사용하기 위해서는 데이터를 담고 송수신 할 일종의 허브(Hub)가 필요한데요. 이때 사용되는 것이 Context 입니다. 개발자가 서로 식별하기 쉽게 이러한 이름으로 호칭 하는 것이며 Context가 될 컴포넌트의 실제 이름은 무엇이든 상관 없습니다.

color.js

import { createContext } from "react";

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

export default ColorContext;
  • 색상에 대한 CSS 속성 값을 담는 Context 인 Color.js 를 위와 같이 선언합니다.

Context Consumer

  • Context로부터 값을 받아서 컴포넌트에 적용하려면 Context Consumer를 통해 값을 받아와야 합니다.
  • Context Consumer는 아래와 같이 사용할 수 있습니다.

ColorBox.js

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

const ColorBox = () => {
  return (
    <ColorContext.Consumer>
      {/* 
        Consumer 태그를 열고 중괄호를 열어서 이 사이에 함수를 넣어주었다.
        이러한 패턴으로 코드를 작성하는 것을 Function as a child 혹은 Render Props라고 한다. 
        컴포넌트의 JSX가 있어야 할 자리에 문자열이 아닌 함수 자체를 전달하는 것
      */}
      {(value) => (
        <div
          style={{
            width: "64px",
            height: "64px",
            background: value.color,
          }}
        ></div>
      )}
    </ColorContext.Consumer>
  );
};

export default ColorBox;
  • ColorContext 로부터 값을 받아 시각적으로 보여주는 데 사용할 컴포넌트를 하나 생성합니다.
  • ColorContext 로부터 값을 수신하기 위해서는 <ColorContext.Consumer/> 라는 태그로 감싸여야 합니다.
  • 위의 ColorBox.js에서는 독특한 문법을 하나 볼 수 있는데, 분명 <ColorContext.Consumer/> 라는 태그도 JSX에 일종일 텐데 children 영역에 어떤 값이나 태그가 아닌 함수 가 삽입이 가능한 것을 확인할 수 있습니다. 이렇게 작성하는 패턴을 function as child 또는 Render props 라고 부르며, Context 에서 뿐만 아니라 다른 일반적인 컴포넌트에서도 이러한 패턴을 사용할 수 있습니다.

App.js

import "./App.css";
import "./Custom.css";

//컴포넌트 삽입
import ColorBox from "./components/ColorBox";
import ColorContext from "./contexts/color";

const App = () => {
  return (
    <>
      <h1>기본 context 사용하기</h1>

      <div className="color-div">
        {/* Context를 설정하는 첫번째 방법! */}
        <ColorBox />
      </div>
		</>
  );
};

export default App;
  • 위와 같이 App.js에 ColorBox 태그를 입력하게 되면 아래와 같이 컴포넌트가 정상 렌더링 된 것을 확인할 수 있습니다.


Context Provider

  • Context 의 값을 props처럼 변경하여 동적으로 사용하고 싶다면 아래와 같이 사용할 수 있습니다.

App.js

import "./App.css";
import "./Custom.css";

//컴포넌트 삽입
import ColorBox from "./components/ColorBox";
import ColorContext from "./contexts/color";

const App = () => {
  return (
    <>
      <h1>기본 context 사용하기</h1>

      <div className="color-div">
        {/* Context를 설정하는 첫번째 방법! */}
        <ColorBox />
				<ColorContext.Provider value={{ color: "red" }}>
				          <ColorBox />
				</ColorContext.Provider>
      </div>
		</>
  );
};

export default App;

  • Provider를 통해 값이 변경된 컴포넌트는 아래와 같이 다른 색상으로 보입니다.


동적으로 Context 사용하기

  • Context 는 위 처럼 이미 작성된 것을 사용해도 되지만, 개발자가 별도로 커스터마이징 하여 사용 가능합니다.
  • Context 의 Consumer와 Provider는 아래와 같이 커스터마이징 가능합니다.

color2.js

import { createContext, useState } from "react";

// createContext를 통해 생성하는 ColorContext의 state 값은
// 아래의 ColorProvider에서 실제로 사용할 객체의 기본 값으로 일치시켜주는 것이 좋다.
// 나중에 실수해서 값을 안넣어도 오류 안나기도 하고, 코드를 분석하는데도 도움을 준다.
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;

Context Consumer

ColorBox3.js

// Hook을 사용한 ColorBox

import { useContext } from "react";
import ColorContext from "../contexts/color2";

const ColorBox3 = () => {
  const { state } = useContext(ColorContext);
  return (
    <>
      <div
        className="color-box"
        style={{
          background: state.color,
        }}
      />
      <div
        className="color-box"
        style={{
          background: state.subColor,
        }}
      />
    </>
  );
};

export default ColorBox3;
  • 함수형 컴포넌트라면 useContext 라는 Hook을 통해 context를 조금 더 편하게 사용 가능합니다.

일반 컴포넌트 (SelectColor)

  • 여러가지 색상 팔레트를 선택하면 그 색상으로 다른 컴포넌트의 색상을 바꿔보겠습니다.
  • 이를 위해 일종의 리모컨의 역할을 할 일반 컴포넌트 (SelectColor)를 생성합니다.
  • 여기서 발생한 이벤트로 ColorBox3 컴포넌트 색상을 바꿔보도록 하겠습니다.
import { ColorConsumer } from "../contexts/color2";

const colors = ["red", "orangered", "orange", "yellow", "green", "blue", "skyblue", "navy", "violet", "pink"];

const SelectColor = () => {
  return (
    <>
      <div>
        <h1>색상을 선택하세요.</h1>
        <ColorConsumer>
          {({ actions }) => (
            <>
              <div style={{ display: "flex" }}>
                {colors.map((color) => (
                  <div
                    key={color}
                    className="color-box"
                    style={{
                      background: color,
                      cursor: "pointer",
                    }}
                    onClick={() => actions.setColor(color)}
                    onContextMenu={(e) => {
                      e.preventDefault(); // 마우스 오른쪽 버튼 클릭시 메뉴가 뜨는 것을 방지함
                      actions.setSubColor(color);
                    }}
                  />
                ))}
              </div>
            </>
          )}
        </ColorConsumer>
        <hr />
      </div>
    </>
  );
};

export default SelectColor;

App.js

import "./App.css";
import "./Custom.css";

//컴포넌트 삽입
import ColorBox from "./components/ColorBox";
import ColorContext from "./contexts/color";

// color2로부터 ColorProvider를 최상위 컴포넌트에 삽입
import { ColorProvider } from "./contexts/color2";

// colorBox2를 삽입
import ColorBox2 from "./components/ColorBox2";

// SelectColor 컴포넌트를 삽입
import SelectColor from "./components/SelectColor";

동적으로 context의 값이 변하면 다른 컴포넌트에 반영되는 모습