본문 바로가기
JavaScript/React

React) immer - 사용법 및 useState 함수형 업데이트와 immer 함께 쓰기

by 박채니 2022. 11. 21.
안녕하세요, 코린이의 코딩 학습기 채니 입니다.
[리액트를 다루는 기술]의 책을 참고하여 포스팅한 개인 공부 내용입니다.

 

immer

- 구조가 복잡한 객체도 매우 쉽고 짧은 코드를 사용해 불변성을 유지하며 업데이트해주는 라이브러리

 

리액트는 상태를 업데이트할 때 반드시 불변성을 유지해야합니다.

하지만, 아래와 같이 객체의 구조가 깊어진다면 업데이트하는 것은 굉장히 번거로울 것입니다.

 

immer 사용하지 않을 때 예시

const object = {
    somewhere: {
        deep: {
            inside: 3,
            array: [1, 2, 3, 4]
        },
        bar: 2
    },
    foo: 1
};

// somewhere.deep.inside 값을 4로 바꾸기
let nextObject = {
    ...object,
    somewhere: {
        ...object.somewhere,
        deep: {
            ...object.somewhere.deep,
            inside: 4
        }
    }
};

// somewhere.deep.array에 5추가
let nextObject = {
    ...object,
    somewhere: {
        ...object.somewhere,
        deep: {
            ...object.somewhere.deep,
            array: object.somewhere.deep.array.concat(5)
        }
    }
};

코드가 굉장히 복잡해지는 것을 확인할 수 있습니다. 가독성 또한 좋지 않습니다.

이를 immer 라이브러리를 통해 해결할 수 있습니다.

 


immer 설치 및 사용법

 

프로젝트 준비

 

$ yarn create react-app immer-tutorial
$ cd immer-tutorial
$ yarn add immer

immer-tutorial 프로젝트 생성 후 immer 라이브러리를 설치하였습니다.

 

immer를 사용하지 않고 불변성 유지

 

차이를 알아보기 위해, 먼저 immer를 사용하지 않고 불변성 유지를 해보겠습니다.

 

App.js (immer 사용 X)

import { useRef, useCallback, useState } from 'react';

const App = () => {
  const nextId = useRef(1);
  const [form, setForm] = useState({ name: '', username: '' });
  const [data, setData] = useState({ array: [], uselessValue: null });

  // input 수정
  const onChange = useCallback(e => {
    const { name, value } = e.target;

    setForm({
      ...form,
      [name]: [value]
    });
  }, [form]);

  // form 등록
  const onSubmit = useCallback(e => {
    e.preventDefault(); // 폼 제출 방지
    const info = {
      id: nextId.current,
      name: form.name,
      username: form.username
    };

    // array 새 항목 등록
    setData({
      ...data,
      array: data.array.concat(info)
    });

    // form 초기화
    setForm({
      name: '',
      username: ''
    });
    nextId.current += 1;
  }, [data, form.name, form.username]);

  // 항목 삭제
  const onRemove = useCallback(id => {
    setData({
      ...data,
      array: data.array.filter(info => info.id !== id)
    });
  }, [data]);

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input name='username' placeholder='아이디' value={form.username} onChange={onChange} />
        <input name='name' placeholder='이름' value={form.name} onChange={onChange} />
        <button type='submit'>등록</button>
      </form>
      <div>
        <ul>
          {data.array.map(info => (
            <li key={info.id} onClick={() => onRemove(info.id)}>
              {info.username} ({info.name})
            </li>
          ))}
        </ul>
      </div>
    </div>
  )
}

export default App;

폼에서 아이디/이름 입력 후 등록 버튼을 누르면 리스트에 추가되고, 리스트 항목 클릭 시 해당 항목이 삭제됩니다.

위처럼 전개 연산자 및 함수를 사용해 불변성을 유지하는 것은 어렵진 않지만, 만일 상태가 복잡해진다면 귀찮을 수도 있겠죠.

 

immer 사용법

 

예시

import produce from 'immer';

const nextState = produce(originalState, draft => {
	// 바꾸고 싶은 값 바꾸기
	draft.somewhere.deep.inside = 5
});

produce 함수

첫 번째 파라미터 : '수정하고 싶은 상태'

두 번째 파라미터 : '어떻게 업데이트할지 정의하는 함수'

를 파라미터로 받습니다.

 

두 번째 파라미터로 전달되는 함수 내부에서 원하는 값을 변경하면, produce 함수가 불변성 유지를 해주며 새로운 상태를 생성해줍니다.

 

☆ 불변성에 신경 쓰지 않는 것처럼 코드를 작성하되 불변성 관리는 제대로 해줌!

 

예시

import produce from 'immer';

const originalState = [
  {
    id: 1,
    todo: '전개 연산자와 배열 내장 함수로 불변성 유지하기',
    checked: true
  },
  {
    id: 2,
    todo: 'immer로 불변성 유지하기',
    checked: false
  }
];

const nextState = produce(originalState, draft => {
  // id가 2인 항목의 checked 값을 true로 설정
  const todo = draft.find(t => t.id === 2); // id로 항목 찾기
  todo.checked = true;
  // 혹은 draft[1].checked. = true;

  // 배열에 새로운 데이터 추가
  draft.push({
    id: 3,
    todo: '일정 관리 앱에 immer 적용하기',
    checked: false
  });

  // id = 1인 항목 제거
  draft.splice(draft.findIndex(t => t.id === 1), 1);
})

 

App 컴포넌트에 immer 적용하기

 

App.js

import { useRef, useCallback, useState } from 'react';
import produce from 'immer';

const App = () => {
  const nextId = useRef(1);
  const [form, setForm] = useState({ name: '', username: '' });
  const [data, setData] = useState({ array: [], uselessValue: null });

  // input 수정
  const onChange = useCallback(e => {
    const { name, value } = e.target;

    // immer 사용
    setForm(
      produce(form, draft => {
        draft[name] = value;
      })
    )
  }, [form]);

  // form 등록
  const onSubmit = useCallback(e => {
    e.preventDefault(); // 폼 제출 방지
    const info = {
      id: nextId.current,
      name: form.name,
      username: form.username
    };

    // array 새 항목 등록
    // immer 사용
    setData(
      produce(data, draft => {
        draft.array.push(info);
      })
    )

    // form 초기화
    setForm({
      name: '',
      username: ''
    });
    nextId.current += 1;
  }, [data, form.name, form.username]);

  // 항목 삭제
  const onRemove = useCallback(id => {
    setData(
      produce(data, draft => {
        draft.array.splice(draft.array.findIndex(info => info.id === id), 1)
      })
    );
  }, [data]);
  
  ...
  ...

immer를 사용하여 객체의 값을 수정하거나 배열에 직접적으로 접근하여 추가하는 push, splice 함수를 사용한 것을 확인할 수 있습니다.

draft가 변경하려고 하는 객체 혹은 배열의 복사본을 가지고 변경해주는 함수다! 라고 쉽게 생각하면 이해가 쉽습니다.

 

useState의 함수형 업데이트와 immer 함께 쓰기

 

useState의 함수형 업데이트

const [number, setNumber] = useState(0);

//prevNumber는 현재 number를 가리킴
const onIncrease = useCallback(
	() => setNumber(prevNumber => prevNumber + 1), []
);

 

immer에서 제공하는 produce 함수를 호출 시, 첫 번째 파라미터가 함수 형태라면 업데이트 함수를 리턴합니다.

 

예시

  const update = produce(draft => {
    draft.value = 2;
  });
  const originalState = {
    value: 1,
    foo: 'bar'
  };
  const nextState = update(originalState);
  console.log(nextState); // { value: 2, foo: 'bar' }

 

이렇게 immer의 속성과 useState의 함수형 업데이트를 함께 활용 시 깔끔하게 코드를 만들 수 있습니다.

App.js

import { useRef, useCallback, useState } from 'react';
import produce from 'immer';

const App = () => {
  const nextId = useRef(1);
  const [form, setForm] = useState({ name: '', username: '' });
  const [data, setData] = useState({ array: [], uselessValue: null });

  // input 수정
  const onChange = useCallback(e => {
    const { name, value } = e.target;

    // 함수형 업데이트 + immer 사용
    setForm(
      produce(draft => {
        draft[name] = value;
      })
    )
  }, []); // [form] => []로 변경

  // form 등록
  const onSubmit = useCallback(e => {
    e.preventDefault(); // 폼 제출 방지
    const info = {
      id: nextId.current,
      name: form.name,
      username: form.username
    };

    // array 새 항목 등록
    //  함수형 업데이트 + immer 사용
    setData(
      produce(draft => {
        draft.array.push(info);
      })
    )

    // form 초기화
    setForm({
      name: '',
      username: ''
    });
    nextId.current += 1;
  }, [form.name, form.username]); // [data, form.name, form.username] -> [form.name, form.username]로 변경

  // 항목 삭제
  const onRemove = useCallback(id => {
     // 함수형 업데이트 + immer 사용
    setData(
      produce(draft => {
        draft.array.splice(draft.array.findIndex(info => info.id === id), 1)
      })
    );
  }, []); // [data] -> []로 변경

produce 함수의 파라미터를 함수 형태로 사용하니 코드가 깔끔해진 것을 확인할 수 있습니다.