본문 바로가기

react

[리액트를 다루는 기술] 12장 immer를 사용하여 더 쉽게 불변성 유지하기

 

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

 

- 전개 연산자와 배열의 내장 함수를 사용하면 간단하게 배열 혹은 객체를 복사하고 새로운 값을 덮어 쓸 수 있음

- 하지만 객체의 구조가 엄청나게 깊어지면 불변성을 유지하면서 이를 업데이트하는 것이 매우 힘듬

 

- immer 라이브러리를 사용하면, 구조가 복잡한 객체도 매우 쉽고 짧은 코드를 사용하여 불변성을 유지하면서 업데이트해 줄 수 있음 

 

- 컴포넌트의 상태 업데이트가 조금 까다로울 때 사용하면 좋음 

 

npm install immer

immer - npm (npmjs.com)

 

immer

Create your next immutable state by mutating the current one. Latest version: 10.0.3, last published: a month ago. Start using immer in your project by running `npm i immer`. There are 4200 other projects in the npm registry using immer.

www.npmjs.com

 

 

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

 

App.js

 

- 폼에서 아이디/이름을 입력하면 하단 리스트에 추가되고, 리스트 항목을 클릭하면 삭제되는 컴포넌트 

import { 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
          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.1.2 immer 사용법 

 

ex)

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

 

- produce 함수는 두가지 파라미터를 받음

  - 첫 번째 파라미터 = 수정하고 싶은 상태 

  - 두 번째 파라미터 = 상태를 어떻게 업데이트할지 정의하는 함수 

 

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

 

ex)

import produce from 'immer';

//수정하고 싶은 상태 
const originalState = [
    {
        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.3 App 컴포넌트에 immer 적용하기 

 

App.js

 

- immer를 사용하여 컴포넌트 상태를 작성할 때는 객체 안에 있는 값을 직접 수정하거나, 배열에 직접적인 변화를 일으키는 push, splice 등의 함수를 사용해도 무방 

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

      // 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]
  );

  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.1.4 useState의 함수형 업데이트와 immer 함께 쓰기 

 

- useState의 함수형 업데이트 

 

ex)

const [number, setNumber] = useState(0);
//prevNumbers는 현재 number 값을 가리킴
const onIncrease = useCallback(
    ()=> setNumber(prevNumber => prevNumber +1),
    [],
);

 

- immer에서 제공하는 produce 함수를 호출할 때, 첫 번째 파라미터가 함수 형태라면 업데이트 함수를 반환 

 

ex)

const update = produce(draft => {
    draft.value = 2;
});

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

const nextState = update(originalState);
console.log(nextState)l //{value:2, foo:'bar'}

 

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

 

App.js

...
setForm (
	produce(draft => {
    	draft[name] = value;
    });
...
setData (
	produce(draft => {
    	draft.array.push(info);
    });
...
produce(draft => {
	draft.array.splice(draft.array.findIndex(info => info.id === id),1);