12장 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],
	  });
	},
	[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>
  );
};

전개 연산자 or 배열 내장 함수 사용해서 객체를 새롭게 만들어서 불변성을 유지한다.
→ 상태가 복잡해지면 귀찮아질 수 있다.

immer 사용

immer 의 produce 함수 사용해서 불변성 유지
produce 함수 2가지 인자를 받음

  1. 수정하고 싶은 state
  2. state를 어떻게 업데이트 할 지 정의하는 함수(콜백 함수)

콜백 함수에 state가 인자로 넘겨진다.

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

콜백 함수 내부에서 값을 변경하면, 불변성 유지를 대신해주면서 새로운 상태를 생성해줌.
→ 그냥 코드를 작성하는데 알아서 불변성 관리를 해준다.

즉, 지금까지 state 변경할 때, 기존 배열이나 객체 스프레드 문법,concat으로 불러와서 새로운 객체 생성해서 불변성 유지하던 것을 쉽게 해준다.

// 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.concat(info),
      // });
      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({
      //   ...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]
  );

그냥 기존의 state를 바꿔버리는 것 같지만, 불변성을 유지해준다.
그렇다고 해서 만능은 아니고 filter 같은 경우 오히려 produce사용하는 게 더 복잡하다.

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

useState의 함수형 업데이트 전에 나왔는데, immer에서도 적용가능
produce 함수 호출할 때, 첫번째 파라미터가 함수 형태면, 업데이트 함수를 반환

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

근데 애초에 produce안에 함수 형태가 들어가서, 업데이트 함수 형태로 사용하는게 state 빠진 느낌.
이를 이용해서 useState의 함수형 업데이트와 함께 사용할 수 있다.
setState함수안에 업데이트 함수를 반환해줘서 오히려 더 간결해짐.

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