react

immer를 이용해 깊은 복사를 케이크처럼 간단히 먹자

냠냠맨 2023. 4. 8. 16:51

🐕 깊은 복사 구현하기 개 귀찮다.

리액트는 불변성을 유지해줘야 함과 동시에 객체의 중첩구조가 쉽게 만들어지는 모순을 갖고있습니다.

간편하게 쓸 수 있는 애들은 죄다 shallow copy이기 때문에

깊은 중첩 구조를 가진 애들에게는 얕은 복사를 여러번 해야하는 수고로움이 있어요

 

따라서 깊은 복사로 한번에 불변성을 유지해주면 참 편할 것입니다.

const object = {
  somewhere: {
    deep: {
      inside: 3,
      array: [1, 2, 3, 4],
    },
  },
};

이따구로 된 중첩구조에서 하나하나 스프레드 연산자 다 넣어주면 어지럽겠다 그죠

 

https://xionwcfm.tistory.com/189

 

getter,setter까지 복사하는 deep copy를 구현하자

😎Shallow copy, Deep copy라고도한다. 쉽게 설명하면 얕은 복사는 depth 1까지만 복사를 수행해주는 것 깊은 복사는 depth 2이상도 잘 복사해주는 것입니다. 얕은 복사는 쉽게 수행할 수 있지만. 깊은 복

xionwcfm.tistory.com

근데 깊은 복사를 구현하는게  왜 귀찮은지는 제 예전 포스트에서 이유를 찾을 수 있습니다.

굳이 저거 만들어서 쓰시고 싶다면 말리진 않겠지만 그냥 갖다쓰는게 더 빠르긴 하겠네요

이번엔 갖다 써보는것을 한번 해보려고 합니다.


👻immer를 쓰고싶으면 immer를 깔아라...

yarn add immer

전 yarn을 쓰고 있으니 yarn add 명령어로 추가해줬습니다.


🥶 immer의 기본적인 사용법

let { produce } = require('immer');

console.log(produce);

let originalState = {
  daeun: {
    islegend: 'inziyong',
    minjae: [1, 2, 3, 4, 5],
  },
};
const nextState = produce(originalState, (draft) => {
  originalState.daeun.minjae[0] = 500;
});

console.log(nextState);
console.log(originalState);
console.log(originalState.daeun.minjae === nextState.daeun.minjae);

처음엔 이렇게 코드를 짰는데 원본도 바뀌길래 뭐임? 하고 있다가

다시 사용법을 잘 살펴보니 원본 객체에 접근하는게 아니라

콜백의 인자로 받은 draft에 접근해야하는거였다.

let { produce } = require('immer');

console.log(produce);
// [Function (anonymous)]

let originalState = {
  daeun: {
    islegend: 'inziyong',
    minjae: [1, 2, 3, 4, 5],
  },
};
const nextState = produce(originalState, (draft) => {
  draft.daeun.minjae[0] = 500;
});

console.log(nextState);
// { daeun: { islegend: 'inziyong', minjae: [ 500, 2, 3, 4, 5 ] } }
console.log(originalState);
//{ daeun: { islegend: 'inziyong', minjae: [ 1, 2, 3, 4, 5 ] } }
console.log(originalState.daeun.minjae === nextState.daeun.minjae);
//false

따라서 이렇게 콜백의 인자에 접근해서 값을 바꿔주면?

값이 아주 멋지게 바뀌는것을 볼 수 있다.

와 너무 편한데?

 

import 문으로 불러오고 싶었지만 아래와 같은 자잘한 오류가 발생해서

(node:4536) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
d:\REACT PROJECT\immer-tutorial\src\produce.js:1
import { produce } from 'immer';
^^^^^^

그냥 require문으로 불러와서 객체디스트럭처링 할당해서 써봤다.

저거 package.json 수정하는게 귀찮음


🌞진짜 실습

let { produce } = require('immer');

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

const nextState = produce(originalState, (draft) => {
  const todo = draft.find((t) => t.id === 2);
  todo.checked = true;

  draft.push({
    id: 3,
    todo: '일정 관리 앱에 immer 적용하기',
    checked: false,
  });

  draft.splice(
    draft.findIndex((t) => t.id === 1),
    1,
  );
});

console.log(nextState);

화살표 함수에 유의하면서 작성합시다.

만약 중괄호를 축약하고 싶지않다면 명시적으로 리턴문을 작성해줄것..

  const todo = draft.find((t) => {
    return t.id === 2;
  });

이렇게 말이에요

그리고 살짝 헷갈릴 수 있는 부분이 하나 더 있는데

  draft.splice(
    draft.findIndex((t) => t.id === 1),
    1,
  );

요부분은 splice 메서드를 지정된 인덱스만 제거하는 것이기때문에

splice 메서드 안에 findIndex 메서드가 첫번째 인자로 들어가는 거고

두번째 인자는 splice 메서드가 어느정도의 길이를 자를지를 정해주는 것...

[
  { id: 2, todo: 'immer로 유지', checked: true },
  { id: 3, todo: '일정 관리 앱에 immer 적용하기', checked: false }
]

잘 했으면 이렇게 값이 잘 출력되는 것을 확인할 수 있습니다.


😋이제 실제 프로젝트에 적용하기

 

간단하게 등록버튼을 누르면

아이디와 이름이 있는 리스트가 생성되고

리스트를 클릭하면 지워지는 프로그램을 만들것입니다.

import React, { useCallback, useState, useRef } from 'react';
import produce from 'immer';
import { Fragment } from 'react';

const App = () => {
  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(
        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,
          );
        }),
      );
    },
    [data],
  );

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

export default App;

 

 

 

 

에에.... 근데 책의 예제 코드가 좀 이상해요

전 위에서 분명 produce 함수의 첫번째 인자로는 베이스가 될 객체가 들어가야하고

두번째 인자로 함수를 넣어줘야한다고 배웠는데

책에서는 왜 냅다 첫번째 인자로 함수를 집어넣어버리는거죠?

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

이렇게 작성해야할 것 같은데

책에서는

setData(
  produce(draft => {draft.array.push(info)})
)

이런식으로 냅다 집어 넣어 버려요

왜 그런가 했는데

immer에서 제공하는 produce 함수를 호출할 때 첫 번째 파라미터가 함수 형태라면

업데이트 함수를 반환한다고 하네요

let { produce } = require('immer');

const update = produce((draft) => {
  draft.value = '난 업데이트';
});

const originalState = {
  value: 1,
  foo: 'bar',
};

const nextState = update(originalState);
console.log(nextState);
//{ value: '난 업데이트', foo: 'bar' }

와... 그러니까 produce 함수가 함수를 반환하게 만들어 놓고

그 함수를 setData 에 인자로 집어넣는 식으로 코드를 짰다?

무친...


🐶마치며

immer의 produce 함수 사용법도 간단한데

정말 편리하네요

이제 스프레드지옥에서 벗어날 수 있나봐요 너무 신나


이해했다고 생각할 때가 가장 무서울 때다.

 

반응형