본문 바로가기
FE/React

4. immer를 사용하여 더 쉽게 불변성 유지하기

by aeyong-dev 2023. 7. 28.

이전 포스팅에서 어떻게 컴포넌트를 최적화 하는지에 대해 배웠습니다. 

불변성을 유지하면서 상태를 업데이트 하는 것이 정말 중요했죠.

복잡한 상황에서는 값 하나를 업데이트하기 위해 엄청나게 긴 코드가 필요합니다.

일일이 전개연산자를 써주기에도 참 불편하고 가독성도 떨어집니다.

불변성을 더 편리하게 유지하게 해주는 도구가 immer라고 했고, 이제 immer에 대해 자세히 다뤄볼 예정입니다. 


4. 1 immer 설치와 사용법

4. 1. 1 준비

새로운 리액트 프로젝트를 만들고, immer를 설치합시다.

$ yarn add immer

 

4. 1. 2 immer를 사용하지 않고 불변성 유지

일단 immer를 안쓰고 불변성을 유지해볼게요. 

App.js를 다음과 같이 고쳐봅시다. 

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;

후, 참 기네요.

아무튼, 실행시키면 이렇게 됩니다. 

<li> 태그로 아이디가 표시되고, 괄호 안에 이름이 들어가요.

이렇게 아주 간단한 컴포넌트를 만들어 보았는데요.

지금은 간단해서 크게 문제 없지만 나중에 복잡한 상황이 된다면 불변성을 유지하기 꽤 번거로워집니다. 

이제부터 immer를 사용해볼게요.

 

4. 1. 3 immer 사용법

immer는 불변성을 간단한 방법으로 유지할 수 있게 해주는 라이브러리라고 했습니다.

다음과 같이 사용할 수 있어요.

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

첫번째 파라미터로 수정하고 싶은 상태를, 

두번째 파라미터로 어떻게 수정할 것인지 정의하는 함수를 넣어줍니다. 

두 번째 파라미터로 원하는 값을 바꿔주면 produce( )함수가 불변성을 유지하면서 새로운 상태를 생성해줍니다. 

 

immer의 핵심은 다음과 같습니다.

불변성을 신경쓰지 않는 것 처럼 코드를 작성하되, 
불변성 관리는 제대로 해주자.

draft.somewhere.deep.inside 처럼 깊은 곳에 위치한 값을 바꾸기도 편리하지만, 배열을 다룰 때도 편리합니다. 

아래의 예제를 보겠습니다. 

import produce from 'immer'

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

const nextState = produce(origianlSate, draft => {
	//id가 2인 항목의 checked값을 true로 설정
    const todo = draft.find(t => t.id === 2); //id로 항목 찾기
    todo.checked = true;
    //or draft[1].checked = true;
    
    //배열에 새로운 데이터 추가
    draft.push({
    	id: 3, 
        todo: '일정 관리 앱에 immer 적용하기',
        checked: false,
    });
    
    //id = 1인 항목을 제거하기
    draft.splice(draft.findIndex(t => t.id === 1), 1);
});

이렇게, 조금 더 복잡한 상황에서 불변성을 유지할 수 있습니다. 

 

4. 1. 4 App 컴포넌트에 immer  적용하기

immer에 대한 사용법을 어느정도 익혔으니, 위의 코드에서 immer를 적용해볼게요.

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;
      // setForm({
      //   ...form,
      //   [name]: [value],
      // });
      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에 새 항목 등록
      setData(
        // ...data,
        // array: data.array.concat(info),
        produce(data, (draft) => {
          draft.array.push(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),
        produce(data, (draft) => {
          draft.array.splice(
            draft.array.findIndex((info) => info.id === id),
            1
          );
        })
      );
    },
    [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를 쓸 때는 

  1. 객체의 값을 직접 수정하거나,
  2. 배열에 직접적인 변화를 일으키는 함수(splice, push...)를 써도 됩니다. 

하지만 immer를 쓴다고 무조건 간결해지는 것은 아닙니다. 

안쓸때가 더 간결하고 좋은 경우가 있어요.

불변성 유지가 복잡해질 때만 immer를 사용하면 돼요.

 

4. 1. 5 useState의 함수형 업데이트와 immer 함께 쓰기

immer의 produce( ) 함수를 사용할 때, 첫번째 파라미터가 함수라면 업데이트 함수를 반환합니다. 

이것과 useState의 함수형 업데이트를 활용하면 코드가 더 깔끔해져요. 

우리의 컴포넌트를 아래와 같이 고쳐볼게요.

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;
      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에 새 항목 등록
      setData(
        produce((draft) => {
          draft.array.push(info);
        })
      );

      // form 초기화
      setForm({
        name: "",
        username: "",
      });

      nextId.current += 1;
    },
    [form.name, form.username]
  );

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

  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와 useState를 활용하여 코드를 더욱 깔끔하게 만들 수 있었습니다. 

다음 포스팅에서는 리액트 라우터를 사용하여 SPA(Single Page Application) 개발을 해볼게요.

그럼안녕