React 공식문서 톺아보기: useOptimistic

GwangSoo·2025년 4월 7일
0

개인공부

목록 보기
22/34
post-thumbnail

학교 현장실습(IPP)을 시작한 지 1달이 조금 넘었다. 프로젝트를 진행하다 보니 React에 대한 이해도가 부족한 것 같다는 생각이 문득 들기 시작했고, 이에 공식 문서를 톺아보는 시리즈를 시작해보려고 한다.

그 첫 번째 훅으로 useOptimistic에 대해 알아보고자 한다.

useOptimistic을 첫 번째로 선택한 이유는, 프로젝트에서 좋아요/좋아요 취소 API를 연동할 때 요청이 성공하면 alert 창이 뜨도록 해두었는데, 팀 내에서는 알림 창이 번거롭게 느껴졌고, 즉각적인 UI 피드백이 있으면 좋겠다는 의견이 있었다. 이에 따라 useOptimistic 훅을 첫 번째 주제로 정하게 되었다.

소개

공식 문서에서 정의한 내용을 살펴보자.

useOptimistic은 React Hook으로, 비동기 작업이 진행 중일 때 다른 상태를 보여줄 수 있게 해줍니다. 인자로 주어진 일부 상태를 받아, 네트워크 요청과 같은 비동기 작업 기간 동안 달라질 수 있는 그 상태의 복사본을 반환합니다. 현재 상태와 작업의 입력을 취하는 함수를 제공하고, 작업이 대기 중일 때 사용할 낙관적인 상태를 반환합니다.

여기서 “낙관적”이라는 말은 어떤 의미일까?

사전적 정의는 다음과 같다.

  • 인생이나 사물을 밝고 희망적인 것으로 보는 것
  • 앞으로의 일 따위가 잘될 것이라고 여기는 것

출처: 네이버 국어사전

이 뜻을 위 설명에 적용해보면, 비동기 작업이 성공할 것(잘될 것)으로 여기고 UI를 미리 업데이트시키는 것이다. 이는 사용자에게 즉각적인 피드백을 제공하여 앱이 더 빠르고 부드럽게 느껴지도록 만든다.

예를 들어, 인스타그램의 좋아요 기능을 생각해보자. 인터넷에 연결되지 않은 상태에서 좋아요 버튼을 눌러도 UI적으로는 좋아요가 반영된다. 물론 요청이 실패했다면 잠시 후에 좋아요가 취소된다.

useOptimistic은 바로 이러한 UX를 가능하게 해주는 훅이다.

정의

import { useOptimistic } from 'react';

function AppContainer() {
  const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);
}

매개변수

  • state: 초기에 할당되는 값.
  • updateFn(currentState, optimisticValue)
    • currentState: 현재 상태(state)
    • optimisticValue: addOptimistic을 통해 들어온 값 이 함수는 현재 상태와 optimisticValue를 합쳐서 반환한다.

반환 값

  • optimisticState
    • 비동기 작업이 없을 때는 초기 상태를 그대로 유지
    • 작업 중에는 updateFn의 반환 값을 보여줌
  • addOptimistic
    • 비동기 작업이 시작될 때 호출
    • 인자로 optimisticValue를 받음

예시

JSONPlaceholder 사이트의 /todos mock 데이터를 이용한 예시

interface TodoProps {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

const [todos, setTodos] = useState<TodoProps[]>([]);

const [optimisticTodos, updateOptimisticTodos] = useOptimistic<
  TodoProps[],
  number
>(todos, (current, todoId) => {
  return current.map((todo) =>
    todo.id === todoId ? { ...todo, completed: !todo.completed } : todo
  );
});

useEffect(() => {
  const fetchTodos = async () => {
    const res = await fetch("https://jsonplaceholder.typicode.com/todos");
    setTodos(await res.json());
  };
  fetchTodos();
}, []);
  • 데이터를 받아와 todos에 저장
  • 해당 데이터를 useOptimistic의 첫 번째 인자로 사용
  • useOptimistic은 제네릭으로 두 개의 타입을 받는다
    1. 상태(state)의 타입 – 여기서는 TodoProps[]
    2. optimisticValue의 타입 – 여기서는 todoId
const onUpdateComplete = async (todoId: number) => {
  startTransition(async () => {
    updateOptimisticTodos(todoId);
    try {
      await new Promise((resolve, reject) =>
        setTimeout(() => {
          const num = (Math.random() * 100).toFixed(0);
          const isEven = +num % 2 === 0;
          if (isEven) {
            resolve(isEven);
          } else {
            reject(new Error("Error"));
          }
        }, 1000)
      );

      const newTodos = todos.map((todo) =>
        todo.id === todoId ? { ...todo, completed: !todo.completed } : todo
      );
      setTodos(newTodos);
    } catch (err) {
      console.error(err);
    }
  });
};
<main>
  <ul>
    {optimisticTodos.map((todo) => (
      <li key={todo.id} className="todo-wrapper">
        <input
          type="checkbox"
          checked={todo.completed}
          onChange={() => onUpdateComplete(todo.id)}
        />
        <p>{todo.title}</p>
      </li>
    ))}
  </ul>
</main>
  • optimisticTodos를 기반으로 목록을 렌더링
  • 체크박스를 변경할 때 onUpdateComplete 호출

⚠️ 주의: formAction이 아닌 경우 반드시 startTransition 안에 작성해야 한다.
그렇지 않으면 아래와 같은 에러가 발생한다
에러 사진

성공 여부는 랜덤하게 설정하였다. (짝수면 resolve, 홀수면 reject)

성공했을 때

UI가 유지되며 상태 반영이 정상적으로 완료된다.

요청 성공 시

실패했을 때

요청 실패 시

UI는 optimistic 상태를 반영하지만, 요청 실패로 인해 원래대로 돌아간다.

후기

데이터를 받아오거나 로그인을 할 때 로딩 UI를 통해 UX를 개선하곤 했었다.

이제는 useOptimistic 훅을 통해 좋아요, 채팅처럼 실시간성이 중요한 UI에서도 사용자 경험을 더 높일 수 있을 것 같다.

참고

0개의 댓글