카테고리 없음

React에서 useImmer를 사용하여 쉽게 불변성 관리하기

Neda 2023. 10. 8. 22:38

React에서 useImmer를 사용하여 쉽게 불변성 관리하기

use-immer 라이브러리

useImmer는 Immer 라이브러리를 통해 만들어진 React Hook이다

기본 사용법은 React의 내장 훅인 useState과 거의 비슷하다

useState가 "set + 상태 이름"을 세터 함수 이름으로 쓴다면

useImmer에서는 "update + 상태 이름"을 업데이트 함수 이름으로 사용한다.

const [state,setState] = useState(initialState);
const [state,updateState] = useImmer(initialState);

 

use-immer로 상태 업데이트 하기

단일 값 업데이트

단일 값에 대한 업데이트는 useState와 동일하게 그대로 인자로 넣어 호출하면 된다.

 const handleChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
    updateInput(e.target.value);
  };

 

참조 값 업데이트

useState의 setState 함수 안에서 이전 상태를 가져올 수 있듯이

useImmer에서는 draft라는 이름으로 보통 이전 상태를 가져온다

다른 점은 useImmer는 알아서 불변성을 처리해주는 것이다.

스프레드 연산자나 slice(), map()과 같은 순수 함수를 써서 복사본을 만들지 않더라도 불변성이 유지된다.

때문에 push(), splice()를 쓰더라도 정상적으로 동작한다.

 

  const addTodo = ({ title }: { title: Todo["title"] }) => {
    if (!title) return;

    const newTodo = { id: crypto.randomUUID(), title };

    updateTodos((draft: Todo[]) => {
      draft.push(newTodo);
    });
    clearTitle();
  };
  const handleDeleteTodo = (todoId: Todo["id"]) => {
    const indexAt = todos.findIndex((todo) => todo.id === todoId);
    if (indexAt === -1) return window.alert("해당 Todo를 찾을 수 없습니다.");
    updateTodos((draft: Todo[]) => {
      draft.splice(indexAt, 1);
    });
  };

 

Immer 라이브러리가 주는 장점

상태 속에 변수의 개수가 적거나, 깊이가 얕다면 손수 불변성을 유지하는 작업을 해도 크게 어려움은 없을 것이다.

하지만 매번 불변성을 의식적으로 생각하고 코드를 작성하는 것은 쉽지 않다.

깊이가 두 단계 이상을 넘어가면 스프레드 연산자가 남발되어 코드를 다루고 읽기 어려워질 수 있다.

그런 면에서 useImmer는 useState에서의 불변성이라는 잠재적 위험을 없애준다.

불변성을 다루기 어려운 사람들에게 좋은 라이브러리 같다

 


소스 코드

import { useEffect } from "react";
import { useImmer } from "use-immer";
import "./App.css";

type Todo = {
  id: string;
  title: string;
};

function App() {
  const [todos, updateTodos] = useImmer<Todo[]>([
    { id: crypto.randomUUID(), title: "first todo" },
  ]);
  const [title, updateTitle] = useImmer("");

  const handleChangeTitle = (e: React.ChangeEvent<HTMLInputElement>) => {
    updateTitle(e.target.value);
  };

  const clearTitle = () => {
    updateTitle("");
  };

  const addTodo = ({ title }: { title: Todo["title"] }) => {
    if (!title) return;

    const newTodo = { id: crypto.randomUUID(), title };

    updateTodos((draft: Todo[]) => {
      draft.push(newTodo);
    });
    clearTitle();
  };

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    addTodo({ title });
  };

  const handleDeleteTodo = (todoId: Todo["id"]) => {
    const indexAt = todos.findIndex((todo) => todo.id === todoId);
    if (indexAt === -1) return window.alert("해당 Todo를 찾을 수 없습니다.");
    updateTodos((draft: Todo[]) => {
      draft.splice(indexAt, 1);
    });
  };

  useEffect(() => {
    console.log(todos);
  }, [todos]);

  return (
    <div className="App">
      <h1>use Immer 투투리스트</h1>
      <form onSubmit={handleSubmit}>
        <label htmlFor="title">제목:</label>
        <input
          type="text"
          id="title"
          value={title}
          onChange={handleChangeTitle}
        />
        <button>추가</button>
      </form>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            {todo.title}
            <button onClick={() => handleDeleteTodo(todo.id)}>삭제</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;