[React] Context API와 전역 상태 이해하기

김유진·2022년 11월 13일
0

React

목록 보기
48/64

React로 앱을 제작하다 보면, 전역변수를 사용하지 않아서 불편할 때가 많다.
내가 지금 하고 있는 React Todo App 만들기 또한 여러 가지 컴포넌트를 쓰고 싶은데 App.js에다가 모든 정보를 다 몰아넣어 놓고 애플리케이션을 만들려다 보니까 랜더링이나 컴포넌트의 깊이 등등 신경써야 하는 부분이 많아서 불편한 점이 한두가지가 아니다. 이런 점을 개선하기 위하여 Context API를 사용할 수 있는데 ,이번 기회에 해당 API를 사용하여 보려고 한다.

먼저 context API를 사용하기 위해서 src 폴더에 구분할 수 있는 contexts 폴더를 생성하였다.

import { createContext } from 'react';

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

export default ColorContext;

이렇게 context 관련한 폴더를 생성할 수 있었고, 새로운 context를 생성하는 것이니까 createContext 함수를 사용하였다.

1. Consumer 사용하기

그럼 이러한 context를 사용하기 위해서는 어떻게 해야 할까?
component에 colorBox라는 이름을 가진 컴포넌트를 만들어보자.

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;

이렇게 코드를 작성할 수 있는데, ColorContext 안에 들어 있는 색상을 보여주는 것입니다.
이 때 색상을 props로 받아 오는 것이 아니라, ColorContext 안에 들어 있는 Consumer라는 컴포넌트를 통하여 색상을 조회할 수 있다.
Consumer라는 것은 잘 들어 있는 Context의 내용을 사용하기 때문에 이렇게 이름지은 게 아닐까 싶다.

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

2. Provider 사용하기

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

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의 기본값을 넣어 주었는데. 기본값은 Provider를 사용하지 않았을 때에만 사용됩니다.

만약 Provider를 사용하였는데, value를 명시하지 않았더라면 기본값을 사용하지 않기 때문에 오류가 발생하므로, Provider 사용시에는 꼭 value 명시해주기~

import ColorBox from "./components/ColorBox";
import ColorContext from './contexts/color';

const App = () => {
  return (
    <ColorContext.Provider>
      <div>
      <ColorBox/>
      </div>
    </ColorContext.Provider>
  )
}

export default App;

이렇게 쓰면 오류가 발생해요. 꼭 value 값 함께 써주기!

3. 동적 Context 사용하기

context의 value에는 무조건 고정된 상태 값만이 있는 것이 아니고, 함수를 전달해 줄 수도 있다.
기존에 작성했던 Colorcontext 코드를 수정해보자.

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

먼저 기존에 state만 존재하였던 ColorContext를 위와 같이 고쳐 주었다. actions를 추가하여서 어떤 동작을 하는지 보여주는 것이다. 그리고 ColorProvider라는 컴포넌트를 새로 작성하였다. ColorProvider라는 컴포넌트는 ColorContext.Provider를 랜더링하고 있다.

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>
    );
};

이 Provider의 value에는 상태는 state로, 업데이트 관련한 것은 actions로 묶어서 전달핟고 있다.

꼭 항상 묶어서 값을 전달해야 하는 것은 아니지만~ 이렇게 state와 actions를 따로 분리해주면 나중에 재사용할 때 편리하다는 점!!

추가로, createContext를 사용할 때 기본적으로 사용할 객체도 수정했다. createContext의 기본값은 실제로 Providervalue에 넣는 객체의 형태와 일치시키는 것이 좋다. 이렇게 하면 Context 코드 내부의 값이 어떻게 구성되어 있는지 확인하기도 편하고, 실수로 Provider를 사용하지 않을 때 리액트에서 에러가 발생하지 않기 때문이다 .이렇게 개선한 Context를 한번 반영해보자.

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

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

export default App;

기존에 <ColorContext.Provider> 이렇게 쓰던거를 위처럼 간단하게 작성할 수 있다.
그리고 color.js도 아래와 같이 변경 가능하다.

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

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

export default ColorBox;

Provider.Consumer라고 쓰던 코드를 위와 같이 깔끔하게 바꿀 수 있다. 그리고 안에 있는 함수 꼴도 바꾸어보자.

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 : '32px',
                            height : '32px',
                            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;

Context의 actions에 넣어 준 함수를 호출하는 컴포넌트를 만들어보겠습니다.

const colors = ['red', 'oragne', '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 : '24px',
                            height : '24px',
                            cursor : 'pointer'
                        }}
                    />
                ))}
            </div>
            <hr />
        </div>
    );
};

export default SelectColors;

이것을 App.js에 랜더링 시켜주면 이렇게 귀엽게 화면에서 색을 선택할 수 있게끔 색이 나열되어 있습니다.

색을 선택할 수 있는 목록을 만들어 보도록 하자.


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

const colors = ['red', 'oragne', '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>
            )}
                <hr />
            </ColorConsumer>
        </div>
    );
};

export default SelectColors;

이렇게 코드를 작성하게 되면 마우스 왼쪽 버튼을 눌렀을 때 설정되는 색은 color에 저장되고, 마우스 오른쪽 버튼을 눌렀을 때 기본적으로 복사 박스 같은 것이 드러나니까, preventDefault를 통하여 기존에 발생하는 이벤트를 예방할 수 있도록 하고, subColor를 설정하도록 한다.

4. Consumer 대신에 Hook이나 static contextType 사용하기

Context에 있는 값을 사용할 때, Consumer를 사용하지 않고 가져올 수 있는 방법에 대해서 고민해보자.

useContext Hook 사용하기

아주 편하게 context를 사용할 수 있는 방법 중의 하나이다.
Consumer 사용하던 부분을 다 지워주고!

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;

이전보다 훨씬 간결해졌다. chilren에 직접적으로 전달해주는 Render Props 패턴이 나는 개인적으로 불편하기 때문에 useContext를 자주 사용하곤 한다. 그런데 이것은 함수 컴포넌트에서밖에 사용할 수 없다.

기존에는 컴포넌트 간의 상태를 교류해야 할 때에는 무조건 부모 -> 자식 흐름으로 props를 통하여 전달하였다. 그런데 우리는 context API를 통하여 더욱 쉽게 상태를 교류할 수 있게 되었다.
전역적으로 사용되는 상태가 있고, 컴포넌트의 개수가 많은 상태라면 Context API를 사용하는 것이 맞다.

원래 상태관리의 정석은 Redux를 사용하는 것인데, 단순한 전역관리만 한다면 Context API로 리덕스를 대체할 수 있다. 하지만 리덕스는 성능이 더욱 좋고, 미들웨어 기능, 개발자 도구까지 있어서 유지보수가 좋아 리덕스가 좋을 때도 있다는 점을 기억하며 상황에 맞게 사용해야겠다

0개의 댓글