병원카드에 좋아요 기능을 추가했다. 실시간으로 하트를 누르면 업데이트 되도록하고 싶어서 일단 useState의 초기값을 likes 목록에 존재하는지 여부로 정한 후, 클릭하면 반대로 바뀌도록 했다. TanStack Query로 낙관적 업데이트할 수 있기 때문에 적용해보기로 했다.
낙관적 업에이트는 실제 데이터 업데이트가 느릴 수 있기 때문에, 그 간극을 줄이기 위해 성공할 것이라고 가정하고 변화를 주는 것이다. 이를 통해 사용자 경험을 향상시킬 수 있다.
1. `useMutation()`
`useMutation`는 하나의 parameter에 여러 옵션을 넣을 수 있는데, 이를 활용하면 낙관적 업데이트를 적용할 수 있다. 그 중 오늘 사용할 옵션들은 `mutationFn`, `onMutate`, `onError`, `onSettled`이다. 이중에서 당연히 많은 사람들이 `mutationFn`은 기본으로 넣고 있었을 것이다. 데이터를 변경하고 싶을 때 주로 사용하는 함수이기 때문에 실제 데이터를 변형하는 함수를 넣을 것이다. 그래서 앞으로는 `onMutate`, `onError`, `onSettled`에 대해 살펴볼 것이다.
2. `onMutate`
먼저 `onMutate`는 `mutationFn`이 실행되기 전에 먼저 실행되는 함수이며, mutationFn에 넣는 값과 동일한 값을 자동으로 입력받는다. 우리는 이 옵션에 이전에 불러와진 데이터에 임시로 변형을 가하는 코드를 넣어줄 것이다.
먼저 페이지에서 refetch 중이면 충돌이 발생할 수 있기 때문에 해당 refetch를 취소해야 한다. 아래 코드로 해당 쿼리키에 대한 refetch를 취소할 수 있다.
await queryClient.cancelQueries({ queryKey: ["user", "like", userId] });
이제 임시로 변형된 다음 좋아요 데이터를 넣어줘야 한다. 이때 `.setQueryData()` 메서드를 사용하는데, 쿼리키와 updater를 인자로 받는다.
queryClient.setQueryData<Like[]>(["user", "like", userId ?? ""], (prev) => [...(prev ?? []), newLike]);
updater에는 업데이트될 데이터를 직접 넣어줘도 되고, 함수를 넣어줘도 된다. 이때 함수를 넣게 되면 직전 데이터를 인자로 받게 된다. 따라서 위와 같이 데이터를 업데이트할 수 있다. 만약 updater에 `undefined`를 넣게 되면 데이터가 업데이트 되지 않는다고 한다.
`onMutate`는 context를 반환하거나 아무것도 반환하지 않을 수 있는데, context에 이전 데이터를 넣어 추후에 `onError`나 `onSettled`에서 사용할 수 있게 할 것이다. 아래 코드를 통해 지금까지 저장된 데이터를 불러올 수 있다. 만약 TypeScript를 사용중이라면 해당 데이터에 대한 타입을 명시해주어야 한다.
const prevLikes = queryClient.getQueryData<Like[]>(["user", "like", userId ?? ""]);
여기서 조금 신경써야할 것은 '지금까지 저장된 데이터'를 불러온다는 것이다. 만약 `.setQueryData()` 이후에 사용한다면 해당 데이터까지 저장된다. 따라서 변형이 일어나기 전 데이터를 저장할 목적이므로 지금은 `.setQueryData()` 이전에 작성되어야 한다.
마지막으로 `onMutate`의 반환값이 있다면 객체여야 한다.
// 낙관적 업데이트
onMutate: async ({ hospitalInfo }: { hospitalInfo: HopsitalItem }) => {
await queryClient.cancelQueries({ queryKey: ["user", "like", userId] });
const prevLikes = queryClient.getQueryData<Like[]>(["user", "like", userId]);
// 낙관적 업데이트를 위한 임시 데이터
const newLike: Like = convertToLike(hospitalInfo);
queryClient.setQueryData<Like[]>(["user", "like", userId], (prev) => [...(prev ?? []), newLike]);
// context에 이전 데이터 넣어둠
return { prevLikes };
},
3. `onError`
이 옵션은 `mutationFn`이 에러를 발생할 때 실행된다. 이 옵션에 사용되는 함수는 error와 variables를 인자로 받으며 원하면 `onMutate`에서 반환한 context를 사용할 수 있다. 실패한 경우 이전 데이터를 덮어씌우기로 했으므로 다음과 같이 작성했다.
// 에러가 발생하면 이전 데이터로 변경
onError: (_, __, context) => {
if (context?.prevLikes) {
queryClient.setQueryData<Like[]>(["user", "like", userId], context.prevLikes);
}
},
4. `onSettled`
이 옵션은 `onSuccess`와 달리 에러가 발생하거나 발생하지 않아도 모두 작동하는 옵션이다. (`onSuccess`는 에러가 발생하지 않은 경우에만 실행된다.)
쿼리 데이터가 변경됐으므로 무효화하거나 데이터를 다시 가져오는 로직이 여기에 들어간다.
앞서 말했듯이 임시 데이터를 넣었기 때문에 실제 데이터를 가져오도록 `.refetchQueries()` 메서드를 사용하거나, 렌더링이 발생할 때 refetch되도록 `.invalidateQueries()` 메서드를 사용해도 된다. 마이페이지에서 좋아요 데이터를 모아볼 때에는 정확한 데이터가 필요해서 refetch가 필요하고, 병원 목록을 볼때에는 데이터를 불필요하게 요청하지 않아도 될 것이라 생각했다. 하지만 좋아요 버튼을 누르면 해당 카드가 리렌더링 되므로 invalidateQueries만 사용해도 될 것이라 생각했다.
onSettled: () => {
queryClient.invalidateQueries({
queryKey: ["user", "like", userId]
});
}
지금부터는 낙관적 업데이트를 적용한 후에 발생한 오류에 대해 정리해보려고 한다.
5. 데이터가 자꾸 깜빡이는 문제
낙관적 업데이트를 적용해서 데이터가 깜빡임 없이 잘 적용될 줄 알았는데, 여전히 깜빡임이 발생했다. 이는 useQueries의 반환값 중 isFetching을 받아오지 않음으로써 해결했다. 데이터를 refetch하면 isFetching이 true가 되어 데이터 로딩 컴포넌트를 보여주는 것이었다.
6. 비로그인 상태에서의 오류
6-1. 쿼리 데이터가 `undefined`라 발생한 경고문
로그아웃하면 데이터가 바뀌도록 코드를 작성하는 것을 까먹고 급히 signout 로직에 "user"에 대한 모든 쿼리 데이터를 무효화하는 코드를 넣었는데 아래 경고문을 받았다.
로그인 정보를 반환하는 함수에서 로그인 정보를 반환하거나 `undefined`를 반환하도록 했는데, `undefined`를 반환하도록 해서 발생한 경고였다. 이는 해당 함수의 반환값을 `null`로 바꾸면서 해결했다.
6-2. 로그아웃 시 페이지 이동 안되는 문제
로그아웃하면 홈으로 이동하도록 설정했는데, invalidateQueries 한 줄 추가했다고 홈으로 이동하지 않고, 로그인 페이지로 이동하는 오류가 발생했다.
현재 마이페이지에서 로그아웃 버튼을 눌러야만 로그아웃할 수 있다. 현재 코드는 마이페이지에서 "user"를 쿼리키로 쓰는 커스텀 훅을 통해 로그인 정보를 불러오고 있었는데, 내가 작성한 `.invalidateQueries()`가 "user"를 무효화하면서 발생한 오류였다.
해당 문제는 좋아요가 필요한 상위 page 컴포넌트에서 서버에서 직접 로그인 정보를 가져와 전달하는 방식으로 변경함으로써 해결했다.
'Frontend > Projects' 카테고리의 다른 글
[Next.js] 카카오맵 api 사용하기 - 1 (0) | 2025.01.07 |
---|---|
[Next.js] useRouter 페이지 로딩 속도 개선 (0) | 2024.11.19 |
[React] mutationFn에 여러 parameter를 갖는 함수 사용하기 (0) | 2024.10.15 |
[Next.js] supabase server side auth 설정하기 (5) | 2024.10.14 |
[Next.js] 리그오브레전드 챔피언, 아이템 정보 제공하는 사이트 만들기 (0) | 2024.10.07 |