개발은 재밌어야 한다
article thumbnail
반응형

 

React의 컴포넌트에서 상태의 불변성을 유지하는 것은 매우 중요합니다.

불변성은 기존 값을 직접 수정하지 않으면서 새로운 값을 만들어내는 것을 의미합니다.

따라서 배열이나 객체의 값을 변경할 때는 새로운 배열이나 객체를 만들어 필요한 부분을 변경해주어야 합니다.

불변성이 유지되지 않으면 값이 변경되어도 감지하지 못하게 되고 렌더링 성능을 최적화하지 못하는 문제가 발생합니다.

위의 불변성에 대한 정의로, 우리는 불변성을 지키기 위해 필요한 값을 변형해 사용하고 싶다면 어떤 값을 복사본을 만들어 사용해야 합니다.

 

 

 

const user = { name: 'Yeum', age: 25 };

const copyUser = user;
// user와 copyUser 변수에는 같은 참조값 즉, 같은 힙 영역의 메모리를 가지고 있습니다.​

 

위와 같이 사용할 수 있지만 둘은 같은 힙 영역의 메모리를 가지고 있기 때문에 어느 하나가 값을 변경하면 어디에서 값을 바꾸었는지 알 수 없다→ 에러 유발 가능성

그렇기 때문에 참조타입의 경우에는 메모리 주소를 공유하기 보다, 값 자체를 복사해서 전달해주는 것이 좋다.

 

스프레드 문법을 통한 값 복사
const user = { name: 'Yeum', age: 25 };

const copyUser = { ...user };
 

그렇다면, 스프레드 문법으로 복사만 하면 불변성의 다 지켜진것일까? 😕

 

안타깝게도, 아닙니다

위에서 사용했던 스프레드 연산자는 얕은 복사(Shallow Copy) 즉, 한단계까지만 복사하게 됩니다.

그렇기 때문에 객체 안에 또 다른 참조 타입이 있다면 본래 힙 메모리가 또 복사되게 됩니다.

 

const user = { name: 'Nam', age:29, friends: ['Kang', 'Bae']};
const otherUser = { ... user};

user.name = 'Lee';
user.friend.push('jung')

console.log(user === otherUser) // false
console.log(user.friends === otherUser.friends) // true

 

이와 같이 객체의 구조가 복잡해 질수록 불변성의 유지가 어려워 지게 됩니다.

그래서 Immer와 같은 라이브러리를 사용하게 되는 이유입니다.

 

 

12.1 immer를 설치하고 사용법 알아보기

 

12.1.1 프로젝트 준비

 

 

12.1.1 프로젝트 준비
 
yarn create react-app immer-tutorial

cd immer-tutorial

yarn add immer​

 

 

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

먼저 immer를 사용하지 않고 불변성을 유지하면서 값을 업데이트하는 컴포넌트를 작성

App.js
import React, { 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
          type="text"
          name="username"
          placeholder="username"
          value={form.username}
          onChange={onChange}
        />
        <input
          type="text"
          name="name"
          placeholder="name"
          value={form.name}
          onChange={onChange}
        />
        <button type="submit">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를 사용하지 않고 불변성 유지

 

12.1.3 immer 사용법

immer를 사용하면 불변성을 유지하는 작업을 매우 간단하게 처리할 수 있습니다.

예시 코드
import produce from 'immer';
const nextState = produce(originalState, draft => {
    // 바꾸고 싶은 칸 바꾸기
    draft.somewhere.deep.inside = 5;
})

 

produce라는 함수는 두가지 파라미터를 받습니다.

첫번째는 수정하고 싶은 상태이고, 두번째 파라미터는 상태를 어떻게 업데이트할지 정의하는 함수입니다.

 

두번째 파라미터로 전달되는 함수 내부에서 원하는 값을 변경하면, produce 함수가 불변성을 유지를 대신해 주면서 새로운 상태를 생성해 줍니다.

 

좀 더 복잡한 데이터를 불변성을 유지하면서 업데이트하는 예시입니다.

예시 코드
import produce form 'immer';

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

const nextState = produce(originalState, draft => {
    // id가 2인 항목의 checked 값을 true로 설정
    const todo = draft.find(t => t.id === 2); // id로 항목 찾기
    todo.checked = true;
    // 혹은 draft[1].checked = true;

    // 배열에 새로운 데이터 추가
    draft.push({
        id: 3,
        todo: '일정 관리 앱에 immer 적용하기',
        checked: false,
    });

    // id = 1인 항목을 제거하기
    draft.splice(draft.findIndex(t=>t.id === 1), 1);
});​

 

12.1.4 컴포넌트에 immer 적용하기

방금 만든 App 컴포넌트에  immer를 적용하여 더 깔끔한 코드로 수정해보자

App.js
import React, { 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]
//      });
      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({
//        ...data,
//       array: data.array.filter(info => info.id !== id)
//      });
      setData(
        produce(data, draft => {
          draft.array.push(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)
//      });
      setData(
        produce(data, draft => {
          draft.array.splice(draft.array.findIndex(info => info.id === id), 1);
        })
      );
    }, [data]
  );

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input
          type="text"
          name="username"
          placeholder="username"
          value={form.username}
          onChange={onChange}
        />
        <input
          type="text"
          name="name"
          placeholder="name"
          value={form.name}
          onChange={onChange}
        />
        <button type="submit">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를 사용하여 컴포넌트 상태를 작성할 때는 객체 안에 있는 값을 직접 수정하거나, 배열에 직접적인 변화를 일으키는 push, splice 등의 사용을 사용해도 무방

그렇기 때문에 불변성 유지에 익숙하지 않아도 자바스크립트에 익숙하다면 컴포넌트 상태에 원하는 변화를 쉽게 반영 시킬 수 있다.

immer를 사용한다고 해서 무조건 코드가 간결해지지는 않습니다.

 onRemove의 경우에는 배열 내장 함수 filter를 사용하는 것이 코드가 더 깔끔하므로, 굳이 immer를 적용할 필요가 없다

immer는 불변성을 유지하는 코드가 복잡할 때만 사용해도 충분하다.

 

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

 

11장에서 useState의 함수형 업데이트에 대해 알아봤습니다.

예시코드
 
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 = {
    valuel: 1,
    foo: 'bar',
};
const nextState = update(originalState);
console.log(nextState); // {value:2, foo: 'bar'}

 

 

이렇게 immer속성과 useState의 함수형 업데이트를 함께 활용하여 코드를 더욱 깔끔하게 만들 수 있다.

App.js
import React, { 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 등록을 위한 함수
  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;

 

 

12.2 정리

라이브러리인 immer에 대해 알아보았습니다. 이 라이브러리는 컴포넌트의 

상태 업데이트가 조금 까다로울 때 사용하면 매우 좋습니다,. 추후 상태 관리 라이브러리인 리덕스를 배워서 사용할 때도 immer를 쓰면 코드를 매우 쉽게 작성할 수 있습니다.

이러한 라이브러리는 편의를 위한 것이므로 꼭 필요하지 않지만, 사용한다면 생산성을 크게 높일 수 있습니다. 만약 immer를 사용하는것이 오히려 불편하게 느껴진다면

사용하지 않아도 좋습니다.

 

 

 

 

 

 

 

반응형

'javascript > React' 카테고리의 다른 글

리덕스 라이브러리 이해하기  (0) 2023.04.05
[React] 리액트 기초 예시 -2  (0) 2022.07.30
[React] 리액트 기초 예시 - 1  (2) 2022.07.29
profile

개발은 재밌어야 한다

@ghyeong

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!