모든 API를 불러오는 axios가 아닌, fetch를 통해 가져왔다. axios보다는 별도의 라이브러리 설치를 하지 않고 사용할 수 있는 fetch를 사용하게 되었다. 구현하기에 앞서 초기 세팅을 해주면 좋을 것 같아, 사용되는 파트 별로 분리하여 세팅을 해주었다.
내가 맡은 초기세팅은 follow.ts와 product.ts였다.


프로필 카드에 대한 유저 데이터 정보는 user.ts에서 받아올 수 있었다. 유저 ID에 따라 다른 데이터 정보를 받아오게 되었다.

데이터를 받아오면 위와 같은 응답값을 가지게 되는데, 이때 프로필 카드에서 필요로 하는 정보는 image, nickname, description, isFollowing, followersCount, followeesCount 정도이다. 구조 분해 할당으로 모든 response 데이터에서 필요로 한 것만 뽑아 사용했다.
비동기 데이터를 좀 더 효율적으로 관리하기 위해 리액트의 Suspense 컴포넌트를 사용했다. 심지어 useSuspenseQuery라는 훅도 존재하기 때문에, 로딩 상태나 에러 처리를 간편하게 할 수 있었다.

우린 이 이점을 살려 로딩 상태를 스켈레톤으로 사용할 것이기 때문에 fallback 함수에 스켈레톤 컴포넌트를 사용했다.
<Suspense fallback={<SkeletonProfileCard />}>
<HydrationBoundary state={dehydrate(queryClient)}>
<ProfileCard loginedId={loginedId} accessToken={accessToken} />
</HydrationBoundary>
</Suspense>
<div className="flex flex-col grow lg:gap-[80px] gap-[60px] max-w-[940px] md:w-full mobile:w-full">
<Suspense fallback={<SkeletonActivitySection />}>
<HydrationBoundary state={dehydrate(queryClient)}>
<ActivitySection />
</HydrationBoundary>
</Suspense>
<Suspense fallback={<SkeletonProductSection />}>
<HydrationBoundary state={dehydrate(queryClient)}>
<ProductSection loginedId={loginedId} />
</HydrationBoundary>
</Suspense>
로딩 상태를 거쳐 로딩 완료된 프로필 카드의 모습이 보이게 되었다. 프로필 이미지, 닉네임, 설명, 팔로워, 팔로잉 수까지 모두 잘 나오는 것을 볼 수 있다. 그럼 다른 유저의 프로필 페이지로 이동했을 때에도 해당 유저의 프로필이 잘 나와야 한다.
https://example/profile?userId=17
위 URL 처럼 정보가 저장되어 있는 것을 볼 수 있다. 쿼리 스트링이라고도 불리는데, URL에 저장하고 searchParams를 통해 이를 쉽게 가져올 수가 있다. 쿼리키로 이 유저 아이디를 사용하면 userId가 바뀔 때마다 기존 캐시를 지우고 새로운 데이터 정보로 다시 불러오게 되는 과정을 거쳐 유용하게 사용할 수 있었다.
export const profileOptions = (userId: number, accessToken: string) => {
return queryOptions({
queryKey: ProfileKeys.user(Number(userId)),
queryFn: async () => {
const response = await getUserInfo(Number(userId), accessToken);
if (!response.ok) {
notFound();
}
return response.json();
},
});
};
const userId = searchParams.userId ?? loginedId;
URL에 userId이 없으면 본인 프로필이 뜨도록 null 병합 연산자를 활용하여 loginedId를 가질 수 있도록 했다. loginedId는 쿠키에 저장된 로그인 한 유저 아이디이다.
본인 프로필에는 프로필 편집 버튼이 보였지만, 다른 유저의 프로필로 들어왔을 때는 팔로우 버튼이 보이는 것을 볼 수 있다. 이 역시 쿼리스트링을 활용하여 조건부 연산자를 통해 구현해 나갔다.
낙관적 업데이트라 불리는 Optimistic Update는 사용자 UX 향상을 위해 유용하다. 예측되는 결과를 미리 반영하여 보여주게 되는데, 보통 좋아요 기능같은 곳에서 자주 사용된다.
나는 팔로우 기능에다가 Optimistic Update를 적용해봤다. 사용자 입장에서 팔로우 버튼을 눌렀는데 늦게 반영이 되면 답답한 느낌을 받을 수 있고 요청되는 사이에 다시 API 호출을 할 수가 있다. 1~2초라도 크다고 생각한다.
onMutate: async ({ isFollowing }: { isFollowing: boolean }) => {
await queryClient.cancelQueries({
queryKey: ProfileKeys.user(currentProfileId),
});
const previousFollow = queryClient.getQueryData<UserInfoData>(
ProfileKeys.user(currentProfileId),
);
queryClient.setQueryData(
ProfileKeys.user(currentProfileId),
(prev: UserInfoData) => ({
...prev,
isFollowing: !isFollowing,
followersCount: !isFollowing && prev.followersCount + 1,
}),
);
return { previousFollow };
},
cancelQueries: 이전 쿼리 데이터가 낙관적 업데이트를 덮어쓰지 않도록 함
getQueryData: 기존 선택지 데이터의 스냅샷
setQueryData: 새로운 선택지 데이터로 낙관적 업데이트 실행
위에서는 로그인을 했다는 가정하에 동작하도록 보였다. 현재는 로그아웃이 된 상태에서 다른 유저의 프로필로 이동했을 때, 팔로우 버튼을 클릭 시 로그인 알림 모달이 나오게 된다.
accessToken이 존재하지 않을 경우 router.push를 사용해 로그인 알림 모달을 띄어주었다.
사실 아직 완벽하게 해결하지 못한 있다. pararell route를 사용하여 모달을 띄우게 되는데, '로그인 하러 가기'버튼을 클릭하면 모달이 닫히지 않는 상태에서 URL만 변경되는 것이다. 모달뒤에 비쳐지는 화면은 로그인 페이지로 이동한 게 보이는데, 모달이 계속 있으니..
해결은 router.push로 로그인 페이지로 이동한 다음, router.refresh를 연이어 사용하게 되면 모달이 사라지면서 로그인 페이지만 보이긴했다. 다만, refresh는 새로고침을 하다보니 효율성에서 떨어지긴 한다. 분명 방법이 있을 것 같은데 더 공부를 해봐야겠다.
잘 보고 갑니다 :)