오늘 새로 추가할 기능은 즐겨찾기 기능이다. 전부터 즐겨찾기하는 부분은 만들어뒀지만 실제로 api에서 지원하지 않아서 흉내만 낸 부분이였다. 그래서 즐겨찾기 버튼을 누르면 별 색깔만 바뀌고 아무것도 바뀌지 않았다.

기능 자체는 복잡하지 않다. 즐겨찾기 여부는 boolean값으로 구분하고 있고 즐겨찾기한 폴더들은 즐겨찾기 폴더에 추가되는 방식으로 구현되어 있다. 지금까지 쿼리를 이용해서 구현한 것들을 종합해서 구현했다.
const { mutate } = useMutation({
mutationFn: async (data: { linkId: string; favorite: boolean }) => {
if (data.favorite) {
await putLinkLike(data.linkId, false);
} else {
await putLinkLike(data.linkId, true);
}
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['links', folderId],
});
queryClient.invalidateQueries({
queryKey: ['links'],
});
queryClient.invalidateQueries({
queryKey: ['links', favoriteFolder],
});
},
});
카드에 추가한 별모양 아이콘을 누르면 현재 각 링크의 즐겨찾기 여부에 따라서 다른 요청을 보내게 된다.
지금 코드를 보니 그냥 data.favorite의 반대인 !data.favorite을 해주면 됬지싶다,,,
그리고 요청에 성공하면 세가지 쿼리를 다시 받아오도록 한다. 링크 전체 목록, 즐겨찾기 폴더 목록, 링크가 포함된 폴더 세가지를 모두 새로 받아와야한다. 그래야 모든 곳에서 링크의 즐겨찾기 여부를 확인할 수 있기 때문이다.
여기에서 문제점이 생긴다. 지금 즐겨찾기를 하고 refetch하는 시간동안은 별 아이콘에 아무런 변화가 생기지 않는다. 그리고 서버에서 새로운 데이터가 도착해야 별아이콘에 변화가 생긴다. 물론 내가 지금까지 만든 로직에서는 그정도 딜레이는 로딩 컴포넌트를 사용해서 사용자가 불편함을 겪지 않도록 했다.
{(linkLoading || folderLoading) && (
<ModalPortal>
<Loading />
</ModalPortal>
)}
하지만 즐겨찾기나 좋아요, 싫어요 기능은 자주 일어나는 이벤트이고 자잘한 이벤트에도 로딩이 계속 렌더링되면 사용성이 떨어진다. 그래서 이런 기능에서는 바로 화면에 적용되도록 optimistic update를 해주는 것이다.
optimistic update는 낙관적 업데이트로 우선 변경사항을 사용자에게 바로 보여주는 것이다. 만약 오류가 발생하더라도 큰 문제가 안되는 부분에만 사용하는 것이다. 그래서 좋아요나 즐겨찾기의 경우 요청이 제대로 적용되지 않아도 사용자 본인의 실수라고 착각해 다시 시도하는 경우가 있다.
이제 내 코드에 적용시켜 보자.
먼저 어떤 순서로 이뤄지는지 이해해야한다. 우선 캐시에 저장된 쿼리 데이터를 가져온다. 그리고 변경할 데이터를 가져온 쿼리 데이터에 삽입해주고 변경된 데이터를 다시 쿼리에 세팅해줘서 바로 적용되도록 하는 것이다.
업데이트하는 시점은 mutateFn이 실행되기 전이다.
onMutate: async (data: { linkId: string; favorite: boolean }) => {
if (folderId) {
const result = queryClient.getQueryData<any>(["links", folderId]);
});
} else {
const result = queryClient.getQueryData<any>(["links"]);
}
},
우선 각 폴더 페이지에 있는 경우와 전체 폴더 페이지에 있는 경우를 나눠봤다. 그래서 폴더id가 있는 경우에는 해당 폴더 id의 쿼리 데이터를 가져오고 없는 경우에는 전체 링크 쿼리 데이터를 가져온다. 이 부분에서 쿼리 데이터의 타입을 지정해서 받아오고 싶었는데 쿼리 데이터의 경우 타입이 어떻게 될지 모르기도 해서 일단 any타입으로 받아온다.
그리고 데이터를 직접 변경시켜주는 로직을 추가해준다.
const refetchLink = { ...result.data[index], favorite: !data.favorite };
let refetchLinkArr = [...result.data];
refetchLinkArr[index] = refetchLink;
우선 refetchLink는 쿼리 데이터를 받아서 즐겨찾기 부분을 수정한 데이터이다. 그리고 쿼리데이터를 그대로 받은 refetchLinkArr를 만들고 수정이 이뤄진 index값을 이용해 변경된 데이터를 적용시켜준다.
1번 데이터 즐겨찾기 -> 쿼리 데이터 가져옴 -> 쿼리 데이터에서 1번 index의 링크 객체의 favorite값을 변경해줌 -> 쿼리와 같은 데이터 한개 생성 -> 변경된 링크 객체를 링크 배열에 적용시켜줌
이제 쿼리에 주입시켜주면 된다.
queryClient.setQueryData(["links", folderId], (prev: any) => {
return {
...prev,
data: refetchLinkArr,
};
});
위에서 만들어진 수정된 데이터를 쿼리의 링크 배열에 넣어주면 된다. 일련의 과정을 거친뒤 서버에 즐겨찾기 요청을 보내게되고 서버에서 응답받기 전에 사용자의 화면에서는 바로 적용되는 것이다.
폴더id가 없어도 같은 구조이다.
...
} else {
const result = queryClient.getQueryData<any>(["links"]);
const refetchLink = { ...result.data[index], favorite: !data.favorite };
let refetchLinkArr = [...result.data];
refetchLinkArr[index] = refetchLink;
queryClient.setQueryData(["links"], (prev: any) => {
return {
...prev,
data: refetchLinkArr,
};
});
}
이제 버튼을 누를때마다 바로 화면에 적용되는 것을 볼수 있다.
하나씩 기능이 완성되어 가면서 서비스처럼 되는 기분이다. 기능 구현에 성공해서 기쁜것보다 내가 새로운 것을 이해하고 적용했다는 것에 더 뿌듯함을 느낀다. 하지만 이제 다시 새로운 것을 배울때가 왔다. 현재 nextJs는 버전 14로 업데이트하면서 app router를 사용하고 있다. 지금까지 nextJs를 사용하면서 page router만 사용했는데 약간 걱정이다.
물론 두개의 차이가 크지는 않다. 하지만 SSR을 활용하고 앞으로 프론트엔드 개발자로서의 방향에서는 app router를 배워야만한다. 그래서 일단 이후에는 app router 마이그레이션을 진행해보도록 하겠다.