1. immer.js 를 사용한 너무 간단한 불변성관리

immer.js 는 리액트 종속적인 라이브러리는 아니지만, 리액트와 함께 사용하면 너무나 편리한 불변성관리 라이브러리입니다. 이 라이브러리의 핵심은 "불변성에 대하여 신경쓰지 않는 것 처럼 코드를 작성하되 불변성 관리는 제대로 해주는 것"입니다.

Edit m9vkjvp189

위 프로젝트를 열어보시면, 다음과 같은 코드가 있습니다:

src/App.js

import React, { Component } from 'react';

class App extends Component {
  id = 0;
  state = {
    counter: 0,
    textList: {
      input: '',
      list: [
        {
          id: 0,
          text: '안녕하세요'
        }
      ]
    }
  };
  handleClick = () => {
    this.setState({
      counter: this.state.counter + 1
    });
  };

  handleChange = e => {
    this.setState({
      textList: {
        ...this.state.textList,
        input: e.target.value
      }
    });
  };

  handleKeyPress = e => {
    if (e.key !== 'Enter') return;
    this.setState({
      textList: {
        input: '',
        list: this.state.textList.list.concat({
          id: ++this.id, // 추가 할 때마다 기존 id 에 1 더해서 설정
          text: this.state.textList.input
        })
      }
    });
  };

  render() {
    return (
      <div>
        <h2>카운터</h2>
        <h3>{this.state.counter}</h3>
        <button onClick={this.handleClick}>+1</button>
        <hr />
        <h2>텍스트박스와 리스트</h2>
        <input
          value={this.state.textList.input}
          onChange={this.handleChange}
          onKeyPress={this.handleKeyPress}
        />
        <ul>
          {this.state.textList.list.map(item => (
            <li key={item.id}>{item.text}</li>
          ))}
        </ul>
      </div>
    );
  }
}

export default App;

보시면, 단순히 counter 에 새 값을 설정하는건 그리 번거롭지 않은데, textList 객체 내부에있는 input 이나 list 배열을 건드릴때에는 불변성 유지 때문에 기존의 객체를 한번 복사해야 돼서 조금은 귀찮습니다.

물론, 익숙해지면 별거아니긴 하지만, 상태의 구조가 더욱 깊어지면 깊어질수록 관리하기 힘들어집니다. (그렇기에 상태의 구조는 최대한 깊지 않게 구성하는것이 권장됩니다.) 예를들어서

{
  something: {
    inside: {
      here: {
        there: {
          changeMe: 0,
          keepMe: 1
        },
        where: 'unknown'
      },
      hello: 'world'
    },
    outside: 'what?'
  }
}

이렇게 있을 때 changeMe 에 해당하는곳만 바꾸고싶다면...

this.setState({
  somthing: {
    ...this.state.something,
    here: {
      ...this.state.here,
      there: {
        ...this.state.here.there,
        changeMe: 10
      }
    }
  }
})

정말 귀찮습니다... 이런 코드는 피하긴해야하지만 가끔씩, 어쩌다보면 저렇게까지 해야하는일이 발생 할 수도 있는데요, Immer 를 사용하면 정말 편해집니다!

우선 immer 라는 라이브러리를 설치하세요. (CodeSandbox 에선 File Editor 탭에서 Add Dependency 부분에서 설치 할 수 있습니다.)

$ yarn add immer

immer 의 기본적인 사용법

import produce from "immer"

const baseState = [
    {
        todo: "Learn typescript",
        done: true
    },
    {
        todo: "Try immer",
        done: false
    }
]

const nextState = produce(baseState, draftState => {
    draftState.push({todo: "Tweet about it"})
    draftState[1].done = true
})

위 코드는 immer 라이브러리의 README 에 있는 예시 코드입니다. immer 를 불러오면 produce 라는 함수를 사용 할 수 있게 되는데요, 이 함수에 첫번째 파라미터로 불변성을 지키고싶은 객체 (혹은 배열) 을 집어넣고, 두번째 파라미터로 전달하는 함수에서는 어떻게 변화를 주고 싶은지 로직을 작성해주면, 라이브러리가 알아서 불변성 지키면서 새로운 객체를 만들어서 반환해줍니다.

한번 우리의 기존 코드에서 불러와서 사용하면 어떻게 변하는지 볼까요?

src/App.js

상단에서 불러와주고,

import React, { Component } from 'react';
import produce from 'immer';

handleClick 함수를 재작성해보겠습니다.

  handleClick = () => {
    this.setState(
      produce(this.state, draft => {
        draft.counter++;
      })
    );
  };

사실 카운터 값을 바꾸는 곳에서는 그냥 setState 로 하는것도 충분히 편하지만, 이렇게 연습삼아 작성을 해보았습니다.

리액트에서 setState는, 파라미터로 전달받는 값에서 객체형태를 처리 할 수도 있고 업데이터 함수형태도 처리 할 수 있습니다. 이렇게 말이죠:

  handleClick = () => {
    this.setState(state => ({
      counter: state.counter + 1
    }));
  };

produce 함수는 첫번째 파라미터를 생략하면, 업데이터 함수를 반환합니다.

예를 들어,

const updater = produce(draft => {
  draft.counter++;
});
const prevState = {
  counter: 1
};
const nextState = updater(prevState); 
// nextState: { counter: 2 }

가 되는 것이죠.

그렇기에, immer 를 setState 와 함께 사용한다면 이렇게 할 수도 있습니다.

  handleClick = () => {
    this.setState(
      produce(draft => {
        draft.counter++;
      })
    );
  };

이렇게 값 하나 바꾸는건 아무래도 일반 setState 가 코드도 더 짧고 편하겠죠? 하지만 깊은 데이터를 수정할땐 immer 를 사용하면 훨씬 간단해집니다.

  handleChange = e => {
    e.persist(); // 하단에 있는 콜백에서 e를 계속 사용 할 수 있도록 해줍니다
    this.setState(
      produce(draft => {
        draft.textList.input = e.target.value;
      })
    );
  };

  handleKeyPress = e => {
    if (e.key !== 'Enter') return;
    this.setState(
      produce(draft => {
        draft.textList.input = '';
        draft.textList.list.push({
          id: ++this.id,
          text: this.state.textList.input
        });
      })
    );
  };

Edit m4y09okmlp

이 라이브러리는 나중에 리덕스라는 상태관리 라이브러리와 함께 사용하면 정말 편합니다!

results matching ""

    No results matching ""