본문 바로가기

프론트엔드(Front-End)/React

[REACT] Immer와 불변성

Immer란??

immer에 대해 이야기 하기 전 우리는 리액트의 함수형 컴포넌트Hook에 대하여 이해해야 합니다.

 

https://1-hee.tistory.com/27

 

[REACT] 리액트의 컴포넌트(COMPONENT)

1. REACT의 컴포넌트(Component)란? 리액트는 화면을 컴포넌트(Component) 단위로 구성합니다. 컴포넌트란 화면을 구성할 수 있는 각각의 자바스크립트 파일들을 의미하며 root 태그로 묶여있습니다. 리

1-hee.tistory.com

 

https://1-hee.tistory.com/33

 

[REACT] 리액트와 훅(HOOK)

리액트의 훅(Hook)이란? 리액트의 함수형 컴포넌트는 클래스형 컴포넌트와 같은 문법으로 state를 생성, 작성, 수정 등이 어렵습니다. 그래서 함수형 컴포넌트에서도 클래스형 컴포넌트처럼 state나

1-hee.tistory.com

 

 

리액트에서 함수형 컴포넌트에서 어떤 state에 담긴 배열 또는 자바스크립트 객체는 값이 추가되거나 삭제될때 기존의 상태를 보전해야합니다. 가령 10개의 데이터가 담긴 배열에 데이터 1개를 추가한다고 해서 이전의 배열이 새로 삽입되는 데이터로 덮어쓰여서는 안된다는 뜻입니다. 애초에 이 문장에서 모순이 존재하죠? 그래서 리액트에서는 이렇게 기존의 데이터를 유지하면서 새로운 데이터가 추가 삭제 될 수 있도록 하기 위해 Shallow copy를 사용하게 됩니다. 이때 사용하는 것이 ... 연산자 인데, 이것은 ECMA JavaScript에서 기본적으로 지원하는 문법입니다.

 

이처럼 리액트에서 이렇게 Shallow copy와 같은 문법을 사용하여 state 등에 저장된 데이터를 보전하는 것을 불변성 을 유지한다고 말합니다. 이런 맥락에서 ... 또한, 불변성을 지키기 위한 좋은 방법이 될 수 있지만 프로젝트의 규모가 커지고 핸들링해야하는 데이터가 늘어날수록 분명 ... 라는 문법도 굉장히 간편한 문법이었지만, 나중에는 반복되고 귀찮은 작업으로 전락하게 됩니다.

 

그래서 이에 대한 해결 방안으로 immer 라고 하는 별도의 라이브러리를 사용하게 되는데, 이것은 우리가 useState를 React로부터 import하여 사용하는 것과 다르게 immer 로부터 import하여 사용하기에 엄연히 구분짓다면 리액트와는 조금 다른 별도의 라이브러리입니다.

 

하지만, 앞으로 리액트를 개발하면서 이렇게 리액트와 궁합이 좋은 여러가지 라이브러리를 자연스럽게 접하게 될 것이고, 생각보다 흔한 상황이므로 이부분에 익숙해지면 좋을 것입니다.

 

 

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

  • 불변성에 대한 이해를 돕기 위해 input 태그를 사용해 리스트를 추가하고 삭제하는 예제를 만들어 보겠습니다.
import "./App.css";

// Hooks Import
import { useRef, useCallback, useState } from "react";

function 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(
	      { // immer가 없을 경우
	        ...form,
	        [name]: value,
        }
      );
    },
    [form]
  );

  // form 등록을 위한 함수
  const onSubmit = useCallback(
    (e) => {
      e.preventDefault();
      const info = {
        id: nextId.current,
        name: form.name,
        username: form.username,
      };

      // array에 새 항목 등록
      setData(
        { // immer가 없을 경우
         ...data,
         array: data.array.concat(info),
        }
      );

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

  // 항목을 삭제하는 함수
  const onRemove = useCallback(
    (id) => {
      setData();
      { // immer가 없을 경우!
       ...data,
       array: data.array.filter((info) => info.id !== id),
      }      
    },
    [data]
  );

  return (
    <>
      <form onSubmit={onSubmit}>
        <input name="username" placeholder="ID를 입력하세요" 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>
    </>
  );
}

export default App;
  • immer가 없을 때는 위와 같이 … 연산자를 통해 불변성을 유지할 수 있습니다.

 


immer 사용하기

import produce from "immer";
  • immer는 위와 같이 리액트 컴포넌트(.js) 소스코드의 상단에 import 합니다.

 

리액트 컴포넌트와 immer를 사용해 불변성 유지하기

import "./App.css";

// Hooks Import
import { useRef, useCallback, useState } from "react";
import produce from "immer";

function App() {
  const nextId = useRef(1);
  const [form, setForm] = useState({ name: "", username: "" });
  const [data, setData] = useState({
    array: [],
    uselessValue: null,
  });
  /*
     immer에서 제공하는 produce 함수를 호추할 때 첫 번째 파라미터가 함수형태라면 업데이트 함수를 반환함.
  */

  // input 수정을 위한 함수
  const onChange = useCallback(
    (e) => {
      const { name, value } = e.target;
      setForm(
        // { // immer가 없을 경우
        // ...form,
        // [name]: value,
        // }
        produce(form, (draft) => {
          // immer를 사용한 경우
          draft[name] = value;
        })
        // immer를 간략하게 사용하는 경우 !
        // 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(
        // { // immer가 없을 경우
        // ...data,
        // array: data.array.concat(info),
        // }
        produce(data, (draft) => {
          // immer를 사용한 경우
          draft.array.push(info);
        })
        // immer를 간략하게 사용하는 경우 !
        // produce((draft) => {
        //   draft.array.push(info);
        // })
      );

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

  // 항목을 삭제하는 함수
  const onRemove = useCallback(
    (id) => {
      setData(
        // {
        // // immer가 없을 경우!
        // ...data,
        // array: data.array.filter((info) => info.id !== id),
        // }
        // produce(data, (draft) => {
        // immer를 사용한 경우
        //   draft.array.splice(
        //     draft.array.findIndex((info) => info.id === id),
        //     1
        //   );
        // });
        // immer를 간략하게 사용하는 경우 !
        produce((draft) => {
          draft.array.splice(
            draft.array.findIndex((info) => info.id === id),
            1
          );
        })
      );
    },
    [data]
  );

  return (
    <>
      <form onSubmit={onSubmit}>
        <input name="username" placeholder="ID를 입력하세요" 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>
    </>
  );
}

export default App;

 

 

immer를 통해 불변성이 잘 유지되어 리스트 화면에 잘 잘동하는 모습