본문 바로가기

Frontend/React

2021-05-20 :: Context API

Context API는 리액트 프로젝트에서 전역적으로 사용할 데이터가 있을 때 유용한 기능이다. 이를테면 사용자 로그인 정보, 어플리케이션 환경 설정, 테마 등이 있다.

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

리액트 어플리케이션에서 여기저기에 필요한 데이터는 보통 최상위 컴포넌트인 App의 state에 넣어서 관리한다. 그리고 최상위 컴포넌트에서 해당 데이터를 사용하는 컴포넌트 까지 props형태로 전달해주는데 그 경로가 복잡해지는 경우 관리하기가 매우 복잡하다. 이런 경우 Context API를 사용하면 Context를 만들어 단 한 번에 원하는 값을 받아와서 사용이 가능하다.

Context API 사용법 익히기

src 디렉터리에 contexts 디렉터리를 만든 뒤 그 안에 color.js라는 파일을 만든다. 다음 파일을 생성한다.

contexts/color.js

import {createContext} from 'react';

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

export default ColorContext;

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

Consumer 사용하기

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

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

더보기
import React from 'react';

const RenderPropsSample = ({children}) => {
   return <div>결과: {children(5)}</div>
};

export default RenderPropsSample;

위와 같은 컴포넌트가 있다면 추후 사용할 때 다음과 같이 사용할 수 있다.

<RenderPropsSample> {value => 2*value} </RenderPropsSample>

Provider

provider를 사용하면 Context의 value를 변경할 수 있다. App 컴포넌트를 다음과 같이 수정해보자. 

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

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

export default App;

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

동적 Context 사용하기

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

context 파일 수정하기

Context의 value에는 상태 값 뿐만 아니라 함수를 전달할 수도 있다.

contexts/color.js

import React, {createContext, useState} from 'react';

const ColorContext = createContext({
   state: {color: 'black', subcolor:'red'},
   action: {
      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;

export {ColorProvider, ColorConsumer};

export default ColorContext;

위 파일에서 ColorProvider라는 컴포넌트를 새로 작성해 주었다. 그리고 그 컴포넌트에서는 ColorContext.Provider를 렌더링하고 있다. 이 Provider의 value에는 상태는 state로, 업데이트 함수는 actions로 묶어서 전달하고 있다.

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

색상 선택 컴포넌트 만들기

이번에는 context의 action에 넣어 준 함수를 호출하는 컴포넌트를 만들어 보겠다.

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: '24px',
                           height: '24px',
                           cursor: 'pointer'
                        }}
                        onClick={()=>actions.setColor(color)}
                        onContextmenu={e=>{
                           e.preventDefault();
                           actions.setSubcolor(color);
                        }}
                     />
                  ))}
               </div>
            )}
         </ColorConsumer>
         <hr/>
      </div>    
   );
};

export default SelectColors;

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

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

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

useContext Hook 사용하기

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

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은 함수형 컴포넌트에서만 사용할 수 있다는 점에 주의하자.