All Articles

[Redux 1탄] Redux로 리액트 애플리케이션 상태 관리


Redux

효율적으로 상태 관리를 할 수 있는 라이브러리

스토어에 상태 정보를 가진 객체를 넣어 두고, 액션이 디스패치되었을 때, 리듀서 함수를 이용하여 상태를 변화시키는 것이 주요 역할 ! 상태가 변화될 때마다 스토어에 구독된 함수를 실행시킵니다.


프로젝트 만들기

1. 프로젝트 생성

$ create-react-app redux-counter

다음 프로젝트 디렉터리로 이동 후,

$ yarn add redux react-redux


2. 디렉터리 생성
  • actions : 액션 타입과 액션 생성자 파일을 저장합니다.

  • components : 컴포넌트의 뷰가 어떻게 생길지만 담당하는 프레젠테이셔널 컴포넌트를 저장합니다.

  • containers : 스토어에 있는 상태를 props로 받아 오는 컨테이너 컴포넌트들을 저장합니다.

  • reducers : 스토어의 기본 상태 값과 상태의 업데이트를 담당하는 리듀서 파일들을 저장합니다.

  • lib : 일부 컴포넌트에서 함께 사용되는 파일을 저장합니다.


3. Counter 컴포넌트 생성

src/components/Counter.js

import React from 'react';
import PropTypes from 'prop-types';
import './Counter.css';

const Counter = ({number, color, onIncrement, onDecrement, onSetColor}) => {
  return (
    <div>
      className="Counter"
      onClick={onIncrement}
      (...)
    </div>
  );
};

Counter.propTypes = {
  number: PropTypes.number,
  (...)
};

Counter.defaultProps = {
  number: 0,
  color: 'black',
  onIncrement: () => console.warn('onIncrement not defined'),
  (...)
};

export default Counter;

그리고 Counter.css 파일을 생성하여 스타일링 합니다.


4. 액션 함수 생성

먼저 액션은 객체입니다. 모든 액션 객체에는 type 값이 필수입니다. type은 액션 이름과도 같은데, 이 값들을 따로 파일로 만들어서 저장하면 관리하기가 편합니다.

actions 디렉터리에 ActionTypes.js 라는 디렉터리를 만들어서 상수를 선언합니다.


src/actions/ActionTypes.js

export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const SET_COLOR = 'SET_COLOR';

src/actions/index.js

import * as types from './ActionTypes';

export const increment = () => ({
  type: types.INCREMENT
});

(...)

5. 리듀서 생성

리듀서는 액션의 type에 따라 변화를 일으키는 함수입니다. 최초 변화를 일으키기 전 가지고 있어야 할 초기 상태를 정의해야 합니다.

reducers 디렉터리에 index.js 파일을 만들고, 액션 타입들을 불러온 후 초기 상태를 선언하세요.


src/reducers/index.js

import * as types from '../actions/ActionTypes';

const initialState = {
  color: 'black',
  number: 0
};

function counter(state = initialState, action) {
  switch (action.type) {
    case types.INCREMENT:
      return {
        ...state,
        number: state.number + 1
      };
    case types.DECREMENT:
      return {
        ...state,
        number: state.number - 1
      };
    case types.SET_COLOR:
      return {
        ...state,
        color: action.color
      };
    default:
      return state;
  }
};

export default counter;

6. 스토어 생성

리덕스에서 createStore를 불러와 해당 함수에 우리가 만든 리듀서를 파라미터로 넣어 스토어를 생성해 보세요.


src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './containers/App';
import './index.css';

import { createStore } from 'redux';
import reducers from './reducers';

const store = createStore(reducers);

ReactDOM.render(<App />, document.getElementById('root'));

7. Provider 컴포넌트로 리액트 앱에 store 연동

Provider는 react-redux 라이브러리에 내장된 리액트 애플리케이션에 손쉽게 스토어를 연동할 수 있도록 도와주는 컴포넌트입니다.

이 컴포넌트를 불러온 후, 연동할 프로젝트의 최상위 컴포넌트(이 프로젝트에서는 App 컴포넌트)를 감싸고, Provider 컴포넌트의 props로 store를 넣어 주면 됩니다.


src/index.js

(...)
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

8. CounterContainer 컴포넌트 생성

컨테이너 컴포넌트에는 스토어가 연동되어 있습니다. react-redux 라이브러리의 connect 함수를 사용하여 컴포넌트를 스토어에 연결시킵니다.

connect([mapStateToProps], [mapDispatchToProps], [mergeProps])

이 파라미터들은 함수 형태며, 컴포넌트에서 사용할 props를 반환합니다.

  • mapStateToProps : store.getState() 결과 값인 state를 파라미터로 받아 컴포넌트의 props로 사용할 객체를 반환합니다.

  • mapDispatchToProps : dispatch를 파라미터로 받아 액션을 디스패치하는 함수들을 객체 안에 넣어서 반환합니다.

  • mergeProps : state와 dispatch가 동시에 필요한 함수를 props로 전달해야 할 때 사용하는데, 일반적으로는 잘 사용하지 않습니다.


src/containers/CounterContainer.js

import Counter from '../components/Counter';
import * as actions from '../actions';
import { connect } from 'react-redux';

export function getRandomColor() {
  const colors = [
    '#495057',
    ...
  ];

  const random = Math.floor(Math.random() * 13);

  return colors[random];
}

// store 안의 state 값을 props로 연결합니다.
const mapStateToProps = (state) => ({
  color: state.color,
  number: state.number
});

/* 액션 생성 함수를 사용하여 액션을 생성하고,
   해당 액션을 dispatch하는 함수를 만든 후 이를 props로 연결합니다.
*/
const mapDispatchToProps = (dispatch) => ({
  onIncrement: () => dispatch(actions.increment()),
  onDecrement: () => dispatch(actions.decrement()),
  onSetColor: () => {
    const color = getRandomColor();
    dispatch(actions.setColor(color));
  }
});

// Counter 컴포넌트를 애플리케이션의 데이터 레이어와 묶는 역할을 합니다.
const CounterContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter);

export default CounterContainer;

이렇게 하면 mapStateToProps의 color, number 값과 mapDispatchToProps의 onIncrement, onDecrement, onSetColor 값이 Counter 컴포넌트의 props로 들어갑니다.

이렇게 리덕스와 연동된 컴포넌트를 CounterContainer 안에 담아 이를 내보낸 후, App 컴포넌트에서 CounterContainer 컴포넌트를 불러와 렌더링하세요.

src/containers/App.js

import React, { Component } from 'react';
import CounterContainer from './CounterContainer';

class App extends Component {
  render() {
    return (
      <div>
        <CounterContainer />
      </div>
    );
  }
}


서브 리듀서 생성

위에 예제에서 만든 리듀서는 색상과 숫자를 한 객체 안에 넣어서 관리했습니다.

이번에는 이 리듀서를 서브 리듀서 두 개로 나누어 파일을 따로 분리시킨 후, combineReducers로 다시 합쳐 루트 리듀서를 만들어 보겠습니다.

우선 reducers 디렉터리에 color.js와 number.js 파일을 만드세요.

src/reducers/color.js

import * as types from '../actions/ActionTypes';

const initialState = {
  color: 'black'
};

const color = (state = initialState, action) => {
  switch(action.type) {
    case types.SET_COLOR:
      return {
        color: action.color
      };
    default:
      return state;
  }
}

export default color;

src/reducers/number.js

import * as types from '../actions/ActionTypes';

const initialState = {
  number: 0
};

const number = (state = initialState, action) => {
  switch(action.type) {
    case types.INCREMENT:
      return {
        number: state.number + 1
      };
    case types.DECREMENT:
      return {
        number: state.number - 1
      };
    default:
      return state;
  }
}

export default number;

서브 리듀서를 만든 후에는 이를 통합시키는 루트 리듀서를 만들어야 합니다.

color와 number 리듀서를 불러온 뒤, redux 라이브러리의 combineReducers로 리듀서를 합쳐 주고, 내보내기 합니다.

src/reducers/index.js

import number from "./number";
import color from "./color";

import { combineReducers } from 'redux';

/*
서브 리듀서들을 하나로 합칩니다.
combineReducers를 실행하고 나면, 나중에 store 형태를
파라미터로 전달한 객체 모양대로 만듭니다.
지금은 다음과 같이 만듭니다.

{
  numberData: {
    number: 0
  },
  colorData: {
    color: 'black'
  }
}
*/

const reducers = combineReducers({
  numberData: number,
  colorData: color
});

export default reducers;

combineReducers를 호출할 때는 객체를 파라미터로 전달하는데, 이 객체 구조에 따라 합친 리듀서 상태 구조를 정의한니다.

그 다음에는 CounterContainer 컴포넌트의 mapStateToProps를 조금 수정하여 코드가 작동하게 만듭니다.

src/containers/CounterContainer.js

(...)
const mapStateToProps = (state) => ({
  color: state.colorData.color,
  number: state.numberData.number
});
(...)


리덕스 개발자 도구 사용

1. 확장 프로그램 설치

크롬 웹 스토어(https://chrome.google.com/webstore/) 에서 Redux DevTools를 검색하여 크롬에 추가하세요.

이 확장 프로그램을 설치하고 크롬 개발자 도구를 열면 Redux 탭이 나타납니다.

2. 프로젝트에서 개발자 도구 설정

src 디렉터리의 index.js 파일을 열어 스토어를 생성하는 코드를 다음과 같이 수정하세요.

src/index.js - 스토어 생성 코드

const store = createStore(reducers, window.devToolsExtension && window.devToolsExtension());

이 도구를 사용하면 현재 리덕스 상태는 어떤지, 방금 디스패치한 액션은 무엇인지, 액션으로 어떤 값을 바꾸었는지 확인할 수 있습니다.