Optimistic UI를 적용한 힙한 '좋아요' 버튼

Jay·2023년 11월 16일
4

안녕하세요! 열일하는 프론트엔드 개발자 제이입니다 😀
이번 글에서는 부동산 플랫폼을 개발하며 '좋아요'버튼에 Optimistic UI를 적용한 경험을 공유하고자 합니다.

해당 페이지는 단지 정보를 담고 있는, 유저가 가장 많이 머무는 페이지 중 하나입니다. 많은 데이터를 서버에 요청하고 있으며, 웹뷰에서도 활용되는 부분이라 3G 환경에서의 성능 개선은 중요한 최적화 포인트입니다.

const handleToggleFavoriteButton = async () {
  // 좋아요 상태에 따라 API 호출한다.
  if(isFavorite) {
    await removeFavorite()
  }  	       
  else {
    await addFavorite()
  }
  mutate() // 서버 상태를 다시 가져온다.
}

이벤트 핸들러 함수를 다음과 같이 구현한다면, '좋아요'의 새로운 상태가 반영되기 위해 서버의 응답을 받아야 합니다. 네트워크 쓰로틀링을 3G로 설정하고 테스트한 결과, 클릭에 대한 레이턴시가 발생하고 UX가 저하되는 문제를 확인할 수 있었습니다.

5G환경에서는 PATCH 요청 후 GET 응답을 받기 까지 약 0.1초, 3G환경에서는 약 1.2초가 걸렸습니다.

네트워크 쓰로틀링 제한없음 5G

3G환경

레이턴시 문제를 확인했으니 개선을 해야겠죠? Optimistic UI를 적용하여 문제를 해결할 수 있었습니다. 팀에서는 데이터 페칭 라이브러리로 SWR을 사용하고 있어, mutate api의 optimisticData 옵션을 활용하여 remote mutation이 완료될 때까지 로컬 데이터를 수동으로 업데이트하는 방식을 선택했습니다.

아래는 swr 공식문서에서 optimisticData옵션을 활용하는 예시 코드입니다.

import useSWR, { useSWRConfig } from 'swr'
 
function Profile () {
  const { mutate } = useSWRConfig()
  const { data } = useSWR('/api/user', fetcher)
 
  return (
    <div>
      <h1>My name is {data.name}.</h1>
      <button onClick={async () => {
        const newName = data.name.toUpperCase()
        const user = { ...data, name: newName }
        const options = {
          optimisticData: user,
          rollbackOnError(error) {
            // timeout 오류인 경우 롤백하지 않기
            return error.name !== 'AbortError'
          },
        }
 
        // 로컬 데이터를 즉시 업데이트
        // 데이터 업데이트 요청 보내기
        // 로컬 데이터가 올바른지 확인하기 위해 재검증(refetch)를 트리거 합니다
        mutate('/api/user', updateFn(user), options);
      }}>Uppercase my name!</button>
    </div>
  )
}

캐시된 데이터를 수정한 후 revalidate를 통해 서버에서 데이터를 다시 가져와 로컬 데이터를 업데이트하는 방식입니다.

다만 공식 문서의 예시 코드를 그대로 활용할 수는 없었습니다. 현재 저희 API 로직은 공식문서의 예시 코드처럼, PATCH요청을 보냈을 때, 업데이트 된 데이터를 반환하고 있지 않기 때문입니다.

공식문서에 따르면 optimisticData 옵션을 사용하고 refetch를 트리거 하기 위해서는 아래와 같은 방식으로 사용해야 합니다.

updateFn 은 remote mutation을 처리하는 프로미스 또는 비동기 함수여야 하며, 업데이트 된 데이터를 반환해야 합니다.

업데이트 함수에 단지 데이터를 fetch하는 로직을 추가하여 문제를 해결했습니다.

function Danji () {
	const { mutate } = useSWRConfig()
	const { danji } = useSWR('api/danji', fetcher)
    
    const addFavoriteOptimistic = async () => {
      await addFavorite() // 서버에 좋아요 추가 요청
      const { data } = await getDanji() // 업데이트 된 데이터 다시 가져오기
      return data    
    }
    
    return (
    	<div>
        	{/* ... */}
      		{/* 좋아요 버튼 클릭 시, 로컬 데이터 업데이트와 서버 요청 */}
            <button onClick={async () => {
       			mutate('/api/danji', addFavoriteOptimistic(), {
            	optimisticData: danji => ({ ...danji, is_favorite: true }),
           		rollbackOnError: true
        	});
      	}}>좋아요 버튼</button>      
        </div>
    )
}

아래는 Optimistic UI를 적용한 후 3G환경에서 테스트한 결과입니다. 네트워크 환경에 구애받지 않고 유저의 액션에 즉각적으로 반응하는 UI를 확인할 수 있습니다.

참고로 트래픽이 많은 사이트라면 로컬 캐시 데이터를 수정한 후에 revalidate: false 옵션을 설정하여 refetch를 하지 않고 서버 부하를 줄일 수도 있습니다.

'좋아요'버튼은 특성상 통신 에러가 발생할 가능성이 희박하고, 결제 기능과 같이 결과에 민감하지 않기 때문에 Optimistic UI를 적용하기 적합한 기능이라고 생각합니다.

감사합니다.

[참고]
https://swr.vercel.app/ko/docs/mutation#optimistic-updates

0개의 댓글