ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • React :: 15. Context API
    Dev/프론트엔드 2021. 7. 26. 10:58
    이 글은 김민준(벨로퍼트)님의 - 리액트를 다루는 기술 도서를 참고하여 학습하는 글 입니다.

     Context API는 리액트 프로젝트에서 전역적으로 사용할 데이터가 있을 때 유용한 기능입니다. 이를테면 사용자 로그인 정보, 애플리케이션 환경 설정, 테마 등 여러 종류가 있겠지요. Context API는 리액트 v16.3부터 사용하기 쉽게 많이 개선되었습니다. 이 기능은 리액트 관련 라이브러리에서도 많이 사용되고 있습니다. 예를 들어 리덕스, 리액트 라우터, styled-components 등의 라이브러리는 Context API를 기반으로 구현되어 있습니다.

    이번 실습은 다음 흐름으로 진행됩니다.

    Context API를 사용한 전역 상태 관리 흐름 이해하기 - 기본적인 사용법 익히기 - 동적 Context 사용하기 - Consumer 대신 Hook 또는 static contextType 사용하기

     

    15.1 Context API를 사용한 전역 상태 관리 흐름 이해하기

    15-2 일반적인 전역 상태 관리 흐름

    프로젝트 내에서 환경 설정, 사용자 정보와 같은 전역적으로 필요한 상태를 관리해야 할 때는 어떻게 해야 할까요? 리액트 애플리케이션은 컴포넌트 간에 데이터를 props로 전달하기 때문에 컴포넌트 여기저기서 필요한 데이터가 있을 때는 주로 최상위 컴포넌트인 App의 state에 넣어서 관리합니다.

     

    다음과 같이 가정해 볼까요? G 컴포넌트는 전역상태를 업데이트시키고, F와 J컴포넌트는 업데이트된 상태를 렌더링합니다. 그렇다면 App 컴포넌트에서는 다음과 같이 상태와 업데이트 함수를 정의해야 합니다.

    const [value, setValue] = useState('hello')
    const onSetValue = useCallback(value =>setValue(value),[]);

    그리고 App(Root)이 지니고 있는 value 값을 F 컴포넌트와 J 컴포넌트에 전달하려면 여러 컴포넌트를 거쳐야 합니다. F의 경우 App -> A -> B -> F의 흐름이고, J의 경우 App -> H -> J의 흐름입니다. 추가로 G 컴포넌트에 상태 업데이트 함수를 전달할 때도 App -> A -> B -> E -> G와 같이 복잡하게 여러 번 거쳐서 전달해야 합니다.

    실제 리액트 프로젝트에서는 더 많은 컴포넌트를 거쳐야 할 때도 있고 다루어야 하는 데이터가 훨씬 많아질 수도 있으므로, 이런 방식을 사용하면 유지 보수성이 낮아질 가능성이 있습니다.=

    그렇기 때문에 리덕스나 MobX 같은 상태 관리 라이브러리를 사용하여 전역 상태 관리 작업을 더 편하게 처리하기도 하는데요. 리액트 v16.3 업데이트 이후에는 Context API가 많이 개선되었기 때문에 별도의 라이브러리를 사용하지 않아도 전역 상태를 손쉽게 관리할 수 있습니다.

    그림 15-3과 같이 기존에는 최상위 컴포넌트에서 여러 컴포넌트를 거쳐 props로 원하는 상태와 함수를 전달했지만, Context API를 사용하면 Context를 만들어 단 한 번에 원하는 값을 받아와서 사용할 수 있습니다.

    15-3 Conetxt API를 사용한 전역 상태 관리 흐름

    15.2 Context API 사용법 익히기

    그럼 본격적으로 Context API를 사용하는 방법을 배워 봅시다! 먼저 연습할 리액트 프로젝트를 새로 생성해 주세요.

    $ yarn create react-app context-tutorial

    15.2.1 새 Conetext 만들기

    프로젝트를 생성한 후, 새로운 Context를 만들어 보세요. src 디렉터리에 contexts 디렉터리를 만든 뒤 그 안에 color.js라는 파일을 만듭니다. Context를 만들 때 반드시 contexts 디렉터리에 만들 필요는 없습니다. 다만, 다른 파일과 구분하기 위해 따로 디렉터리를 만들었으며, 추후 Context를 사용할 때는 마음대로 경로를 지정해도 상관없습니다.

    파일을 만들었으면 다음 코드를 입력해 보세요.

    contexts/color.js
    import { createContext } from 'react';
    
    const ColorContext = createContext({ color: "black" });
    export default ColorContext;

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

    15.2.2 Consumer 사용하기

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

    src 디렉터리에 components 디렉터리를 만들고, 그 안에 ColorBox.js 파일을 생성하여 다음 코드를 입력해 보세요.

    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;

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

    Render Props 예제
    Render Props 패턴이 헷갈린다면 다음 예제를 살펴보세요. 이해하는 데 도움이 될 것입니다.

    import React from "react";
    const RenderPropSample = ({children}) =>{
        return <div>결과 : { children(5) }</div>;
    };
    
    export default RenderPropSample;

     

    만약 위와 같은 컴포넌트가 있다면 추후 사용할 때 다음과 같이 사용할 수 있습니다.
    <RenderPropsSample>{value => 2 * value}</RenderPropsSample>;​

    RenderPropsSample에게 children props로 파라미터에 2를 곱해서 반환하는 함수를 전달하면 해당 컴포넌트에서는 이 함수에서 5를 인자로 넣어서 "결과: 10" 을 렌더링합니다.

    컴포넌트를 다 만들었다면 이 컴포넌트를 App에서 렌더링하세요.

    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 컴포넌트를 다음과 같이 수정해보세요.

    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;

    코드를 저장하고 나면 다음과 같이 빨간색 정사각형이 나타날 것입니다.

    Provider로 value 덮어쓰기

    기존에 createContext 함수를 사용할 때는 파라미터로 Context의 기본값을 넣어 주었지요? 이 기본값은 Provider를 사용하지 않았을 때만 사용됩니다. 만약 Provider는 사용했는데 value를 명시하지 않았다면, 이 기본값을 사용하지 않기 때문에 오류가 발생합니다.

    다음 코드는 오류가 발생하는 코드입니다.

    App.js
    import React from "react";
    import ColorBox from "./components/ColorBox";
    import ColorContext from "./contexts/color";
    const App = () => {
      return (
        <ColorContext.Provider>
          <div>
            <ColorBox />
          </div>
        </ColorContext.Provider>
      );
    };
    
    export default App;

    15.3 동적 Context 사용하기

    지금까지 배운 내용으로는 고정적인 값만 사용할 수 있습니다. 이번에는 Context의 값을 업데이트 해야 하는 경우 어떻게 해야 하는지 알아보겠습니다.

    15.3.1 Context 파일 수정하기

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

    기존에 작성했던 ColorContext의 코드를 다음과 같이 수정해 주세요. 이번에 코드를 작성한 후 저장하면 오류가 발생할 텐데, 해당 오류는 나중에 수정할 것이므로 걱정하지 마세요.

    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 },
        action: { 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.Provider를 렌더링하고 있죠. 이 Provider의 value에는 상태는 state로, 업데이트 함수는 action로 묶어서 전달하고 있습니다.

    Context에서 값을 동적으로 사용할 때 반드시 묶어줄 필요는 없지만, 이렇게 state와 action 객체를 따로따로 분리해 주면 나중에 다른 컴포넌트에서 Context의 값을 사용할 때 편합니다.

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

    15.3.2 새로워진 Context를 프로젝트에 반영하기

    코드를 다 작성했으면 App컴포넌트에서 ColorCuontext.Provider를 ColorProvider로 대체하세요.

    App.js
    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의 형태도 바뀌었으니 이에 따른 변화를 다음과 같이 반영시켜 보세요.

    component/ColorBox.js
    import React from "react";
    import ColorCunsumer from "../contexts/color";
    
    const ColorBox = () => {
      return (
        <ColorCunsumer>
          {(value) => (
            <>
              <div
                style={{
                  width: "64px",
                  height: "64px",
                  background: value.state.color,
                }}
              />
              <div
                style={{
                  width: "32px",
                  height: "32px",
                  background: value.state.subcolor,
                }}
              />
            </>
          )}
        </ColorCunsumer>
      );
    };
    
    export default ColorBox;

    위 코드에서 객체 비구조화 할당 문법을 사용하면 다음과 같이 value를 조회하는 것을 생략할수 도 있습니다.

    components/ColorBox.js
    import React from "react";
    import ColorCunsumer from "../contexts/color";
    
    const ColorBox = () => {
      return (
        <ColorCunsumer>
          {({state}) => (
            <>
              <div
                style={{
                  width: "64px",
                  height: "64px",
                  background: state.color,
                }}
              />
              <div
                style={{
                  width: "32px",
                  height: "32px",
                  background: state.subcolor,
                }}
              />
            </>
          )}
        </ColorCunsumer>
      );
    };
    
    export default ColorBox;

    코드를 다 작성했다면 브라우저를 확인해 보세요.

    여러 상태 값 사용하기

    검정색 정사각형과 빨간색 정사각형이 잘 보이나요?

    15.3.3 색상 선택 컴포넌트 만들기

    이번에는 Context의 actions에 넣어 준 함수를 호출하는 컴포넌트룰 만들어 보겠습니다. components 디렉터리에 SelectColor.js라는 파일을 생성하여 다음 코드를 작성해 보세요. 지금은 Consumer를 사용하지 않고 UI만 준비해 봅시다.

    components/SelectColor.js
    import React from "react";
    
    const colors = ["red", "orange", "yellow", "green", "blud", "indigo", "violet"];
    
    const SelectColors = () => {
      return (
        <div>
          <h2>색상을 선택하세요</h2>
          <div style={{ display: "flex" }}>
            {colors.map((color) => (
              <div
                key={color}
                style={{
                  background: color,
                  width: "24px",
                  height: "24px",
                  cusrsor: "pointer",
                }}
              />
            ))}
          </div>
          <hr />
        </div>
      );
    };
    
    export default SelectColors;

    다 작성했으면 이 컴포넌트를 App 컴포넌트에서 ColorBox위에 렌더링하세요.

    App.js
    import React from "react";
    import ColorBox from "./components/ColorBox";
    import SelectColors from "./components/SelectColors";
    import { ColorProvider } from "./contexts/color";
    const App = () => {
      return (
        <ColorProvider>
          <div>
            <SelectColors/>
            <ColorBox />
          </div>
        </ColorProvider>
      );
    };
    
    export default App;

    브라우저에 다음과 같이 무지개 색상으로 이루어진 정사각형들이 나타났나요?

    SelectColors UI

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

    components/SelectColors.js
    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: '24px',
                    height: '24px',
                    cursor: 'pointer'
                  }}
                  onClick={() => this.handleSetColor(color)}
                  onContextMenu={e => {
                    e.preventDefault();
                    this.handleSetSubcolor(color);
                  }}
                />
              ))}
            </div>
            <hr />
          </div>
        );
      }
    }
    
    export default SelectColors;

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

    브라우저를 열어서 SelectColors에 있는 정사각형들을 마우스 왼쪽 및 오른쪽 버튼으로 클릭해보세요. 하단에 있는 정사각형들의 색상이 잘 바뀌나요?

    색상 바꿔 보기

    15.4 Consumer 대신 Hook 또는 static contextType 사용하기

    이번에는 Context에 있는 값을 사용할 때 Consumer 대신 다른 방식을 사용하여 값을 받아 오는 방법을 알아보겠습니다.

    15.4.1 useContext Hook 사용하기

    리액트에 내장되어 있는 Hooks 중에서 useContext라는 Hook을 사용하면, 함수형 컴포넌트에서 Conetext를 아주 편하게 사용할 수 있습니다. ColorBox 컴포넌트의 코드를 다음과 같이 수정해 보세요.

    components/ColorBox.js
    import React, { useContext } from 'react';
    import ColorContext from '../contexts/color';
    a
    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은 함수형 컴포넌트에서만 사용할 수 있다는 점에 주의하세요. 클래스형 컴포넌트에서는 Hook를 사용할 수 없습니다.

    15.4.2 static contexType 사용하기

    클래스형 컴포넌트에서 Context를 좀 더 쉽게 사용하고 싶다면 static contextType을 정의하는 방법이 있습니다. SelectColors 컴포넌트를 다음과 같이 클래스형으로 변환해 보세요. 그리고 Consumer 쪽 코드는 일단 제거해 주세요.

    components/SelectColors.js
    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: "24px",
                    height: "24px;",
                    cursor: "pointer",
                  }}
                />
              ))}
            </div>
            <hr />
          </div>
        );
      }
    }
    
    export default SelectColors;

    그리고 클래스 상단에 static contextType 값을 지정해 주세요.

    components/SelectColor.js
    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: "24px",
                    height: "24px;",
                    cursor: "pointer",
                  }}
                />
              ))}
            </div>
            <hr />
          </div>
        );
      }
    }
    
    export default SelectColors;

    이렇게 해 주면 this.context를 조회했을 때 현재 Context의 value를 가리키게 됩니다. 만약 setColor를 호출하고 싶다면 this.context.setColor을 호출하면 되겠죠?

    컴포넌트를 다음과 같이 완성해 보세요.

    components/SelectColors.js
    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: "24px",
                    height: "24px",
                    cursor: "pointer",
                  }}
                />
              ))}
            </div>
            <hr />
          </div>
        );
      }
    }
    
    export default SelectColors;

    static contextType을 정의하면 클래스 메서드에서도 Context에 넣어 둔 함수를 호출할 수 있다는 장점이 있습니다. 단점이라면, 한 클래스에서 하나의 Context밖에 사용하지 못한다는 것입니다. 그러나 앞으로 새로운 컴포넌트를 작성할 때 클래스형으로 작성하는 일은 많지 않기 때문에 useContext를 사용하는 쪽을 권합니다.

    15.5 정리

    기존에는 컴포넌트 간에 상태를 교류해야 할 때 무조건 부모 -> 자식 흐름으로 props를 통해 전달해 주었는데요. 이제는 Context API를 통해 더욱 쉽게 상태를 교류할 수 있게 되었습니다.

    프로젝트 컴포넌트 구조가 꽤 간단하고 다루는 상태의 종류가 그다지 많지 않다면, 굳이 Context를 사용할 필요는 없습니다. 하지만 전역적으로 여기저기서 사용되고 있는 상태가 있고 컴포넌트의 개수가 많은 상황이라면, Context를 사용하는 것을 권장합니다.

    다음 장에서는 리덕스라는 상태 관리 라이브러리를 배워 보겠습니다. 이 라이브러리는 Context API 기반으로 만들어져 있으며, Conetxt API와 마찬가지로 전역 상태 관리를 도와줍니다. 리액트 v16.3에서 Conetxt API가 개선되기 전에는 주로 리덕스를 사용하여 전역 상태를 관리해 왔습니다. 단순한 전역 상태 관리라면 이번에 배운 Context API로 리덕스를 대체할 수도 있습니다. 하지만 리덕스는 더욱 향상된 성능과 미들웨어 기능, 강력한 개발자 도구, 코드의 높은 유지보수성을 제공하기 때문에 모든 상황에 대해 대체가 가능하지는 않습니다.

    댓글

Designed by Tistory.