immutable

Immutable JS

저희가 사용하는 React와 Redux에서는 immutablity(불변성)를 항상 강조하고 있습니다.
Immutable JS에 대해 설명하기전에 immutablity(불변성)에 대해 설명을 하고 넘어가겠습니다.

immutablity란?

불변성 - 말 그대로 변하지 않는다는 것 입니다.
※ 우선 리액트 코드를 예를 들어 불변성에 대해 설명하고, Immutable JS 가 필요한 이유에 대해 설명하겠습니다.

React는 state의 불변성을 유지해야 한다고 합니다. 그렇기 때문에
state를 변경할때는 필수로 setState를 사용하여 업데이트를 해야하고,
state 객체 값을 직접 수정하면 안됩니다.

코드로 보자면

1
2
3
4
5
6
7
8
9
10
this.state = {
userList: [
{
name: 'sim'
},
{
name: 'park'
}
]
};

리액트 state에 userList라는 배열이 있고, 그 배열에 name: ‘kim’을 추가하거나
기존 userList를 수정한다 했을 때 아래와 같이 수정을 하는분들도 있을 것 입니다.

1
2
3
this.state.userList.push({ name: 'kim' });
or
this.state.userList[0].name = 'lee';

리액트를 사용하신다면 절대 위와 같이 사용하면 안됩니다.
리액트는 상태가 변할때 리렌더링이 되고, 리렌더링은 setState를 통해서 동작하도록 되어있습니다.
그렇기 때문에 아래 코드와 같이 수정해주어야 합니다.

1
2
3
this.setState({
name: 'kim'
});

위와 같이 setState를 이용하여 코드 작성시
리액트의 기본속성중의 하나인 부모 컴포넌트가 리렌더링 되면, 자식컴포넌트도 리렌더링 되는 현상을 보실 수 있을겁니다.
그 과정은 가상 DOM에서만 이뤄지는 렌더링이고, 렌더링 후에 리액트의 diffing알고리즘을 통하여 변화가 일어나는 부분만 실제로 업데이트 해준다고 합니다.

Ex)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React, { Component } from 'react';
import ItemList from "./item";
import PropTypes from 'prop-types';

class UserList extends Component {
static propTypes = {
users: PropTypes.array,
}

static defaultProps = {
users: [],
}

renderList() {
const { users } = this.props;
return users.map((user) => <ItemList key={`${user.id}_key`} user={user} />);
}

render() {
console.log('users List 렌더됨 : ',this.props.users)
return (
<ul>
{ this.renderList() }
</ul>
);
}
}

export default UserList;

해당 코드는 유저 리스트 화면을 렌더링해주는 코드입니다.
처음 렌더링이 될때 유저의 정보를 받고 해당 텍스트를 console에 출력합니다.
immutable

문제는 부모컴포넌트에서 추가로 userList를 입력했을시 setState를 통해 상태를 변경해주면, 부모컨테이너가 리렌더링 되면서 자식 컴포넌트인 UserList 컴포넌트도 리렌더링 되게됩니다.
만약 부모 컴포넌트에서 입력해주는 input이 변경될때마다 setState를 변경시켰다면, 부모 컨테이너는 물론 UserList컨테이너 까지 리렌더링이되어 아래와 같은 문제가 나타나게 됩니다.
immutable

실제 UserList는 변경되지 않았는데, 계속해서 리렌더링되고 있습니다. 실제 DOM에 반영되지 않는다 하더라도
미세하게 CPU 낭비가 존재 할 것이며, 이런 낭비를 없애기 위해 shouldComponentUpdate 라이프 사이클 훅을 작성합니다.
불변함을 유지하면서 코드를 짯기 때문에 아주 쉽게 코드를 작성할 수 있을 것입니다.

1
2
3
shouldComponentUpdate(nextProps, nextState) {
return nextProps.users !== this.props.users;
}

UserList 컴포넌트에 shouldComponentUpdate 라이프 사이클 훅에, 전달받은 props를 비교하여, Boolean 값을 리턴 해줌으로써
리렌더링을 할것인지 말것인지 판단하도록 해주게 됩니다.

드디어 immutable JS에 관해 설명을 하도록 하겠습니다.

이렇게 위의 내용처럼 최적화를 위해 불변성을 유지시켜 코드를 작성하다보면 복잡한 상태가 있을 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
this.state = {
userList: [
{
id: 01,
name: 'sim'
},
{
id: 02,
name: 'park'
}
]
};

이런 상태의 객체에서 불변성을을 유지하면서 userList 배열의 2번째 객체 이름을 변경하려면,

1
2
3
4
5
6
7
8
9
10
const { userList } = this.state;
const copyUser = [ ...userList ];
// copyUser라는 변수에 spread 연산자를 통해 값을 복사해 넣습니다.
copyUser[1] = {
...userList[1],
name: 'lee'
};
this.setState({
userList: copyUser
});

이렇게 userList의 2번째 객체이름을 변경해주면 불변성을 유지하면서 변경해줄 수 있습니다.
하지만 이런 구조가 더 깊은 구조로 되어있다면… 아래 내용처럼 해주어야합니다.

1
2
3
4
5
6
7
8
9
10
11
this.state = {
userList: {
id: {
idx: 1,
userName: {
name: 'sim',
friendsName: 'park'
}
}
}
};

위 코드를 불변성을 유지하며 변경하려면..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { userList } = this.state;
const copyData = { ...userList };
this.setState({
userList: {
...copyData
id: {
...copyData.id
userName: {
...copyData.id.userName.name,
friends: 'lee'
}
}
}
});

이런식으로 spread 연산자가 반복적으로 사용되게 됩니다.
이렇게 번거롭고 복잡한 작업이 있을때 사용되는것이
Immutable JS 입니다.

우선 immutable은 규칙이 있습니다.

  1. List - 배열
  2. Map - 객체
  3. set - 설정
  4. get - 불러오기(읽기)
  5. update - 불러오고 업데이트 시
  6. List에 배열의 내장함수와 비슷한 함수들 존재 ( push, slice, filter 등 공식 사이트 문서 List() 참조 )
  7. toJS - 일반 javascript 로 변환
  8. setIn, getIn, updateIn - 깊은 구조의 객체의 값을 set, get, update 할때
  9. delete - key를 지울때

이렇게 여러가지 규칙이 있습니다.
자세한 내용은 공식 사이트 문서 참조하시길 바랍니다.

위의 규칙들을 사용해 객체의 구조에 적용해 본다면

1
2
3
4
5
6
7
8
9
10
11
12
this.state = {
userList: Map({
users: List([
{
name: 'sim'
},
{
name: 'park'
}
])
})
}

이렇게 적용이 가능하게 됩니다. immutable로 감싼 state는 사용시에도 규칙을 따르는데,
input에 텍스트 입력 후 버튼 클릭하는 함수에 코드를 작성해보면

onButtonClick = () => {
  const { userList } = this.state;

  this.setState({
    userList: userList.update('users', users => users.push(Map({
      name: 'lee' // 입력을 lee로 했다고 가정함.
    })))
  });
}

위와 같이 작성할 수 있습니다.

immutable을 왜 사용해야 하는지 필요성을 못느낄 수 도 있겠지만,
필자의 경우는 코드의 가독성을 위해서 위와 같은 상황에서는 사용하는 것을 추천합니다.

.get(), getIn()을 계속 하는게 싫다면 Record를 사용하면 좀 더 편리하게 사용할 수 있다고 합니다.
하지만 저는 Record는 따로 설명하지 않으려고 합니다.
그 이유는 다음 포스트로 작성할 Immer JS는 Immutable JS 대신 사용하는 라이브러리 인데
사용하게되면 Record 를 사용하는것과 다를것이 없다고 생각되어 Record 설명은 생략하도록 하겠습니다.


Comments: