Immutable.js 시작하기
프로젝트에서 immutable 을 사용 할 땐, 다음과 같이 패키지를 설치해서 사용합니다.
yarn add immutable
그런데 우리는 이 튜토리얼에서는 Codesandbox 를 사용하므로, 좌측의 Dependencies 를 누른다음에 Add Package 를 누르시면 패키지를 직접 설치하여 사용 할 수 있습니다. 여러모로 유용한 도구입니다.
Immutable 을 사용 할 때는 다음 규칙들을 기억하세요:
- 객체는 Map
- 배열은 List
- 설정할땐 set
- 읽을땐 get
- 읽은다음에 설정 할 땐 update
- 내부에 있는걸 ~ 할땐 뒤에 In 을 붙인다: setIn, getIn, updateIn
- 일반 자바스크립트 객체로 변환 할 땐 toJS
- List 엔 배열 내장함수와 비슷한 함수들이 있다 - push, slice, filter, sort, concat... 전부 불변함을 유지함
- 특정 key 를 지울때 (혹은 List 에서 원소를 지울 때) delete 사용
프로젝트의 엔트리인 index.js 에서 시험삼아 Immutable 을 불러오고, Map 과 List 를 사용해보세요.
다음 코드를 하나하나 이해하면서 직접 작성해보시길 바랍니다.
index.js
import React from 'react';
import { render } from 'react-dom';
import App from './App';
import { Map, List } from 'immutable';
// 1. 객체는 Map
const obj = Map({
foo: 1,
inner: Map({
bar: 10
})
});
console.log(obj.toJS());
// 2. 배열은 List
const arr = List([
Map({ foo: 1 }),
Map({ bar: 2 }),
]);
console.log(arr.toJS());
// 3. 설정할땐 set
let nextObj = obj.set('foo', 5);
console.log(nextObj.toJS());
console.log(nextObj !== obj); // true
// 4. 값을 읽을땐 get
console.log(obj.get('foo'));
console.log(arr.get(0)); // List 에는 index 를 설정하여 읽음
// 5. 읽은다음에 설정 할 때는 update
// 두번째 파라미터로는 updater 함수가 들어감
nextObj = nextObj.update('foo', value => value + 1);
console.log(nextObj.toJS());
// 6. 내부에 있는걸 ~ 할땐 In 을 붙인다
nextObj = obj.setIn(['inner', 'bar'], 20);
console.log(nextObj.getIn(['inner', 'bar']));
let nextArr = arr.setIn([0, 'foo'], 10);
console.log(nextArr.getIn([0, 'foo']));
// 8. List 내장함수는 배열이랑 비슷하다
nextArr = arr.push(Map({ qaz: 3 }));
console.log(nextArr.toJS());
nextArr = arr.filter(item => item.get('foo') === 1);
console.log(nextArr.toJS());
// 9. delete 로 key 를 지울 수 있음
nextObj = nextObj.delete('foo');
console.log(nextObj.toJS());
nextArr = nextArr.delete(0);
console.log(nextArr.toJS());
render(<App />, document.getElementById('root'));
갑자기 이런걸 사용하게 되면 처음엔 적응이 안될지도 모릅니다. 한번 사용해보고 모두 외우는건 무리일수도 있습니다. 일단 이런것들이 있다는것만 알아두고, 앞으로 Immutable 을 사용하면서 차근차근 적응해나가면 되니까 큰 걱정은 하지 마세요~
사실 이 포스트에서 나온건 Immutable 의 극히 일부분입니다. 매뉴얼 을 보시면 더 많은 기능을 확인 하실 수 있습니다. 하지만, 주로 사용 되는건 위 9가지 입니다.
다 확인해보셨다면, 기존에 index.js 에 작성한것들은 지우세요.
리액트 컴포넌트에서 Immutable 사용하기
여기서부터 본격적으로 리액트 컴포넌트에 Immutable 를 사용하는 것을 배워보겠습니다. 우선적으로, Immutable 은 페이스북에서 만들었기 때문에 React 와 호환이 어느정도 되긴 합니다 (예를 들어서 JSX 이뤄진 List 를 렌더링 할 수 있습니다) 하지만, state 자체를 Immutable 데이터로 사용하는것 까지는 지원되지 않습니다.
따라서, state 내부에 하나의 Immutable 객체를 만들어두고, 상태 관리를 모두 이 객체를 통해서 진행하시면 됩니다.
우선 state 부터 변경해보세요:
state = {
data: Map({
input: '',
users: List([
Map({
id: 1,
username: 'velopert'
}),
Map({
id: 2,
username: 'mjkim'
})
])
})
}
data 라는 Map 을 만들었고, 그 내부에는 users List 가 있고, 그 안에 또 Map 두개가 안에 들어있습니다.
이렇게 수정을 하고 나면 코드에서 오류가 나기 시작 할 것입니다. 우리가 함께 차차 고쳐줄테니 당황하지 마세요.
이제 setState 를 하게 될 때도 코드를 조금씩 바꿔줘야 합니다.
onChange 부분도 다음과 같이 수정하세요.
onChange = (e) => {
const { value } = e.target;
const { data } = this.state;
this.setState({
data: data.set('input', value)
});
}
그렇게 복잡하진 않죠? set 을 통해서 값을 변경해줬습니다.
그 다음엔, onButtonClick 도 수정해보세요.
onButtonClick = () => {
const { data } = this.state;
this.setState({
data: data.set('input', '')
.update('users', users => users.push(Map({
id: this.id++,
username: data.get('input')
})))
})
}
여기는 이전 코드보다 아주 조금은 복잡해졌습니다. 그래도, 해석하지 못할 정도는 아닙니다. 이 함수에서는 input 값을 공백으로 만들어야 하고, users 에 새 Map 을 추가해주어야 합니다. 이렇게 여러가지를 하는 경우에는 함수들을 중첩하여 사용하면 됩니다. data.set(...).update(...) 형식으로 말이죠.
상태 업데이트 로직이 완성되고 나면, render 함수도 변경해주어야 합니다. Map 혹은 List 의 값을 읽을땐 data.users 이런식으로는 읽지 못하고, data.get('users') 이런식으로 읽어야 합니다.
render() {
const { onChange, onButtonClick } = this;
const { data } = this.state;
const input = data.get('input');
const users = data.get('users');
return (
<div>
<div>
<input onChange={onChange} value={input} />
<button onClick={onButtonClick}>추가</button>
</div>
<h1>사용자 목록</h1>
<div>
<UserList users={users} />
</div>
</div>
);
}
UserList 와 User 에서도 마찬가지로 값을 읽어올 때 get 을 사용해주어야 합니다.
UserList.js
import React, { Component } from 'react';
import User from './User';
class UserList extends Component {
shouldComponentUpdate(prevProps, prevState) {
return prevProps.users !== this.props.users;
}
renderUsers = () => {
const { users } = this.props;
return users.map((user) => (
<User key={user.get('id')} user={user} />
))
}
render() {
console.log('UserList 가 렌더링되고 있어요!')
const { renderUsers } = this;
return (
<div>
{renderUsers()}
</div>
);
}
}
export default UserList;
User.js
import React, { Component } from 'react';
class User extends Component {
shouldComponentUpdate(prevProps, prevState) {
return this.props.user !== prevProps.user;
}
render() {
const { username } = this.props.user.toJS();
console.log('%s가 렌더링 되고있어요!!!', username);
return (
<div>
{username}
</div>
);
}
}
export default User;
User 컴포넌트에서는, username 을 보여주기 위해서, const username = this.props.user.get('username')
을 해도 좋습니다. 하지만 위와 같은 형식으로 toJS() 를 한 결과를 비구조화 할당 하는 방법도 있답니다.
여기까지 하시면, Immutable 를 사용하여 상태관리하는것이 완성됩니다.
계속 .get, .getIn 하는거 싫다! 그렇다면 Record
Immutable 을 사용하시는 분들 중에서, Record 의 존재를 잘 모르시는 분들도 있습니다. (주관적인 생각입니다. 제가 그랬거든요. 비교적 최근 알게되었습니다.) Record 를 사용하면 Immutable 의 set, update, delete 등을 계속 사용 할 수 있으면서도, 값을 조회 할 때 get, getIn 을 사용 할 필요 없이, data.input 이런식으로 조회를 할 수 있습니다.
Record 는, Typescript 혹은 Flow 같은 타입시스템을 도입 할 때 굉장히 유용합니다.
자, 다시 우리가 아까전에 Immutable 연습을 했을 때 처럼, index.js 에서 Record 를 불러와서 연습해보겠습니다.
다음 코드의 주석을 하나하나 읽어가면서 직접 작성해보세요!
index.js
import React from 'react';
import { render } from 'react-dom';
import App from './App';
import { Record } from 'immutable';
const Person = Record({
name: '홍길동',
age: 1
});
let person = Person();
console.log(person);
// ▶Object {name: "홍길동", age: 1 }
console.log(person.name, person.age);
// "홍길동" 1
person = person.set('name', '김민준');
console.log(person.name); // 김민준
// 이건 오류 납니다: person.name = '철수';
// Record 에서 사전 준비해주지 않은 값을 넣어도 오류납니다.
// person = person.set('job', 5);
// 값을 따로 지정해줄수도 있습니다.
person = Person({
name: '영희',
age: 10
});
const { name, age } = person; // 비구조화 할당도 문제없죠.
console.log(name, age); // "영희" 10
// 재생성 할 일이 없다면 이렇게 해도 됩니다.
const dog = Record({
name: '멍멍이',
age: 1
})()
console.log(dog.name); // 멍멍이
// 이런것도 가능하죠.
const nested = Record({
foo: Record({
bar: true
})()
})();
console.log(nested.foo.bar); // true
// Map 다루듯이 똑같이 쓰면 됩니다.
const nextNested = nested.setIn(['foo', 'bar'], false);
console.log(nextNested);
render(<App />, document.getElementById('root'));
Record 가 어떤식으로 작동하는지 조금 감을 잡으셨나요? 그러면, 우리가 만들었던 사용자목록을 Record 를 사용해서 구현해보도록 수정해보겠습니다. (기존에 index.js 에서 연습한 내용은 다시 지워주세요.)
App.js
import React, { Component } from 'react';
import UserList from './UserList';
import { Map, List, Record } from 'immutable';
// User 를 위한 Record 생성
const User = Record({
id: null,
username: null
});
// Data 를 위한 Record 생성
const Data = Record({
input: '',
users: List()
});
class App extends Component {
id = 3;
state = {
data: Data({
users: List([
User({
id: 1,
username: 'velopert'
}),
User({
id: 2,
username: 'mjkim'
})
])
})
}
onChange = (e) => {
const { value } = e.target;
const { data } = this.state;
this.setState({
data: data.set('input', value)
});
}
onButtonClick = () => {
const { data } = this.state;
this.setState({
data: data.set('input', '')
.update('users', users => users.push(new User({
id: this.id++,
username: data.get('input')
})))
})
}
render() {
const { onChange, onButtonClick } = this;
const { data: { input, users } } = this.state;
return (
<div>
<div>
<input onChange={onChange} value={input} />
<button onClick={onButtonClick}>추가</button>
</div>
<h1>사용자 목록</h1>
<div>
<UserList users={users} />
</div>
</div>
);
}
}
export default App;
User 와 Data 를 위한 Record 를 사전준비 해주었고, onButtonClick 에서 기존에 data.get('input')
부분을 data.input
으로 바꿔주었습니다. (참고로 .get 도 문제없이 작동하기 때문에 무조건 이렇게 바꿔줄 필요는 없습니다.)
그리고, render 부분에서도 기존에 const input = data.get('input')
과 같이 일일히 레퍼런스를 만들어주었던 것을, 그냥 간편하게 비구조화 할당 해주었습니다.
이제, UserList 와 User 도 변경해주겠습니다. 이 또한 무조건 해야 할 작업은 아니지만, 기껏 Record 로 만들어주었는데, 굳이 .get()
을 쓸 필요는 없지 않겠어요?
UserList.js
import React, { Component } from 'react';
import User from './User';
class UserList extends Component {
shouldComponentUpdate(prevProps, prevState) {
return prevProps.users !== this.props.users;
}
renderUsers = () => {
const { users } = this.props;
return users.map((user) => (
<User key={user.id} user={user} />
))
}
render() {
console.log('UserList 가 렌더링되고 있어요!')
const { renderUsers } = this;
return (
<div>
{renderUsers()}
</div>
);
}
}
export default UserList;
User.js
import React, { Component } from 'react';
class User extends Component {
shouldComponentUpdate(prevProps, prevState) {
return this.props.user !== prevProps.user;
}
render() {
const { username } = this.props.user;
console.log('%s가 렌더링 되고있어요!!!', username);
return (
<div>
{username}
</div>
);
}
}
export default User;