WEB🔨/React

리액트를 다루는 기술 요약#9

최문경 블로그 2021. 6. 27. 19:36

해당 시리즈는 제목 그대로 김민준님의 '리액트를 다루는 기술'을 요약한 글입니다.

 

목차

15장 Context API

16장 리덕스 라이브러리 이해하기

 

 

15장 Context API

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

 

흐름

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

 

 

15.2 Context API 사용법 익히기

15.2.1 새 Context 만들기

// contexts/color.js
import { createContext } from 'react';

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

export default ColorContext;

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

 

 

15.2.2 Consumer 사용하기

// 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;

값을 조회할 때는 ColorContext 안에 들어 있는 Consumer라는 컴포넌트를 사용합니다. Consumer 사이에 중괄호를 열어서 그 안에 함수를 넣어주었는데 이러한 패턴을 Function as child, 혹은 Render Props라고 합니다.

 

 

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

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

export default App;

 

 

15.2.3 Provier

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

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

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

export default App;

createContext 함수를 만들 때 파라미터로 넣어준 기본값은 Provider를 사용하지 않았을 때만 사용되기 때문에 Provider는 사용했는데 value를 명시하지 않았다면 기본값을 사용하지 않아서 오류가 발생합니다.

 

 

15.3 동적 Context 사용하기

15.3.1 Context 파일 수정하기

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

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 { Consumer: ColorConsumer } = ColorContext;

export { ColorProvider, ColorConsumer };

export default ColorContext;

ColorProvider 컴포넌트의 value에는 상태는 state로, 업데이트 함수는 actions로 묶어서 전달

-> 나중에 다른 컴포넌트에서 Context의 값을 사용하기 편리함

 

추가로 createContext의 기본값은 Provier의 value에 넣는 객체의 형태와 일치시켜 주는 것이 좋음.

-> 가독성이 좋아지고 실수로 Provider를 사용하지 않았을 때 에러가 발생하지 않음.

 

 

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

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

export default App;
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;

 

 

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

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

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: '24px',
              height: '24px',
              cursor: 'pointer',
            }}
          />
        ))}
      </div>
      <hr />
    </div>
  );
};

export default SelectColors;
import React from 'react';
import ColorBox from './components/ColorBox';
import { ColorProvider } from './contexts/color';
import SelectColors from './components/SelectColors';

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

export default App;


좌클릭 -> 큰 사각형 색상 변경

우클릭 -> 작은 사각형 색상 변경하도록 구현해봅시다.

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;

 

 

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

15.4.1 useContext Hook 사용하기

리액트에 내장되어 있는 Hooks 중에서 useContext라는 Hook을 사용하면, 함수형 컴포넌트에서 Context를 아주 편하게 사용할 수 있습니다.

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;

 

 

15.4.2 static contextType 사용하기

( 생략 ) (클래스형 컴포넌트에서 사용하는 방법이라..)

 

추가 (패스트캠퍼스 강의에서 설명해주신 코드)

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

const MyContext = createContext('defaultValue');

function Child() {
  const text = useContext(MyContext);
  return <div>안녕하세요? {text}</div>;
}

function Parent() {
  return <Child />;
}

function GrandParent() {
  return <Parent />;
}

const ContextSample = () => {
  const [value, setValue] = useState(true);
  return (
    <MyContext.Provider value={value ? 'GOOD' : 'BAD'}>
      <GrandParent />
      <button onClick={() => setValue(!value)}>CLICK ME</button>
    </MyContext.Provider>
  );
};

export default ContextSample;

 

 

정리

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

다음 장에서는 Context API 기반으로 만들어진 리덕스라는 상태 관리 라이브러리를 배워보겠습니다. 단순한 전역 상태 관리라면 Context API로 리덕스를 대체할 수도 있지만 리덕스는 더욱 향상된 성능과 미들웨어 기능, 강력한 개발자 도구, 코드의 높은 유지 보수성을 제공하기 때문에 모든 상황에 대해 대체가 가능하지는 않습니다.

 

 

16장 리덕스 라이브러리 이해하기

리덕스는 가장 많이 사용하는 상태 관리 라이브러리입니다.

 

단순히 전역 상태 관리만 한다면 Context API를 사용하는 것만으로도 충분하지만 리덕스를 사용하면 상태를 더욱 체계적으로 관리할 수 있기 때문에 프로젝트의 규모가 클 경우에는 리덕스를 사용하는 편이 좋습니다.

 

흐름

1. 핵심 키워드 알아보기

2. Parcel로 프로젝트 구성하기

3. 토글 스위치와 카운터 구현하기

 

 

16.1 개념 미리 정리하기

1. 액션 객체

변화에 대한 정보를 가지고 있는 객체

반드시 type 필드를 가지고 있어야 함.

 

2. 액션 생성 함수

액션 객체를 만들어 주는 함수

 

3. 리듀서

변화를 일으키는 함수

 

4. 스토어

현재 애플리케이션의 상태와 리듀서, 그 외에 몇 가지 중요한 내장 함수 저장

한 개의 프로젝트는 단 하나의 스토어

 

5. 디스패치

리듀서를 실행시키는 함수

스토어의 내장 함수 중 하나.

 

6. 구독

subscribe 함수 안에 리스너 함수를 파라미터로 넣어서 호출해 주면, 액션이 디스패치되어 상태가 업데이트될 때마다 리스너 함수 호출.

스토어의 내장 함수 중 하나.

 

 

16.2 리액트 없이 쓰는 리덕스

리덕스는 리액트에 종속되는 라이브러리가 아닙니다. 리액트에서 사용하려고 만들어졌지만 실제로 다른 UI 라이브러리/프레임워크와 함께 사용할 수도 있습니다.

 

16.2.1 Parcel로 프로젝트 만들기

yarn global add parcel-bundler

mkdir vanilla-redux

cd vanilla-redux

yarn init -y

 

<html>
  <head>
    <link rel="stylesheet" href="index.css" />
  </head>
  <body>
    <div class="toggle"></div>
    <hr />
    <h1>0</h1>
    <button id="increase">+1</button>
    <button id="decrease">-1</button>
    <script src="./index.js"></script>
  </body>
</html>
.toggle {
  border: 2px solid black;
  width: 64px;
  height: 64px;
  border-radius: 32px;
  box-sizing: border-box;
}

.toggle.active {
  background: yellow;
}
import { createStore } from 'redux';

// DOM 레퍼런스 만들기
const divToggle = document.querySelector('.toggle');
const counter = document.querySelector('h1');
const btnIncrease = document.querySelector('#increase');
const btnDecrease = document.querySelector('#decrease');

// 액션 타입 정의
const TOGGLE_SWITCH = 'TOGGLE_SWITCH';
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';

// 액션 생성 함수 정의
const toggleSwitch = () => ({ type: TOGGLE_SWITCH });
const increase = (difference) => ({ type: INCREASE, difference });
const decrease = () => ({ type: DECREASE });

// 초깃값 설정
const initialState = {
  toggle: false,
  counter: 0,
};

// 리듀서 함수 정의
function reducer(state = initialState, action) {
  switch (action.type) {
    case TOGGLE_SWITCH:
      return {
        ...state,
        toggle: !state.toggle,
      };
    case INCREASE:
      return {
        ...state,
        counter: state.counter + action.difference,
      };
    case DECREASE:
      return {
        ...state,
        counter: state.counter - 1,
      };
    default:
      return state;
  }
}

// 스토어 만들기 (리듀서 함수를 파라미터로 넣어줘야함)
const store = createStore(reducer);

// render 함수 정의
const render = () => {
  const state = store.getState();

  if (state.toggle) {
    divToggle.classList.add('active');
  } else {
    divToggle.classList.remove('active');
  }

  counter.innerText = state.counter;
};

render();

// 구독하기 (스토어의 상태가 바뀔 때마다 render 함수가 호출)
store.subscribe(render);

// 액션 발생시키기
divToggle.onclick = () => {
  store.dispatch(toggleSwitch());
};
btnIncrease.onclick = () => {
  store.dispatch(increase(1));
};
btnDecrease.onclick = () => {
  store.dispatch(decrease());
};

parcel index.html

 

 

16.3 리덕스의 세 가지 규칙

1. 단일 스토어

여러 개의 스토어를 만들 수도 있지만, 상태 관리가 복잡해질 수 있으므로 권장하지 않습니다.

 

2. 읽기 전용 상태

state를 업데이트할 때도 불변성을 지켜주었던 것처럼 리덕스에서 상태를 업데이트할 때도 불변성을 유지해야함. (객체의 변화를 감지할 때 객체의 깊숙한 안쪽까지 비교하는 것이 아니라 겉핥기 식으로 비교하여 좋은 성능을 유지할 수 있음)

 

3. 리듀서는 순수한 함수

순수한 함수의 조건

  • 이전 상태와 액션 객체를 파라미터로 받는다.
  • 파라미터 외의 값에는 의존하면 안된다.
  • 이전 상태는 절대로 건드리지 않고, 변화를 준 상태 객체를 만들어서 반환한다.
  • 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과 값을 반환해야 한다.