코드노트

SWR에서 Optimistic UI 사용방법 기록 본문

Code note/codenote

SWR에서 Optimistic UI 사용방법 기록

코드노트 2023. 8. 4. 03:06

현재 개인프로젝트를 리팩토링...? 아니 마이그레이션을 하고 있다.

처음에 React를 사용하여 작업을 했었지만 Next.js를 사용하여 프로젝트를 진행중이다.

이번 프로젝트를 진행하면서 Optimistic UI를 적용한 방법을 정리해보려고 한다.

 

Optimistic UI

- 사용자 요청을 서버에서 처리되기전 성공할 것을 가정하여 성공한 결과를 미리 보여주는 것이다.

사용자가 API요청을 보내게되면 서버에 요청을 하고 값을 받아서 변경된 값을 보여주어야하는데 그 값이 변하는게 오래걸린다면 사용자 경험에 있어서 좋지 않은 경험을 받게 된다. UI 시각적으로 우선 보여주고 그 뒤에 백그라운드에서 실제 작업이 실행된다.

 

 

그럼 아무곳에서나 사용해도 될까?

- 아니다. 사용자에게 즉각적으로 피드백이 중요한 상황에서만 사용해야한다. 모든 상황에서 적용하면 안된다. 그리고 UI로는 성공한것처럼 보여지더라도 실제 작업에서 실패할 수도 있기 때문에 처리 방안을 고려해서 사용해야한다.

 


내가 이번 작업에서 Optimistic UI를 사용하려는 이유

- 좋아요 부분에서 API를 요청을 하는데 서버에 요청값을 보내야하고, 그 값을 반영한 post의 데이터를 가져오는데 생각보다 딜레이가 발생했다. 인스타그램이나 페이스북 등등 좋아요부분에서 이렇게 딜레이가 걸린다면 사용자 입장에서는 얼마나 답답할까?

다른 해결방법이 있을 수 있지만 여기서 Optimistic UI를 사용했다. 

 

 

이번 프로젝트에서는 Next.js를 사용하는 만큼 데이터 fetching 관리 라이브러리는 SWR을 사용했다.

SWR에서는 Optimistic옵션을 제공하고 있다.

options을 보게 되면 여러가지가 있었다. 각 옵션값이 하는일을 정리해보자

- optimisticData : mutate함수를 사용할때 예측된 UI 업데이트를 위해 사용자가 기대하는 데이터를 제공하면 된다.

- revalidate : 데이터를 재 요청할지 여부를 결정한다. 데이터를 캐시에서 가져오거나 재요청한다.

- populateCache: 데이터를 가져온 후에 캐시에 데이터를 저장할지 여부를 결정한다.

- rollbackOnError: 요청이 오류로 실패한 경우 캐시로 롤백하여 이전 데이터 상태로 되돌릴지 여부를 결정한다.

 

내가 사용했을때 확인해야할 옵션은 이 4가지였다.

우선 Opimistic UI를 사용해야했고,

데이터를 재요청할 필요는 없다,

그리고 데이터를 캐시할 필요도 없다.

그리고 가장 중요한 만약 UI를 먼저 사용자에게 보여주고 실패한다면 rollback을 시켜야하는 옵션!

 


기존 코드

import { SimplePost } from "@/model/post";
import useSWR, { useSWRConfig } from "swr";

export default function usePosts() {
  const { data: posts, isLoading, error } = useSWR<SimplePost[]>("/api/posts");

  const { mutate } = useSWRConfig();
  const setLike = (post: SimplePost, username: string, like: boolean) => {
    fetch("api/likes", {
      method: "PUT",
      body: JSON.stringify({ id: post.id, like }),
    }).then(() => mutate("/api/posts"));
  };
  return { posts, isLoading, error, setLike };
}

- posts데이터를 hook으로 관리하는 코드였다. mutate를 useSWRConfig에서 가져와서 사용했다.

- setLike에서는 /api/likes 요청을 보내고 likes요청이 성공하면 새로운 posts데이터를 가져오도록 했다.

- 이렇게하면 좋아요 요청을 보내고 요청이 성공하면 그때 다시 posts의 새로운 값을 가져오고 그때서야 좋아요가 반영된 데이터를 사용자는 볼 수 있기 때문에 딜레이가 걸렸다.


수정된 코드

import { SimplePost } from "@/model/post";
import useSWR, { useSWRConfig } from "swr";

async function updateLike(id: string, like: boolean) {
  return fetch("api/likes", {
    method: "PUT",
    body: JSON.stringify({ id, like }),
  }).then((res) => res.json());
}

export default function usePosts() {
  const {
    data: posts,
    isLoading,
    error,
    mutate,
  } = useSWR<SimplePost[]>("/api/posts");

  const setLike = (post: SimplePost, username: string, like: boolean) => {
    const newPost = {
      ...post,
      likes: like
        ? [...post.likes, username]
        : post.likes.filter((item) => item !== username),
    };

    const newPosts = posts?.map((item) =>
      item.id === post.id ? newPost : item
    );

    return mutate(updateLike(post.id, like), {
      optimisticData: newPosts,
      revalidate: false,
    });
  };
  return { posts, isLoading, error, setLike };
}

- mutate를 따로 불러오지 않고 /api/posts에서 바로 불러와서 사용했다.

- updateLike 함수를 분리했다.

- newPost, newPosts에서는 현재 posts데이터에서 fillter를 사용하여 이미 가지고 있는 데이터를 변경했다.

- 변경된 데이터를 optimisticData에 적용시키고 mutate에 전달하여 덮어씌워주었다.


Optimistic 적용 후

- 이제 클릭할때마다 즉각적으로 반영되있는게 보인다 네트워크탭에서는 대기중으로 뜨고 우선적으로 ui에 적용이 된다!

 


좋아요 외에도 메시지를 보내거나, 코멘트, 장바구니 등등 사용자 에게 우선적으로 보여줘야하는 상황에서는 유용하게 사용할 수 있다.

그러나 함부로 사용하면 안된다. 그외에도 에러처리 및 성능에 있어서도 고려해야하는 상황들이 있기 때문에

사용시에는 주의하여 적용시키도록 해야할 것 같다.

사용자 경험에 있어서 항상 고민하는걸 잊지 말자!