[React] 10. Immer Lib

2022. 7. 17. 14:00·Frontend/React
목차
  1. 1. Spread Operator의 문제점
  2. 2. immer
이 글은 김민준(velopert)님의 리액트를 다루는 기술을 참조하였습니다.

얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)의 이론 개념보다는

중첩 객체의 깊은 복사하는 방법에 대해 포스팅할 것이다.

 

목차
1. Spread Operator의 문제점
2. immer

1. Spread Operator의 문제점

 

프로그래밍 언어를 하나라도 배워본 사람이라면 얕은 복사와 깊은 복사에 대한 차이를 알 것이다.

 

[React] 6. 불변성과 Side-Effect

이 부분을 건너뛰고 공부하려고 했는데, setEffect가 side-effect를 유발하기 위한 Hooks이라는 말을 듣고 의문이 들었다. 리액트는 side-effect를 방지하는 것이 원칙이라면서 이런 기능을 제공한다고? 그

jaeseo0519.tistory.com

리액트에서 불변성의 중요성은 이전 포스팅에서 다뤘으므로 생략.

 

중요한 것은 객체의 경우에 아무 생각없이 다른 변수에 정보를 넘겨주려 하다가는

같은 주소를 공유하게 되어 원본 데이터가 변결될 수도 있다는 것이므로 깊은 복사를 해야 한다는 것이다.

 

JS를 공부할 때, 분명 참조형 자료형의 경우엔 Spread Operator로 깊은 복사를 할 수 있다고 처음에 배웠다.

그렇다면 "이것만 알고 있으면 되는 거 아닌가?"라고 할 수 있겠지만 다음의 경우를 살펴보자.

 

user1의 내용을 전개 연산자를 이용해 user2에게 넘겨주고 user2의 레벨을 변경했을 때, user1의 레벨은 그대로 유지된다.

그렇다면 객체 안에 객체가 중첩된 경우는 어떨까?

 

person 객체 정보를 cp_person 변수에게 전개 연산자로 복사시키고 나이를 변경했을 때는 상관이 없다.

 

그런데 한 단계 더 들어간 깊이에선 같은 주소를 가리키고 있다.

즉, 두 단계 이상의 depth부터는 깊은 복사가 이루어지지 않는다는 의미가 된다.

이걸 만약 전개 연산자로 해결하려고 한다면 두 번 사용하면 된다.

 

이번엔 서로 다른 참조값을 가지고 있음을 알 수 있다. 그런데 만약 depth가 10이라면?

미련하게 전개 연산자 10번 사용하는 건 가독성을 망치는 지름길이다.

이런 경우에 사용할 수 있는 방법을 알아보자.

 


2. immer

 

bash에 npm add immer을 입력해 설치해주자. 기본구조는 이렇게 생겼다.

import produce from 'immer';

const nextState = produce(originalState, draft => {
   // 변경할 값 바꾸기
   draft person.tech_stack[3] = "JavaScript";
})

produce 함수는 수정하고 싶은 상태와 업데이트 방법에 대해 정의한 함수를 파라미터로 전달받는다.

두 번째 파라미터의 내부에서 값을 변경하면, produce함수가 알아서 불변성 유지와 새로운 상태 생성을 관리해준다.

 

immer 라이브러리는 단순히 깊은 곳의 값을 바꾸는 것 뿐만 아니라, 배열을 처리할 때도 간편해진다.

import produce from 'immer';

const originalState = [
  {
    id: 1,
    tech_stack: "C++",
    checked: true,
  },
  {
    id: 2,
    tech_stack: "Python",
    checked: true,
  },
  {
    id: 3,
    tech_stack: "Java",
    checked: true,
  }
]

const nextState = produce(originalState, draft => {
  // id가 3이면 checked 값을 true로 고정
  const tech_stack = draft.find(x => x.id === 3);
  tech_stack.checked = true;

  // 새로운 데이터 추가
  draft.push({
    id: 4,
    tech_stack: "JavaScript",
    checked: false,
  });

  // id 1인 항목 제거
  draft.splice(draft.findIndex(x => x.id === 1), 1)
});

물론 그렇다고 해서 immer을 사용한다고 코드가 무조건 더 간결해지진 않는다.

언제나 그렇지만 닭 잡는데 소 잡는 칼을 쓰지말자.

 

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

 

우선, immer을 사용하지 않고 불변성을 유지하는 예를 보자.

import React, { useCallback, useRef, 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;

대충 이런 역할을 하는 앱이다.

 

아무래도 너무 기니까 immer만 사용해서 수정하면 다음과 같이 된다.

import React, { useCallback, useRef, 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(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(
        produce(data, (draft) => {
          draft.array.push(info);
        })

        // {
        //   ...data,
        //   array: data.array.concat(info),
        // }
      );

      //form 초기화
      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,
        //   array: data.array.filter((info) => info.id !== id),
        // }
      );
    },
    [data]
  );

  [ ...]
};

export default App;

 

그런데 사실 produce 함수는 첫 번째 파라미터가 함수 형태면 리턴 값으로 업데이트 함수를 반환한다.

그말은 즉슨 useState로 만들어진 중첩 객체를 수정할 때, immer와 함께 사용할 수가 있게 된다.

 

import React, { useCallback, useRef, 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 등록을 위한 함수
  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);
        })

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

  ( ... )
};

export default App;

 

 

저작자표시 비영리 (새창열림)
  1. 1. Spread Operator의 문제점
  2. 2. immer
'Frontend/React' 카테고리의 다른 글
  • [React] 12. React-Redux
  • [React] 11. Context API
  • [React] 9. 최적화
  • [React] 8. 외부 API 연동
나죽못고나강뿐
나죽못고나강뿐
싱클레어, 대부분의 사람들이 가는 길은 쉽고, 우리가 가는 길은 어려워요. 우리 함께 이 길을 가봅시다.
  • 나죽못고나강뿐
    코드를 찢다
    나죽못고나강뿐
  • 전체
    오늘
    어제
    • 분류 전체보기 (450)
      • Computer Science (59)
        • Git & Github (4)
        • Network (17)
        • Computer Structure & OS (13)
        • Software Engineering (5)
        • Database (9)
        • Security (4)
        • Concept (7)
      • Frontend (21)
        • React (13)
        • Android (4)
        • iOS (4)
      • Backend (74)
        • Spring Boot & JPA (47)
        • Django REST Framework (14)
        • MySQL (8)
        • Nginx (1)
        • FastAPI (4)
      • DevOps (24)
        • Docker & Kubernetes (11)
        • Naver Cloud Platform (1)
        • AWS (2)
        • Linux (6)
        • Jenkins (0)
        • GoCD (3)
      • Coding Test (112)
        • Solution (104)
        • Algorithm (7)
        • Data structure (0)
      • Reference (134)
        • Effective-Java (90)
        • Pragmatic Programmer (0)
        • CleanCode (11)
        • Clean Architecture (2)
        • Test-Driven Development (4)
        • Relational Data Modeling No.. (0)
        • Microservice Architecture (2)
        • 알고리즘 문제 해결 전략 (9)
        • Modern Java in Action (0)
        • Spring in Action (0)
        • DDD start (0)
        • Design Pattern (6)
        • 대규모 시스템 설계 (6)
        • JVM 밑바닥까지 파헤치기 (4)
      • Service Planning (2)
      • Side Project (5)
      • AI (0)
      • MATLAB & Math Concept & Pro.. (1)
      • Review (15)
      • Interview (1)
      • IT News (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 깃
  • 공지사항

    • 취업 전 계획 재조정
    • 취업 전까지 공부 계획
    • 앞으로의 일정에 대하여..
    • 22년 동계 방학 기간 포스팅 일정
    • 중간고사 기간 이후 포스팅 계획 (10.27~)
  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
나죽못고나강뿐
[React] 10. Immer Lib

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.