immer
포스트
취소

immer

immer

정의

  • 불변성을 유지하면서 복잡한 상태 객체를 쉽게 수정할 수 있도록 돕는 라이브러리
  • draft라는 가변적인 프록시 객체를 통해 마치 직접 객체를 수정하는 것처럼 코드를 작성할 수 있게 해준다.
  • 내부적으로는 변경된 부분만 복사하여 새로운 불변 상태를 생성한다.

특징

  • Proxy 사용
    • draft라는 프록시 객체를 통해 상태 변경을 감지하고 불변성을 유지한다.
  • 불변성 자동 처리
    • 개발자가 직접 깊은 복사를 신경 쓸 필요 없이 불변성을 유지한다.
    • 깊은 중첩 객체도 어렵지 않게 작업할 수 있다.
    • 상태를 직접 변경하는 것처럼 작성하지만 실제로는 불변성을 유지한다.
  • 간결한 코드
    • ...spread 연산자나 Object.assign()같은 메소드를 사용하지 않아도 된다.
    • push, pop, splice 등의 메소드를 통해 배열 및 객체에 대한 메소드를 사용할 수 있다.
  • 성능 최적화
    • 변경된 부분만 복사하므로 불필요한 메모리 할당을 줄일 수 있다.
    • Copy-on-write 방식을 사용하여 변경된 부분만 복사하고, 나머지는 기존 참조를 유지하여 성능을 최적화한다.
    • 변경되지 않은 데이터는 기존 참조를 유지하여 메모리 사용을 줄인다.

장점

  • 불변성 유지가 쉬워져 버그 발생 가능성을 줄일 수 있다.
  • 복잡한 상태 업데이트 로직을 간단하게 작성 가능할 수 있다.
  • 코드 가독성과 유지보수성이 향상된다.
  • 타입 안정성이 좋다. (TypeScript 지원)

단점

  • immer를 별도로 설치해야 한다.
  • 러닝커브는 낮은 건 맞지만. 내부 작동 방식을 이해할 필요는 있다.
    • 가장 중요한 것은 produce 메소드를 이해하는 것이다.
  • 매우 단순한 상태 변경에는 오버헤드가 발생할 수 있다.
    • 내부적으로 프록시를 사용하기 때문에 발생하는 문제다.
    • 다만 대부분의 경우 무시할 수 있는 수준이긴 하다.
  • 매우 큰 상태 트리에서는 성능 이슈가 생길 수 있다.
  • 클래스 객체같은 일부 케이스에서 주의가 필요하다.
    • 예시 : [immerable] = true
  • 프록시를 지원하지 않는 환경에서는 ES5 fallback을 사용하게 되어 성능이 떨어질 수 있다.
    • 구형 브라우저나 리액트 네이티브같은 일부 환경이 해당한다.
  • 불변성을 완벽하게 보장하는 것은 아니다.
    • 외부 참조에 대한 문제나 깊은 중첩 구조에서 부분 업데이트 사용 시 안정성이 떨어질 수 있다.

설치 방법

npm install immer

기본 사용 방법 (메소드 별로 적용하기)

import { create } from 'zustand';
import { produce } from 'immer';

const useProfileStore = create((set) => ({
  profile: {
    name: '',
    details: {
      age: 0,
      location: {
        city: '',
        country: '',
      },
    },
    hobbies: [],
  },
  updateCity: (newCity) =>
    set(produce((state) => {
      state.profile.details.location.city = newCity;
    })),
  addHobby: (hobby) =>
    set(produce((state) => {
      state.profile.hobbies.push(hobby);
    })),
}));

export default useProfileStore;

기본 사용 방법 (전체에 일괄 적용하기)

import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

const useProfileStore = create(
  immer(
    (set) => ({
      profile: {
        name: '',
        details: {
          age: 0,
          location: {
            city: '',
            country: '',
          },
        },
        hobbies: [],
      },
      updateCity: (newCity) =>
        set((state) => {
          state.profile.details.location.city = newCity;
        }),
      addHobby: (hobby) =>
        set((state) => {
          state.profile.hobbies.push(hobby);
        }),
    })
  )
);

export default useProfileStore;

차이점 알기

메소드마다 적용할 때는 import { produce } from 'immer';고,
일괄적으로 적용할 때는 import { immer } from 'zustand/middleware/immer';다.
헷갈리지 않게 주의하자.

useImmer

정의

  • useState와 Immer 라이브러리를 결합한 훅
  • 복잡한 중첩 객체나 배열을 불변하게 업데이트하는 작업을 훨씬 간결하고 직관적으로 수행할 수 있게 해준다.

설치 방법

npm install use-immer

사용 방법

useState 사용하듯이 import 받아서 사용하면 된다.
import { useImmer } from 'use-immer'

useState와 useImmer의 비교

  • useState를 사용하는 경우
const [state, setState] = useState({
    user: {
        name: 'John',
        address: {
            city: 'Seoul',
            zip: '12345'
        }
    }
});

setState(prevState => ({
    ...prevState,
    user: {
        ...prevState.user,
        address: {
            ...prevState.user.address,
            zip: '67890'
        }
    }
}));
  • useImmer를 사용하는 경우
const [state, updateState] = useImmer({
    user: {
        name: 'John',
        address: {
            city: 'Seoul',
            zip: '12345'
        }
    }
});

updateState(draft => {
    draft.user.address.zip = '67890';
});
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.