본문 바로가기

React

3장. Immutable.js 익히기

 

https://velopert.com/3354

 

Redux 를 통한 React 어플리케이션 상태 관리 :: 3장. Immutable.js 익히기 | VELOPERT.LOG

이 튜토리얼은 5개의 포스트로 나뉘어진 이어지는 강좌입니다. 목차를 확인하시려면 여기를 참고하세요. 3장. Immutable.js 익히기 Immutable.js는 자바스크립트상에서 불변성의 데이터를 다루는것을 도와줍니다. 3장을 진행하기에 앞서, 우선 자바스크립트에서 객체의 불변성에 대해서 알아보겠습니다. 객체의 불변성 이를 이해하려면, 간단한 자바스크립트 코드들을 실행해보아야합니다. 크롬에서 개발자도구를 열고, 다음 코드를 입력해보세요: let a =

velopert.com

해당 블로그를 보고 공부하면서 정리한 글입니다.

 

객체의 불변성

let a = 7;
let b = 7;

let object1 = { a: 1, b: 2 };
let object2 = { a: 1, b: 2 };

object1과 object3 === 비교를 하면 결과는 무엇일까?

object1 === object2
// false

아래와 같이 하면 또 다른 결과를 얻을 수 있다.

let object3 = object1

object1 === object3
// true

 

하지만 이렇게 할당을 하면 한가지 문제가 있다.

같은 주소를 바라보기 때문에 하나의 객체를 수정하면 같이 수정이 된다.

object3.c = 3;

object1 === object3
// true
object1
// Object { a: 1, b: 2, c: 3 }

같은 주소를 바라보고 있기 때문에 수정을 하여도 항상 같은 값을 가진다.

 

하지만!

 

리액트 컴포넌트에서는 state 혹은 상위 컴포넌트에서 전달받은 props의 값이 변할 때 리랜더링을 해야 하는데, 만약에 객체를 직접 수정하면 내부의 값이 수정되더라도 래퍼런스가 가리키는 곳은 같기 때문에 똑같은 값으로 인식한다.

 

이러한 이슈 때문에, ... 을 사용해 기존의 값을 이용하여 새로운 객체를 만들어야 했다.

 

하지만 이 방법 때문에 간단한 작업도 상당히 복잡해 진다.

 

let object1 = {
    a: 1,
    b: 2,
    c: 3,
    d: {
        e: 4,
        f: { 
            g: 5,
            h: 6
        }
    }
};

// h값을 10으로 업데이트함
let object2 = {
    ...object,
    d: {
        ...object.d,
        f: {
            ...object.d.f,
            h: 10
        }
    }
}

이러한 작업을 간소화하기 위해 만들어진 것이

 

페이스북팀이 만든 라이브러리 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

직관적으로 객체를 수정할 수 있게 되었다.

 

Map

var Map = Immutable.Map;

var data = Map({
  a: 1,
  b: 2,
  c: Map({
    d: 3,
    e: 4,
    f: 5
  })
})

Map을 자바스크립트 객체로 변환하려면

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

다음과 같이 해주면 된다.

 

Map의 특정 값을 가져오는 방법은 2가지가 있다.

data.get('a'); // 1

data.getIn(['c', 'd']) // 3

값을 수정하려면 아래와 같이 하면 된다.

다만, data가 변하는게 아니고 새로운 Map이 생성된다는 것을 기억하자.

var newData = data.set('a', 4);

var newData = data.setIn(['c', 'd'], 10);

여러개의 값을 설정하려면 아래와 같이 하면 된다.

var newData = data.mergeIn(['c'], { d: 10, e: 10 });

var newData = data.setIn(['c', 'd'], 10);
                  .setIn(['c', 'e'], 10);
                  
var newData = data.merge({ a: 10, b: 10 })

하지만 merge 하는것 보다 set을 여러번하는게 성능상 더 좋다.

 

List

var List = Immutable.List;

var list = List([0,1,2,3,4]);

리스트에 아이템 추가하기

var newList = list.push(Map({value: 3}))

// 맨 앞에 추가하고 싶다면
var newList = list.unshift(Map({value: 0}))

리스트에 아이템 삭제하기

var newList = list.delete(1);

// 가장 마지막 제거
var newList = list.pop();

리덕스에서 사용하기

이제 실제 코드에 적용을 해보자.

import { Map, List } from immutable;

const initialState = Map({
    counters: List([
        Map({
            color: 'black',
            number: 0
        })
    ])
})

// 리듀서 함수를 정의합니다. 
function counter(state = initialState, action) {
    const counters = state.get('counters');

    switch(action.type) {
        // 카운터를 새로 추가합니다
        case types.CREATE:
            return state.set('counters', counters.push(Map({
                color: action.color,
                number: 0
            })))
        // slice 를 이용하여 맨 마지막 카운터를 제외시킵니다
        case types.REMOVE:
            return state.set('counters', counters.pop());

        // action.index 번째 카운터의 number 에 1 을 더합니다.
        case types.INCREMENT:
            return state.set('counters', counters.update(
                action.index, 
                (counter) => counter.set('number', counter.get('number') + 1))
            );

        // action.index 번째 카운터의 number 에 1 을 뺍니다
        case types.DECREMENT:
            return state.set('counters', counters.update(
                action.index, 
                (counter) => counter.set('number', counter.get('number') - 1))
            );

        // action.index 번째 카운터의 색상을 변경합니다
        case types.SET_COLOR:
            return state.set('counters', counters.update(
                action.index, 
                (counter) => counter.set('color', action.color))
            );
        default:
            return state;
    }
};

컴포넌트 수정하기

state.counters 대신 아래와 같이 수정을 하면 된다.

// store 안의 state 값을 props 로 연결해줍니다.
const mapStateToProps = (state) => ({
    counters: state.get('counters')
});

기존 배열 매핑 코드도 아래와 같이 수정을 하면 된다.

import React from 'react';
import Counter from './Counter';
import PropTypes from 'prop-types';
import { List } from 'immutable';

import './CounterList.css';

const CounterList = ({counters, onIncrement, onDecrement, onSetColor}) => {

    const counterList = counters.map(
        (counter, i) => (
            <Counter 
                key={i}
                index={i}
                {...counter.toJS()}
                onIncrement={onIncrement}
                onDecrement={onDecrement}
                onSetColor={onSetColor}
            />
        )
    );

    return (
        <div className="CounterList">
            {counterList}
        </div>
    );
};

CounterList.propTypes = {
    counters: PropTypes.instanceOf(List),
    onIncrement: PropTypes.func,
    onDecrement: PropTypes.func,
    onSetColor: PropTypes.func
};

CounterList.defaultProps = {
    counters: [],
    onIncrement: () => console.warn('onIncrement not defined'),
    onDecrement: () => console.warn('onDecrement not defined'),
    onSetColor: () => console.warn('onSetColor not defined')
}

export default CounterList;