Immutable.js 시작하기

프로젝트에서 immutable 을 사용 할 땐, 다음과 같이 패키지를 설치해서 사용합니다.

yarn add immutable

그런데 우리는 이 튜토리얼에서는 Codesandbox 를 사용하므로, 좌측의 Dependencies 를 누른다음에 Add Package 를 누르시면 패키지를 직접 설치하여 사용 할 수 있습니다. 여러모로 유용한 도구입니다.

Immutable 을 사용 할 때는 다음 규칙들을 기억하세요:

  1. 객체는 Map
  2. 배열은 List
  3. 설정할땐 set
  4. 읽을땐 get
  5. 읽은다음에 설정 할 땐 update
  6. 내부에 있는걸 ~ 할땐 뒤에 In 을 붙인다: setIn, getIn, updateIn
  7. 일반 자바스크립트 객체로 변환 할 땐 toJS
  8. List 엔 배열 내장함수와 비슷한 함수들이 있다 - push, slice, filter, sort, concat... 전부 불변함을 유지함
  9. 특정 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 를 사용하여 상태관리하는것이 완성됩니다.

Edit 시작하세요 Immutable.js

계속 .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;

Edit 시작하세요 Immutable.js with Record 지금까지 한 작업은 위 버튼을 클릭하여 확인 할 수 있습니다.

results matching ""

    No results matching ""