useOptimistic 훅으로 낙관적 업데이트 해보기

React에서 낙관적 업데이트를 useOptimistic 훅을 통해 만들어보자.
2024.03.17

클라이언트에서 사용자가 항목을 추가, 변경, 삭제를 api를 통하는 경우 추가적인 처리를 하지 않으면 API의 응답을 기다리는 동안 사용자의 행위가 반영되지 않는다.

이러한 이유로 api의 응답이 오기전에 미리 반영을 하여 사용자 경험을 개선할 수 있는데 이러한 방법을 낙관적 업데이트라고 한다.

이러한 낙관적 업데이트를 useOptimistic 훅을 통해 만들어보자.

useOptimistic 훅은 현재 카나리 버전에서 사용가능하다.

낙관적 업데이트가 없는 경우

일단 낙관적 업데이트가 없는 경우의 동작을 확인해보자.

위와 같이 사용자가 항목을 추가 했지만 API로 부터의 응답을 기다리느라 화면상에는 약간의 시간이 지나고 나서야 반영이 되는 것을 알 수 있다.

짧은 시간이지만 사용자는 자신이 한 행동이 바로 적용되지 않는 것에 대해 불안한 사람도 있을 것이다.

로딩을 보여주는 것도 방법이지만 1초 이내의 짧은 시간안에 응답이 올 것으로 예상되는 경우 잠깐의 순간 로딩이 보이는 것이 오히려 사용자의 경험에 피로를 줄 수도 있다.

이런 점들을 개선하기 위해서 낙관적 업데이트를 사용해보자.

useOptimistic 훅을 이용한 낙관적 업데이트

useOptimistic 훅의 타입은 아래와 같다.

// @types/react/canary.d.ts

function useOptimistic<State>(
    passthrough: State,
): [State, (action: State | ((pendingState: State) => State)) => void];
function useOptimistic<State, Action>(
    passthrough: State,
    reducer: (state: State, action: Action) => State,
): [State, (action: Action) => void];

state만 받는 경우와 state를 조작하는 action도 받는 경우의 2가지를 오버로딩하고 있는데 우리는 후자를 사용해보겠다.

위의 타입에 맞게 어떠한 항목을 추가하는 useOptimistic 훅을 추가해보자.

const [optimisticActivities, addOptimisticActivites] = useOptimistic(
  activities,
  (currentActivities, newActivity) => {
    return [newActivity, ...currentActivities] as Activity[];
  },
);

첫 번째 파라미터로 기존에 가지고 있던 데이터를 넘겨주었고 두 번째 파라미터의 현재 데이터와 새로운 데이터를 받아 둘을 조합해 리턴하는 함수를 추가해주었다.

두 번째 파라미터에서 리턴하는 값이 optimisticActivities 가 되고 두 번째 파라미터의 함수 자체가 addOptimisticActivities 가 된다. (useState 느낌으루다가)

그럼 실제로 넘겨주는 경우는 아래와 같이 되게된다.

...
<AddButton addOptimisticActivities={addOptimisticActivites} />
<ActivityList activities={optimisticActivities} />
...

항목을 더해주는 컴포넌트에는 addOptimisticActivites 을 전달해 API의 응답을 받기전에 데이터를 변경시키고, 데이터를 표현하는 컴포넌트에는 optimisticActivities 를 전달해 바로 업데이트 되는 리스트가 유저에게 보이게 해보자.

그러면 addOptimisticActivites을 어떻게 추가하면 될지 AddButton 컴포넌트를 확인해보자.

...

<form
  action={async (formData) => {
    const activity = Object.fromEntries(
      formData.entries(),
    ) as Activity;
    addOptimisticActivities(activity);      // <- API의 응답전에 먼저 데이터를 변경시켜준다.
    await createActivityAction(activity);
    formState.reset({ name: "", description: "" });
  }}
>

폼 액션을 통해 항목을 추가하고 있었는데 위의 주석처럼 API 보다 먼저 화면상의 데이터를 변경시키는 과정을 추가해주게 된다.

그러면 아래처럼 기존에 API의 응답을 기다린 후에 화면을 업데이트하는 것이 아니라 바로 사용자가 추가한 항목이 반영되는 것이 보인다! (리스트를 추가하는 경우 각 아이템 마다 유니크한 key 값이 필요하기 때문에 유니크한 키값을 추가하는 것도 필요하다.)

마무리

useOptimistic 은 아직 카나리 버전의 API이지만 이후에 스테이블로 넘어가면 많이 사용될 것 같다. 특히 폼 액션과 함께 사용하는 경우 유용했다.

물론 위의 방법으로 낙관적 업데이트는 완전하지 않다. 실제 경우에는 API에 에러가 발생하거나 API의 응답이 오래걸리는데 유저가 여러번 요청을 하는 경우 서버와의 데이터를 싱크하는 등 고려해야할 부분들이 더 있다. 하지만 그러함에도 useOptimistic이 우리가 고려해야할 부분들만 남겨두고 이외에 자질구레한 부분들을 추상화해주고 있어서 편리하게 사용할 수 있다.

모든 경우에 낙관적 업데이트가 필요하지는 않지만 상황에 따라 낙관적 업데이트를 통해 사용자 경험을 개선시켜보자.