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를 반환한다.
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를 사용하면 객체나 배열의 값을 바꾸는 것이 간편해진다.
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를 사용한다.
※ Map 사용
- fromJS
객체 내용을 네트워크에서 받아 오거나 전달받는 객체가 너무 복잡한 상태라면 일일이 내부까지 Map으로 만들기 힘들 것이다. 이때는 fromJS를 사용한다.
const data = fromJS({ a:1, b:2 });
- toJS
Immutable 객체를 일반 객체 형태로 변형하는 방법이다.
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();
참고 - Immutable 공식 홈페이지
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 {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} );
위 세가지 방법을 사용하여 리덕스를 프로젝트에 적용했을 때 장점은 상태 관리를 하는 로직과 뷰에 관련된 로직을 완전히 다른 파일로 분리함으로써 프로젝트 가독성을 높이고 유지 보수를 하기도 쉽다는 것이다.
정리를 잘해주셨네요~^^*
답글삭제