Frontend/React.js

[React]useImmer 라이브러리 사용법

okojin 2025. 1. 4. 12:16

Immer 라이브러리는 주로 객체를 업데이트 하는 용도로 사용된다. 리액트에서는 state 객체를 업데이트 할 때 불변성을 관리하는 용도로 사용될 수 있는데, 기본적으로 state 객체를 업데이트 할 때는 다음과 같은 방법을 사용한다.

import { useState } from 'react';

export default function StateForm() {
  const [form, setForm] = useState({
    name: '홍길동',
    address: {
      do: '경기도',
      city: '성남',
    },
  });

  const handleForm = (e) => {
    const { name, value } = e.target;

    setForm({
        // 기존의 객체를 그대로 유지하기 위해 스프레드 구문 사용
      ...form,
      [name]: value,
    });
  };

  return (
    <form>
      <div>
        <label htmlFor='name'>이름</label>
        <input id='name' name='name' type='text' onChange={handleForm} value={form.name} />
      </div>
      <div>
        <label htmlFor='do'>도</label>
        <input id='do' name='do' type='text' onChange={handleForm} value={form.address.do} />
      </div>
      <div>
        <label htmlFor='city'>시</label>
        <input
          id='city'
          name='city'
          type='text'
          onChange={handleForm}
          value={form.address.city}
        />
      </div>
    </form>
  );
}

하지만 이렇게 코드를 작성할 경우 스프레드 구문은 객체를 얕은 복사하기 때문에 do와 city를 업데이트 하는 경우 다음과 같이 address의 객체도 스프레드 구문을 사용해서 복사를 해줘야 한다.

const handleFormNest = (e) => {
  const { name, value } = e.target;

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

<div>
  <label htmlFor='do'>도</label>
  <input id='do' name='do' type='text' onChange={handleFormNest} value={form.address.do} />
</div>
<div>
  <label htmlFor='city'>시</label>
  <input
    id='city'
    name='city'
    type='text'
    onChange={handleFormNest}
    value={form.address.city}
  />
</div>

하지만 만약 객체의 깊이가 깊어지는 경우 불변성 관리를 하기 매우 힘들어진다. 어떤 이유로 State 객체의 깊이를 제외할 수 없는 경우에는 Immer 라이브러리를 활용하면 보다 직관적으로 중첩된 객체를 업데이트할 수 있다.

Immer를 사용하기 위해서는 다음 명령어를 통해 라이브러리를 프로젝트에 미리 포함시켜야 한다.

npm install use-immer

use-immer는 Immer를 React에서 사용하기 위한 라이브러리이며, use-immer를 설치하면 Immer도 함께 설치된다.

라이브러리 설치가 완료되면 Immer를 사용하여 다시 작성해 보자.

import { useImmer } from 'use-immer';

export default function FormFile() {
  const [form, setForm] = useImmer({
    name: '홍길동',
    address: {
      do: '경기도',
      city: '성남',
    },
  });

  const handleForm = (e) => {
    const { name, value } = e.target;

    setForm((form) => {
      form[name] = value;
    });
  };

  const handleFormNest = (e) => {
    const { name, value } = e.target;

    setForm((form) => {
      form.address[name] = value;
    });
  };

  ...
}

useImmer 함수의 사용법은 useState 함수와 유사하므로 특별히 언급할 사항을 없다. 인수로 State의 초깃값을 전달하고, 반환값으로 [State값, State의 세터]를 받는다.

다른 점은 세터의 사용법이다. useImmer 함수로 생성된 세터에서는 세터의 용도가 다음과 같은 성격을 가지고 있다.

  • State를 인수로 받는다.
  • 함수 내부에서 State를 업데이트할 수 있다.

세터 아래에서 변경된 내용은 내부적으로 복제된 State에 반영되는데, 지금까지 스프레드 구문으로 표현하던 부분을 대신해 주는 것이다. address 내부를 변경하는 코드에서도 거의 비슷하게 작성할 수 있어 코드가 훨씬 간단해졌다.

핸들러 공통화하기

위의 예시에서는 계층에 따라서 별도의 핸들러를 만들어 줬지만 다음과 같이 통일할 수 있다.

import { useImmer } from 'use-immer';

export default function FormFile() {
  const [form, setForm] = useImmer({
    name: '홍길동',
    address: {
      do: '경기도',
      city: '성남',
    },
  });

  const handleNest = (e) => {
    // 요소명을 '.'으로 분해(요소 이름이 'xxxx.xxxx'라는 가정 하에)
    const ns = e.target.name.split('.');

    setForm((form) => {
      // 계층에 따라 대위임처를 변경한다.
      if (ns.length === 1) {
        form[ns[0]] = e.target.value;
      } else {
        form[ns[0]][ns[1]] = e.target.value;
      }
    });
  };

  return (
    <form>
      <div>
        <label htmlFor='name'>이름</label>
        <input id='name' name='name' type='text' onChange={handleNest} value={form.name} />
      </div>
      <div>
        <label htmlFor='do'>도</label>
        <input
          id='do'
          name='address.do'
          type='text'
          onChange={handleNest}
          value={form.address.do}
        />
      </div>
      <div>
        <label htmlFor='city'>시</label>
        <input
          id='city'
          name='address.city'
          type='text'
          onChange={handleNest}
          value={form.address.city}
        />
      </div>
    </form>
  );
}

name 속성에 계층에 따른 이름을 붙여줌으로써 하나의 핸들러로 2계층까지 업데이트하도록 구현하였다. 만약에 3계층이상 업데이트를 해야하는 경우 분기만 추가해주면 된다.