01.15 리덕스 편하게 사용하는 방법

React.js

01.15. 리덕스를 편하게 사용하는 방법

리덕스를 사용하여 멀티 카운터를 만들면서 불편했던 점이 있다.
액션을 만들 때마다 세 가지 파일 ( 액션 타입, 액션 생성 함수, 리듀서)을 수정해야한다는 점, 전개 연산자(...)와 slice 함수로 배열 내부의 아이템을 수정하는 데 가독성이 낮다는 점 등이다.

이런 불편한 점을 해결하여 리덕스를 사용하는 방법을 알아본다.

  • Immutable.js 를 이용한 상태 업데이트
  • Ducks 파일 구조
  • redux-actions를 이용하여 액션 생성 함수 구현

01.15.1. Immutable.js

Immutable.js 는 자바스크립트에서 불변성 데이터를 다룰 수 있게 도와준다.

01.15.1.1. 자바스크립트의 객체 불변성

객체 불변성을 이해하려면 간단한 자바스크립트 코드를 실행해 보아야한다. 크롬 웹 브라우저에서 개발자도구를 이용해 코드를 확인한다.
let a = 7;
let b = 7;

let object1 = {a:1, b:2};
let object2 = {a:1, b:2};
a값과 b값은 같다. 둘을 === 연산자로 비교해보면 당연히 true를 반환할 것이다.
하지만 object1과 object2가 같은 값을 가지더라도 서로 다른 객체이기 때문에 둘을 비교하면 false를 반환한다.
object1 === object2 
// false

let object3 = object1;
object1 === object3
// true

/* object3에 object1을 넣고 두 값을 비교하면 true를 반환한다. 
    obejct1과 object3은 같은 객체를 가르키기 때문이다.
 */

object3.c = 3;

object1 === object3
// true
object1
// Object { a: 1, b: 2, c: 3 }
리액트 컴포넌트는 state 또는 상위 컴포넌트에서 전달받은 props 값이 변할 때 리렌더링되는데, 배열이나 객체를 직접 수정한다면 내부 값을 수정했을지라도 레퍼런스가 가르키는 곳은 같기 때문에 똑같은 값으로 인식한다.
전개연사자(...)를 사용해 기존 값을 가진 새 객체 또는 배열을 만든 이유이다.
Immutable.js를 사용하면 객체나 배열의 값을 바꾸는 것이 간편해진다.
let object1 = Map({
    a: 1,
    b: 2,
    c: 3,
    d: Map({
        e: 4,
        f: Map({
            g: 5, 
            h: 6
        })
    })
});

let object2 = object1.setIn(['d','f','h'],10);

object1 === object2
// false

01.15.1.2. Map

Immutable의 Map은 객체 대신 사용하는 데이터 구조이다. 자바스크립트에 내장된 Map과는 다르다.

※ Map 사용

- fromJS
객체 내용을 네트워크에서 받아 오거나 전달받는 객체가 너무 복잡한 상태라면 일일이 내부까지 Map으로 만들기 힘들 것이다. 이때는 fromJS를 사용한다.
const data = fromJS({ a:1, b:2 });
- toJS
Immutable 객체를 일반 객체 형태로 변형하는 방법이다.
const deserialized = object1.toJS();
console.log(deserialized);
// {a: 1, b: 2}
- 특정 키의 값 불러오기
특정 키의 값을 불러올 때는 get
object.get('a'); // 1
- 깊이 위치한 값 불러오기
Map 내부에 Map 이 존재하고, 그 Map 안에 있는 키 값을 불러 올 때는 getIn
object1.getIn(['c','d']); // 3
- 값 설정
새 값을 설정할 때는 set. 데이터가 실제로 변하는 것이 아니라 변화를 적용한 새 Map 만드는 것이다.
const newData = object1.set('a',4);
newData === object1
// false
- 깊이 위치한 값 수정
깊이 위치한 값을 수정할 때는 setIn. 이때 내부에 있는 객체들도 Map 형태여야 사용할 수있다.
const newData = object1.setIn(['c','d'],10);
- 여러 값 동시 설정
여러 값을 동시에 설정해야 할 때는 mergeIn. 예를 들어 c값과 d값, c값과 e값을 동시에 바꾸어야할 때는 다음과 같이 입력한다.
const newData = object1.mergeIn(['c'], {d: 10, e: 10});
/* mergeIn 를 사용하면 c 안에 들어있는 f 값은 그대로 유지하면서 
   d 값과 e 값만 변경한다. 
   다른 방법은 setIn을 이용한다.
 */
const newData = object1.setIn(['c','d'],10)
                       .setIn(['c','e'],10);
- merge
값을 수정하는 방법으로 다른 방법으로 성능상으로 set을 여러번하는 것이 더 빠르기 때문에 사용하지 않을 예정이지만 알고는 있어야한다.
const newData = object1.merge({ a: 10, b: 10 });

01.15.1.2. List

List는 Immutable 데이터 구조로 배열 대신 사용한다. 
배열과 동일하게 map, filter, sort, push, pop 함수를 내장하고 있다. 이 내장 함수를 실행하면 List 자체를 변경하는 것이 아니라 새로운 List를 반환한다. 
또 리액트 컴포넌트는 List 데이터 구조와 호환되기 때문에 map 함수를 사용하여 데이터가 들어있는 List를 컴포넌트 List 로 변환하여 JSX 에서 보여주어도 제대로 렌더링된다.
※ List 사용
- 생성
const list = List([0,1,2,3,4]);

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

const list = fromJS([
    {a: 1},
    {a: 2},
]);
- 값 읽어오기
list.get(0);

list.getIn([0, 'a']);
- 수정
// 해당 요소를 통째로 바꾸고 싶을 때는 set.
const newList = list.set(0, Map({a: 10}));
// 해당 요소의 값을 변경하고 싶을 때는 setIn.
const newList = list.setIn([0, 'value'], 10);
// 다른 방법으로는 update.
const newList = list.update(0, item => item.set('a', item.get('a') * 10));
/* 값을 업데이트해야 하는데 기존 값을 참조해야할 때는 
   update를 사용하면 펺다. 
   첫번째 파라미터는 선택할 인덱스 값,
   두번째 파라미터는 선택한 아이템을 업데이트하는 함수이다.
 */
// update를 사용하지 않을 경우.
const newList = list.setIn([0, 'a'], list.getIn([0, 'a']*10));
- 추가
아이템을 추가할 때는 push. 이 함수를 사용한다고 해서 Array 처럼 기존 List 자체에 아이템을 추가하는 것이 아니라 새로운 List 를 만들어서 반환한다.
const newList = list.push(Map({a: 3}));
// 리스트 맨 뒤에 데이터를 추가할 때는 push. 
// 맨 앞에 데이터를 추가할 때는  unshift.
const newList = list.unshift(Map({a: 3}));
- 제거
const newList = list.delete(index);
// 마지막 아이템을 제거할 때는 pop
const newList = list.pop();
- 크기
list.size();
// list가 비어있는지 확인할 때는 isEmpty.
list.isEmpty();

01.15.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) {
        // 리듀서 관련 코드
    }
}
Ducks 구조에서는 파일 안에 액션타입, 액션 생성 함수, 리듀서를 한꺼번에 넣어서 관리하는데, 이를 모듈이라고 한다.

01.15.2.1. 규칙

Ducks 구조에서는 지켜야 할 규칙이 있다.
  • export default 를 이용하여 리듀서를 내보내야한다.
  • export 를 이용하여 액션 생성 함수를 내보내야한다.
  • 액션 타입 이름은 npm-module-or-app/reducer/ATCTION_TYPE 형식으로 만들어야한다.[라이브러리를 만들거나 애플리케이션을 여러 프로젝트로 나눈 것이 아니라면 맨 앞은 생략해도 된다.(예: counter/INCREMENT)]
  • 외부 리듀서에서 모듈의 액션 타입이 필요할 때는 액션 타입을 내보내도 된다.
Ducks 구조를 사용할 때는 이 규칙들을 준수해야한다.

01.15.3. redux-actions 를 이용한 액션 관리

redux-actions 패키지에는 리덕스 액션들을 관리할 때 유용한 createAction과 handleActions 함수가 있다.
yarn 을 통해 설치하고
$ yarn add redux-actions
import 를 통해 소스에 적용한다.
import {createAction, handleActions} from 'redux-actions';

01.15.3.1. createAction 을 이용한 액션 생성 자동화

리덕스에서 액션을 만들면 모든 액션에서 일일이 액션 생성자를 만드는 것이 번거로운 일이다.
export const increment = (index) => ({
    type: types.INCREMENT,
    index
});

export const decrement = (index) => ({
    type: types.DECREMENT,
    index
});
createAction 을 사용해 자동화할 수 있다.
export const increment = createAction(types.INCREMENT);
export const decrement = createAction(types.DECREMENT);

increment(3);

/* 결과:
   {
      type: 'INCREMENT',
      payload: 3
   }
 */
함수에 파라미터를 전달하면 payload 키에 파라미터로 받은 값을 넣어 객체를 생성한다. 파라미터가 여러 개일 경우 객체를 만들어 파라미터를 넣어주면 payload 에 객체가 설정된다.
/* 어떤 파라미터를 받는지 명시하지 않아 
    헷갈릴 경우 코드상으로 명시할 수 있다.
 */
export const setColor = createAction(types.SET_COLOR, ({index, color}) => ({index, color}));

01.15.3.2. switch 문 대신 handleActions 사용

리듀서에 switch문을 사용하여 액션 타입에 따라 다른 작업을 하도록 했다.
scope 를 리듀서 함수로 설정했기 때문에 서로다른 case 에서 let 이나 const 를 사용하여 변수를 선언하려고 할 때, 같은 이름이 중첩되어 있으면 오류가 발생한다. 그렇다고 case마다 함수를 정의하면 코드의 가독성이 떨어진다. handelActions 를 사용하면 이 문제를 해결할 수 있다.
const reducer = handleActions(
    // 첫 번째 파라미터는 액션에 따라 실행할 함수들을 가진 개체를 넣는다.
    {
        INCREMENT: (state, action) => ({
            counter: state.counter + action.payload
        }),
        DECREMENT: (state, action) => ({
            counter: state.counter - action.payload
        })
    }, 
    // 두 번째 파라미터는 상태의 기본 값(initialState)을 넣는다.
    {counter: 0}
);

위 세가지 방법을 사용하여 리덕스를 프로젝트에 적용했을 때 장점은 상태 관리를 하는 로직과 뷰에 관련된 로직을 완전히 다른 파일로 분리함으로써 프로젝트 가독성을 높이고 유지 보수를 하기도 쉽다는 것이다.

댓글

댓글 쓰기

이 블로그의 인기 게시물

01.11 리액트 컴포넌트 CSS 적용 ( CSS Module, Sass, styled-components )

01.9 React 컴포넌트 라이프사이클

01.7 React ref (DOM에 이름 달기)