[react] 'immer' 로 쉽게 불변성 유지하기💫

호박쿵야·2021년 12월 8일
1

react

목록 보기
6/7
post-thumbnail

immer

react에서 쉽게 불변성을 유지할 수 있는 코드 작성을 도와주는 라이브러리이다.

reference
Immer 공식 사이트 / inroducing immer!

불변성은 뭘까?🤔

리액트 컴포넌트에서 상태를 업데이트 할 때 불변성을 지키는 것은 매우 중요하다. '불변성을 지킨다' 라는 것은 기존의 값을 직접 수정하지 않으면서 새로운 값을 만들어 내는 것을 말한다.

예를 들어보자.
1부터 5까지의 숫자가 들어있는 array의 0번째 요소를 수정해야 한다. 이때 nextArrayBadarray를 가리키고 있는 변수이고 nextArrayGoodarray를 복사한 다른 배열이다.

const array = [1, 2, 3, 4, 5];

//Bad Case
const nextArrayBad = array;
nextArrayBad[0] = 100
console.log(`nextArrayBad[0] : ${nextArrayBad[0]} / array[0]: ${array[0]}`);

array[0] = 1;
//Good Case 
const nextArrayGood = [...array]
nextArrayGood[0] = 100;
console.log(`nextArrayGood[0] : ${nextArrayGood[0]} / array[0]: ${array[0]}`);

👉 실행 결과

불변성이 지켜지지 않으면 객체 내부의 값이 새로워져도 바뀐 것을 감지하지 못한다. 이것은 다른 문제들을 불러올 수 있다 (ex 컴포넌트 최적화를 위한 react.memo 사용 못함 )

object 의 경우도 마찬가지이다.

const object ={
    id:1, 
    name :"pumpkin"
}

//Bad Case 
const nextObjBad = object;
nextObjBad.name = "carrot"
console.log(`nextObjBad.name : ${nextObjBad.name} / object.name: ${ object.name}`);

//Good Case
const nextObjGood = {...object, name: "apple"}
console.log(`nextObjGood.name : ${nextObjGood.name} / object.name: ${ object.name}`);

👉 실행 결과

이와 같이 전개 연산자 (...*) 를 사용하여 객체나 배열 내부의 값을 복사할 때는 얕은 복사를 하게 된다. 즉, 내부의 값이 완전히 새로 복사되는 것이 아니라 가장 바깥쪽에 있는 값만 복사된다. 따라서 내부 값이 배열이거나 객체일 때 내부의 값 또한 따로 복사해주어야 한다.

const members = [
    {
        id :1,
        name:'pumpkin',
        age:25
    },
    {
        id :2,
        name:'carrot',
        age:23
    },
    {
        id :3,
        name:'apple',
        age:30
    }
]

const nextMembers = [...members]

//얕은 복사
nextMembers[0].name ='mango'
console.log(`nextMembers[0].name : ${nextMembers[0].name} / members[0].name : ${members[0].name}`)

//깊은 복사
nextMembers[0] ={
    ...nextMembers[0],
    name:"melon"
}
console.log(`nextMembers[0].name : ${nextMembers[0].name} / members[0].name : ${members[0].name}`)

👉 실행 결과

그런데 만약 이보다 더 객체의 구조가 복잡해진다면 불변성을 유지하면서 변경 내용을 업데이트 하는 것은 매우 까다로워 진다.

//객체 안에 객체가 있는 구조일 때 
const nextComplexObj ={
    ...complexObj,
    objectInside : {
        ...complexObj.objectInside,
        value : " "
    }
}

이러한 복잡한 구조일 경우에 immer를 통해 쉽고 편하게 불변성을 유지할 수 있다.
그럼 이제 immer를 설치하고 실제로 사용해보자!

immer 설치 및 사용법

준비!

먼저 사용할 프로젝트에 yarn 을 이용해서 immer를 설치해준다.
$ yarn add immer
이름과 아이디를 받아 멤버의 리스트를 보여주는 컴포넌트를 만들어보자

🧩MemberList.js

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

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

  const onChange = useCallback(
    (e) => {
      const { name, value } = e.target;
      setForm({
        ...form,
        [name]: [value],
      });
    },
    [form]
  );

  const onSubmit = useCallback(
    (e) => {
      e.preventDefault();
      const info = {
        id: nextId.current,
        name: form.name,
        username: form.username,
      };
      setData({
        ...data,
        array: data.array.concat(info),
      });

      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 MemberList;

👉 실행 결과

아직 immer를 사용하지 않았지만 크게 문제는 없어보인다. 하지만 이보다 더 복잡한 구조의 데이터를 가진다면 불변성 유지를 위해 더욱 길고 복잡한 코드를 작성하게 된다.

immer 사용법

MemberList 컴포넌트에 immer를 적용해보자.

🧩MemberList.js

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

const MemberList = () => {
 ...
  const onChange = useCallback(
    (e) => {
      const { name, value } = e.target;
      setForm(
        produce(form, (draft) => {
          draft[name] = value;
        })
      );
    },
    [form]
  );

  const onSubmit = useCallback(
    (e) => {
      e.preventDefault();
      const info = {
        id: nextId.current,
        name: form.name,
        username: form.username,
      };
      setData(
        produce(data, (draft) => {
          draft.array.push(info);
        })
      );

      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
          );
          //draft.array.filter(info => info.id!==id)
        })
      );
    },
    [data]
  );
  return (
    ...
  );
};

export default MemberList;

produce 라는 함수는 두가지 파라미터를 받는다. 첫 번째 파라미터는 수정하고 싶은 상태이고, 두 번째 파라미터는 상태를 어떻게 업데이트할 지 정의하는 함수이다.
두 번째 파라미터로 전달되는 함수 내부에서 원하는 값을 변경하면, produce 함수가 불변성 유지를 대신 해주면서 새로운 상태를 생성해준다.

이 라이브러리의 핵심은 불변성에 신경쓰지 않는 것처럼 코드를 작성하되 불변성 관리는 제대로 해주는 것이다. immer를 사용하여 컴포넌트 상태를 작성할 때는 객체 안에 있는 값을 직접 수정하거나, 배열에 직접적인 변화를 일으키는 push, splice 등의 함수를 사용할 수 있다.

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

	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,
        name : "pumpkin"
    }
    const nextState = update(originalState)
    console.log(nextState) //{value:2, name:"pumpkin"}                    

이러한 속성과 useState의 함수형 업데이트를 함께 활용하면 코드를 더욱 깔끔하게 만들 수 있다. produce의 파라미터를 함수 형태로 사용하면 아래와 같이 작성할 수 있다.

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

...
  const onChange = useCallback((e) => {
    const { name, value } = e.target;
    setForm(
     	produce((draft) => {
        draft[name] = value;
      })
    );
  }, []);

  const onSubmit = useCallback(
    (e) => {
      e.preventDefault();
      const info = {
        id: nextId.current,
        name: form.name,
        username: form.username,
      };
      setData(
        produce((draft) => {
          draft.array.push(info);
        })
      );

      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 (
    ...
  );
};

export default MemberList;

0개의 댓글