[React] Redux의 개념과 활용

2019. 5. 15. 22:46Web/React.js

[React] Redux의 개념과 활용

1. Redux 개념

1) Redux의 필요성

리액트에서 상태를 더 효율적으로 관리하는 데 사용하는 상태 관리 라이브러리

위와 같이 컴포넌트가 구성되어있다고 가정하자.

만약, App 컴포넌트에서 데이터 상태(state)에 변화가 생겨서 state를 업데이트하면 App 컴포넌트가 리렌더링 되고,
리액트 특성상 하위 컴포넌트도 모두 리렌더링 된다.

그렇기 때문에 TodoInput만 업데이트하길 원해도 TodoList도 함께 리렌더링 되는데, 이런 경우는 TodoList 컴포넌트에 shouldComponentUpdate를 구현하는 등 방지 할 수 있는 방법이 있긴 하다.

하지만, 프로젝트가 더 복잡해진다면 이런 방법도 어려워질 것이고, 아래와 같은 비효율적인 면이 있다.

1) 자식컴포넌트에 props를 전달할 때, 여러 컴포넌트를 거쳐서 props를 전달해주어야 한다. (불필요한 props 갯수가 증가)
2) 상위 컴포넌트에 상태 관리 로직이 추가되기 때문에 코드 길이가 길어진다.
3) 1, 2 때문에 당연히 상태 추적이나 디버깅이 어려워진다.

2) Redux란 무엇인가?

리덕스는 쉽게 설명하면 상태 관리의 로직을 컴포넌트 밖에서 처리하는 것이다.

  • 스토어: 애플리케이션의 상태 값들을 저장하고 있다.
  • 액션: 상태 변화를 일으킬 때 참조하는 객체
  • 디스패치: 액션을 스토어에 전달하는 과정
  • 리듀서: 상태를 변화시키는 로직이 있는 함수
  • 구독: 스토어 값이 필요한 컴포넌트는 스토어를 구독한다.

2. Redux 활용

1) immutable.js

  • 자바스크립트에서 불변성 데이터를 다룰 수 있도록 도와주는 라이브러리

  • Map, List, fromJS, toJS ...

    • Map : 객체 대신 사용하는 데이터 구조 ( 자바스크립트에 내장된 Map과는 다른 개념 )

    • List : 배열 대신 사용하는 데이터 구조 (map, filter, sort, push, pop 함수 내장 → 기존 List 변경x, 새로운 List 생성)

    • fromJS : immutable 데이터를 만들기 위해서 Map으로 감싸야 하는 부분을 없애주는 역할

Map만 사용한 경우 fromJS를 사용한 경우
const data = Map({
a: 1,
b: 2,
c: Map({
    d: 3,
    e: 4,
    f: 5
  })
});
const data = fromJS({
a: 1,
b: 2,
c: {
    d: 3,
    e: 4,
    f: 5
  }
});

    • toJS : immutable 데이터를 자바스크립트 객체로 변환

Map과 fromJS, toJS 예제

const { Map, fromJS } = Immutable;

const data = fromJS({
    a: 1,
    b: 2,
    c: {
        d: 3,
        e: 4,
        f: 5
    }
});

/* 특정 키의 값 불러오기/설정 */
data.get('a'); // 1
const newData = data.set('a', 4); // newData === data => false!!

/* 깊숙이 위치하는 값 불러오기/수정 */
data.getIn(['c', 'd']); // 3
const newData = data.set(['c', 'd'], 10); // newData === data => false!!

const deserialized = data.toJS();
console.log(deserialized);
// { a: 1, b: 2, c: { d: 3, e: 4 } }

List와 FromJS, toJS 예제

const { List } = immutable;

const list = List([
    Map({ value: 1 }),
    Map({ value: 2 })
]);

// or

const list2 = fromJS([
    { value: 1 },
    { value: 2 }
])

/* 값 읽어오기 */
list.get(0); // Map({ value: 1 })
list.getIn([0, 'value']); // 1

/* 값 수정 */
const newList = list.set(0, Map({value: 10}))
const newList = list.setIn([0, 'value'], 10);
const newList = list.update(0, item => item.set('value', item.get('value') * 5))
/* 기존 값 참고해야 할 때.. */
const newList = list.setIn([0, 'value'], list.getIn([0, 'value'] * 5));

/* 아이템 추가 */
const newList = list.push(Map({value: 3})) // 맨 뒤에 추가
const newList = list.unshift(Map({value: 0})); // 맨 앞에 추가

/* 아이템 제거 */
const newList = list.delete(1); // 인덱스 1인 아이템 제거
const newList = list.pop(); // 마지막 아이템 제거

/* List 크기 가져오기 */
console.log(list.size);
/* 비어있는지 확인 */
list.isEmpty()

2) Ducks 파일 구조

리덕스에서 사용하는 파일들은 일반적으로 액션 타입, 액션 생성 함수, 리듀서로 분리하여 관리하는데, 액션 하나 추가 될 때마다 세 개의 파일을 수정해야 하는 점이 불편하다고 느껴서,
액션 타입, 액션 생성 함수, 리듀서를 모두 한 파일에서 모듈화하여 관리하는 방식Ducks 파일 구조가 생김

// 액션 타입
const CREATE = 'my-app/todos/CREATE';
const REMOVE = 'my-app/todos/REMOVE';
const TOGGLE = 'my-app/todos/TOGGLE';

// 액션 생성 함수
export const create = (todo) => ({
    type: CREATE,
    todo,
});

export const remove = (id) => ({
    type: REMOVE,
    id
});

export const toggle = (id) => ({
    type: TOGGLE,
    id
});

const initialState = {
    // 초기 상태...
}

// 리듀서
export default function reducer(state = initialState, action) {
    switch (action.type) {
        // 리듀서 관련 코드...
    }
}

3) redux-actions

  • 리덕스 액션들을 관리할 때 유용한 createAction과 handleActions 함수가 들어있는 라이브러리
import { createAction, handleActions } from 'redux-actions';

/* createAction을 이용한 액션 생성 자동화 
 * 위의 Duck 파일 구조의 액션 생성 함수 부분을 아래와 같이 작성 가능
 * 액션이 갖고 있을 수 있는 정보의 이름을 payload 값으로 통일
*/
export const create = createAction(types.CREATE);
export const remove = createAction(types.REMOVE);
export const toggle = createAction(types.TOGGLE);
export const increment = createAction(types.INCREMENT);
export const decrement = createAction(types,DECREMENT);

remove({id: 3});
/* 결과:
    {
        type: 'REMOVE',
        payload: {
            id: 3
        }
    }
*/


/* switch 문 대신 handleActions 사용
 * switch 문을 사용하면 scope가 리듀서 함수이기 때문에 서로 다른 case에서 let이나 const 사용 시,
 * 같은 이름이 중첩되어 있으면 오류 발생
 * handleActions(reducerMap, initialState)
*/
const reducer = handleActions({
    INCREMENT: (state, action) => ({
        counter: state.counter + action.payload
    }),

    DECREMENT: (state, action) => ({
        counter: state.counter = action.payload
    })
}, {counter: 0})

4) 비동기 처리 ( Axios + redux-pender)

  • Axios

    ...
    import axios from 'axios';
    
    function getPostAPI(postId) {
        return axios.get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
    }
  • redux-pender

    • Promise 기반 액션들을 관리하는 미들웨어가 포함되어 있는 라이브러리
    • 액션 객체 안에 payload가 Promise 형태라면 시작하기 전, 완료 또는 실패를 했을 때 뒤에 PENDING, SUCCESS, FAILURE 접미사를 붙인다.
    // redux-pender 미들웨어 적용
    // src/store.js
    ...
    import penderMiddleware from 'redux-pender';
    
    ...
    const store createStore(modules, applyMiddleware(logger, penderMiddleware()))
    
    export default store;
    // 리듀서 설정
    // src/modules/index.js
    ...
    import { penderReducer } from 'redux-pender';
    
    export default combineReducers({
        conter,
        post,
        pender: penderReducer
    })
    • 요청 상태에 따라 아래와 같이 리듀서가 상태를 관리한다.

    • pender 리듀서가 액션 이름에 따라서 자동으로 상태 변경

      {
          pending: {
              'ACTION_NAME': false
          },
          success: {
              'ACTION_NAME': false
          },
          failure: {
              'ACTION_NAME': false
          }
      }
    • redux-pender 적용 코드 예제

      // action module 정의 파일
      ...
      function getPostAPI(postId) {
          return axios.get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
      }
      
      const GET_POST = 'GET_POST';
      
      /* redux-pender의 액션 구조는 Flux standard action(https://github.com/acdlite/flux-standard-action)을 따르기 때문에, createAction으로 액션 생성 가능
      두 번째로 들어가는 파라미터는 Promise를 반환하는 함수여야 한다.
      */
      export const getPost = createAction(GET_POST, getPostAPI);
      
      const initialState = {
          // 이 곳에는 요청이 진행 중인지, 오류가 발생했는지 여부에 대한 state는 정의할 필요x
          // penderReducer가 담당
          data: {
              title: '',
              body: ''
          }
      }
      
      export default handleActions({
          ...pender({
              type: GET_POST,    // type이 주어지면 이 type에 접미사를 붙인
                             // 액션 핸들러들이 담긴 객체를 만듭니다.
              /* 요청 중일 때와 실패했을 때 추가로 해야 할 작업이 있다면
               * 이렇게 onPending과 onFailure를 추가하면 됩니다.
               * onPending: (state, action) => state,
               * onFailure: (state, action) => state
              */
              onSuccess: (state, action) => {
                  // 성공했을 때 해야 할 작업이 따로 없으면 이 함수 또한 생략해도 됩니다.
                  const { title, body } = action.payload.data;
                  return {
                      data: {
                          title,
                          body
                      }
                  }
              }
              // 함수를 생략했을 때 기본 값으로는 (state, action) => state를 설정합니다.
      
          })
      })
      
      // or
      
      const reducer = handleActions({
          // 다른 일반 액션들을 관리...
      }, initialState);
      
      export default applyPenders(reducer, [
          {
              type: GET_POST,
              onSuccess: (state, action) => {
                  // 성공했을 때 해야 할 작업이 따로 없으면 이 함수 또한 생략해도 됩니다.
                  const { title, body } = action.payload.data;
                  return {
                      data: {
                          title,
                          body
                      }
                  }
              }
          },
          /* 다른 pender 액션들
           * { type: GET_SOMETHING, onSuccess: (state, action) => ... },
           * { type: GET_SOMETHING, onSuccess: (state, action) => ... }
          */
      ]);
      // App.js (redux 구독 컴포넌트)
      ...
      export default connect(
          (state) => ({
              number: state.counter,
              post: state.post.data,
              loading: state.pender.pending['GET_POST'],    // penderReducer가 관리하는 상태
              error: state.pender.failure['GET_POST']        // penderReducer가 관리하는 상태
          }),
          (dispatch) => ({
              CounterActions: bindActionCreators(counterActions, dispatch),
              PostActions: bindActionCreators(postActions, dispatch)
          })
      )(App);