게시글

5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭
강좌8 - React - 8년 전 등록 / 6년 전 수정

리덕스 패턴(Redux pattern), react-redux

안녕하세요. 이번 시간에는 리덕스 패턴에 대해 알아보겠습니다. 리덕스 패턴은 MVC 패턴을 대체하기 위해서 페이스북이 사용한 Flux 패턴을 살짝 바꾼 겁니다. 대규모 애플리케이션에 적합하다고 나와있는데 제가 제 블로그에 적용해본 결과, 소규모 애플리케이션에도 괜찮은 것 같습니다.

View -> Action -> Dispatcher -> Store(Middleware -> Reducer) -> View

undefined

리덕스 패턴은 위와같이 데이터의 흐름이 갑니다. 한 방향으로만 흐르기 때문에 데이터의 흐름을 예측하기 쉽습니다. 그래서 관리하기도 좋고요. 특히 함수형 프로그래밍을 따르기 때문에 데이터가 불변하여 예측하기도 쉽고, 이전 상태로 되돌리기도 쉽습니다.

예를 하나 들어보겠습니다. 로그인 버튼을 눌러보겠습니다. View는 로그인 폼이 있는 화면이겠죠? 폼에 정보를 넣고 로그인 버튼을 누릅니다. 폼에는 미리 로그인 Action을 연결해두었습니다. Action은 그냥 로그인이라는 동작을 정의해둔 겁니다. 그리고 Dispatcher을 통해서 Action에서 Store로 데이터가 넘어갑니다. Dispatcher는 아까 정의해 둔 로그인 Action을 실행하는 부분입니다. Store에는 Middleware과 Reducer이 있습니다. Reducer은 데이터를 처리하는 부분입니다. Middleware는 Reducer에 가기전 Action을 조작하는 겁니다. 그리고 처리된 데이터를 다시 View로 넘겨줍니다.

State

리덕스는 모든 것을 state로 관리합니다. 위에서 말한 데이터가 state입니다. React의 state와는 좀 다릅니다. 웹앱 전체의 상태를 관리하거든요. 제 홈페이지를 예를 들면 포스트나 유저 정보가 모두 state입니다. 서버로부터 불러와 state에 저장하는 거죠. 그리고 각각의 컴포넌트는 리덕스에 저장된 state를 읽어와 사용합니다. 유저 정보를 다음과 같은 세 가지 state로 표현할 수 있습니다.

// 로그아웃
{
  fetchingUpdate: false,
  isLoggedIn: true,
  user: {},
}
// 로그인 시도중
{
  fetchingUpdate: true,
  isLoggedIn: true,
  user: {},
}
// 로그인
{
  fetchingUpdate: false,
  isLoggedIn: true,
  user: { 'name': 'ZeroCho' },
}

이렇게 객체로 표현된 state만 조작하면 View는 알아서 바뀝니다.

react-redux

리액트에서 리덕스 패턴을 사용할 수 있게 해주는 react-redux 패키지가 있습니다. redux 패키지도 같이 설치해주어야 합니다. 로그인 페이지를 예로 들어보겠습니다.

npm install redux react-redux

connect 함수를 써서 리덕스 패턴을 사용하는 컴포넌트를 만듭니다.

container/Login.jsx

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { login } from '../action/user.js';

class Login extends Component {
  static propTypes = {
    user: PropTypes.objectOf(PropTypes.any).isRequired,
    dispatch: PropTypes.func.isRequired,
  };

  handleSubmit = (e) => {
    e.preventDefault();
    const { dispatch } = this.props;
    const id = this.id.value;
    const pw = this.password.value;
    dispatch(login(id, pw));
  };

  render() {
    const { user } = this.props;
    return (
      user.isLoggedIn
        ? <div>로그인 성공</div>
        :
        <form onSubmit={this.handleSubmit}>
          <label>
            <span>아이디</span>
            <input ref={(ref) => { this.id = ref; }} />
          </label>
          <label>
            <span>비밀번호</span>
            <input type="password" ref={(ref) => { this.password = ref; }} />
          </label>
        </form>
    );
  }
}

function mapStateToProps(state) {
  return { user: state.user }
}

export default connect(mapStateToProps)(Login);

다른 것은 보통 컴포넌트와 같은데 리덕스로부터 불러온 connect 함수로 컴포넌트를 한 번 감싸주었습니다. 그리고 앞에 mapStateToProps도 연결되어 있고요. 이 부분이 redux와 react가 소통하는 부분입니다. redux의 state가 mapStateToProps 함수를 통해 React의 props로 전달됩니다. (Store -> View 부분입니다. Store에서 바뀐 정보가 View로 가는 거죠.) 이제 Action으로 로그인하는 부분을 만듭니다.

mapStateToProps 함수를 보면 state.user(리덕스의 state)을 user라는 props로 연결한 것을 볼 수 있습니다. user.isLoggedIn은 사실 state.user.isLoggedIn과 같습니다. React에서는 props가 업데이트되면 자동으로 컴포넌트가 바뀌기 때문에 위의 경우에서도 유저 로그인 여부에 따라 자동으로 업데이트됩니다.

action/user.js

export const LOGIN = 'LOGIN';
export const LOGIN_REQUEST = 'LOGIN_REQUEST';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAILURE = 'LOGIN_FAILURE';

export const login = (id, password) => {
  return {
    type: LOGIN,
    promise: { method: 'post', url: '/login', data: { id, password } }
  };
};

이렇게 login이라는 Action을 만들었습니다. 함수 위의 대문자로 써져 있는 것들은 Action의 이름들을 뜻합니다. login 함수를 dispatch하는 순간 Store에게 Action이 전달됩니다. 처음 Login 컴포넌트의 handleSubmit 안에 dispatch하는 부분이 들어있습니다. 폼을 submit하면 dispatch 메소드(Dispatcher)가 실행되는 거죠.

Store로는 로그인을 처리하는 스토어를 만들어둡니다. Store 안에 리덕스 패턴의 꽃인 reducer가 들어갑니다. reducer을 먼저 만들고 Store에 연결해줍니다.

reducer/user.js

import { combineReducers } from 'redux';
import { login, LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_FAILURE } from '../action/user.js';

const defaultState = {
  isLoggedIn: false,
  fetchingUpdate: false,
  user: {}
};

const userReducer = (state = defaultState, action) => {
  switch (action.type) {
    case LOGIN_REQUEST:
      return {
        ...state,
        fetchingUpdate: true
      };
    case LOGIN_SUCCESS:
      return {
        ...state,
        fetchingUpdate: false,
        isLoggedIn: true,
        user: action.result,
      };
    case LOGIN_FAILURE:
      return {
        ...state,
        fetchingUpdate: false
      };
  }
};

export default combineReducers({
  user: userReducer
});

Reducer는 아까 만든 Action별로 어떻게 state를 바꿀 지 결정하는 부분입니다. 한 가지 주의할 점은 반드시 새로운 객체를 반환해야합니다. 위의 코드에서도 { ...state }를 통해 먼저 기존 state를 복사했습니다. 이렇게 새로운 객체를 만들어야 현재와 이전 상태가 구분되기 때문에 이전 상태로 쉽게 되돌릴 수 있습니다.

defaultState가 바로 이 로그인 앱의 기본 state입니다. 여러분은 Reducer에서 이 state만 조작해서 정보를 표현해야 합니다. 어차피 Store과 View는 연결되어 있기 때문에 state만 바꾸면 알아서 View도 바뀝니다. LOGIN_REQUEST Action이 발생하면, fetchingUpdate를 true로 바꿔줍니다. 그 후 LOGIN_SUCCESS Action이 발생하면, fetchingUpdate를 false, isLoggedIn을 true로 바꾸고, user에는 서버로부터 받은 유저 정보(action.result)를 넣어줍니다.

Reducer는 여러 개를 사용할 수 있기 때문에 redux 패키지로부터 combineReducers 함수를 불러와 합쳐줍니다. 실제로 사용자에 관한 액션은 사용자 리듀서, 포스트에 관한 액션은 포스트 리듀서 등으로 분리하는 게 관리하기 쉽습니다. 어차피 마지막에 combineReducers로 합치면 되니까요.

store/configureStore.js

import { createStore, applyMiddleware, compose } from 'redux';
import reducer from '../reducer/user.js';
import promiseMiddleware from '../middleware/promiseMiddleware.js';

export default function(initialState) {
  const enhancer = compose(applyMiddleware(promiseMiddleware));
  return createStore(reducer, initialState, enhancer);
}

위의 configureStore 파일은 reducer을 Store과 연결할 때 필요합니다. 그런데 Store는 Node.js처럼 미들웨어를 사용할 수 있습니다. Store에 Action이 넘어갈 때마다 미들웨어를 거칩니다. Action에 만들어 둔 promise 옵션을 처리할 미들웨어를 만들겠습니다. Reducer는 기본적으로 promise 처리를 하지 못하기 때문에 직접 구현해주어야 합니다. 저는 ajax를 전송하는 데 axios를 쓰기 때문에 그걸로 하겠습니다. Action에 있는 promise를 axios로 전송하도록 코딩해줍니다.

npm install axios

middleware/promiseMiddleware.js

import axios from 'axios';

export default () => {
  return next => action => {
    const { promise, type, ...rest } = action;
    next({ ...rest, type: `${type}_REQUEST` });
    return axios({
      method: promise.method,
      url: promise.url,
      data: promise.data
    })
      .then(result => {
        next({ ...rest, result, type: `${type}_SUCCESS` });
      })
      .catch(error => {
        next({ ...rest, error, type: `${type}_FAILURE` });
      });
  };
};

보면 처음에 login을 Dispatch할 때 LOGIN_REQUEST Action가 실행됩니다. 그 후, 전송에 성공하면 LOGIN_SUCCESS, 실패하면 LOGIN_FAILURE Action이 추가적으로 실행됩니다.

store에서 제공하는 dispatch 메소드로 Action을 Store로 넘깁니다. Store에서 View로는 props를 통해 데이터가 넘어갑니다. 아까 connect 함수로 연결한 것이 state를 자동으로 props로 바꿔줍니다. 로그인 버튼을 눌렀을 때 데이터의 흐름을 한 번 봅시다.

// 1단계
container/Login.jsx(View)에서 id와 password 전송
-> action/user.js(Action)을 바탕으로
-> Store에서 promiseMiddleware로 ajax 전송
-> LOGIN_REQUEST가 Dispatch됨
-> Reducer에서 state의 fetchingUpdate가 true로 변경
-> container/Login.jsx(View)에서 state 변경에 따른 점 처리
// 2단계
-> promiseMiddleware로 보낸 ajax 결과 도착
-> ajax 결과에 따라 LOGIN_SUCCESS 또는 LOGIN_FAIL Action이 추가적으로 Dispatch
-> Reducer에서 Action에 따라 state가 변화
-> state의 변화가 container/Login.jsx(View)에 반영

단계는 로그인 요청, 로그인 결과 처리 두 단계이지만, 충실하게 단방향 데이터 흐름을 따르고 있습니다. V -> A -> D -> S(M -> R) -> V에서 벗어난 게 없죠? 만약 이 과정에서 에러가 난다면 위의 순서대로 점검해보면 에러가 반드시 검출됩니다. 버그를 잡아내는 데는 정말 최적입니다.

참고로 실제 로그인 요청을 하려면 서버단이 구현되어 있어야 합니다. 서버단을 구현하는 강좌는 링크 입니다.

리덕스 패턴이 기존 MVC 패턴과 차이가 있기 때문에 처음에는 다소 진입장벽이 있습니다. 하지만 한 번 사용하면 빠져나올 수 없을 정도의 매력이 있습니다. 다음 시간에는 리덕스 패턴을 React에 적용할 때 흔히 사용하는 ContainerComponent의 차이를 알아보겠습니다.

조회수:
0
목록
투표로 게시글에 관해 피드백을 해주시면 게시글 수정 시 반영됩니다. 오류가 있다면 어떤 부분에 오류가 있는지도 알려주세요! 잘못된 정보가 퍼져나가지 않도록 도와주세요.
Copyright 2016- . 무단 전재 및 재배포 금지. 출처 표기 시 인용 가능.
5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭

댓글

2개의 댓글이 있습니다.
5년 전
안녕하세요
리덕스를 쓰면 subscribe 구독을 하는 경우는 언제죠?
todoApp 경우 보면 subscribe같은 코드는 보이지 않네요?
리덕스에서 알아서 해주는건가요?
5년 전
리덕스는 subscribe를 하지 않습니다~ react-redux가 그 부분을 담당합니다.
7년 전
제로님 강의 잘 보고 있습니다. 궁금한게 한가지 있습니다. 로그인 성공하고 데이터를 받아오고나서 render의 뷰 전체를 바꾸고 싶으면 어떻게 해야되는지요?
7년 전
user.isLoggedIn 보이시나요? 그게 true면 로그인된 상황이고 아니면 로그인 안 된 상황이라서 분기처리 하시면 됩니다. 각각의 상황 다른 뷰를 리턴하세요