원인 분석
해결 과정
vite-plugin-pwa
로 전환하고 이 때 vite.config.ts에서 registerType: 'autoUpdate'
옵션을 사용하여 새로운 서비스 워커가 감지되면 자동으로 활성화될 수 있도록 설정했습니다. 또한 개발과정에서는 굳이 서비스 워커가 필요하지 않다 생각되어 환경변수에 VITE_DEV_MODE를 추가하고 main.tsx 파일에서
if (import.meta.env.VITE_DEV_MODE === 'Production') {
registerSW();
코드를 통해 VERCEL의 Production 환경에서만 서비스 워커가 등록되도록 하였습니다.
원인 분석
해결 과정
ex)메인페이지에서 사용하는 useDeleteFollow함수와 프로필 페이지에서 사용하는 useGetFollowing 함수
function useDeleteFollow() {
const { user } = useAuth();
return useMutation({
mutationFn: ({ member_id }: RequestMemberDto) => deleteFollow({ member_id }),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['member-following', user.user_id],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEY.prompterList],
});
},
});
}
function useGetFollowing({ member_id }: RequestMemberDto) {
return useQuery({
queryKey: ['member-following', member_id],
queryFn: () => getFollowing({ member_id }),
staleTime: 0, // 언팔로우시 invalidateQueries
});
}
1. 메인페이지 팔로우/언팔로우 초기값 문제
```jsx
const { data: myFollowingData } = useGetFollowing({ member_id: user.user_id });
const { mutate: mutateFollow } = usePatchFollow({ member_id: user.user_id });
const { mutate: mutateUnFollow } = useDeleteFollow({ member_id: user.user_id });
const [isFollow, setIsFollow] = useState(() =>
Boolean(myFollowingData?.data.some((f) => f.following_id === prompter.user_id)),
);
const handleFollow = () => {
if (isFollow) {
mutateUnFollow({ member_id: prompter.user_id });
} else {
mutateFollow({ member_id: prompter.user_id });
}
setIsFollow((prev) => !prev);
};
```
- 문제 상황
isFollow의 상태를 useState를 이용해 관리하며 자신의 팔로잉 목록을 불러오는 useGetFollowing 훅을 이용하여 나의 팔로잉 목록 유저 아이디와 프롬프터의 아이디를 비교해 팔로우 / 언팔로우 상태를 관리하도록 구현하였다.
구현 결과
로그인 후 메인페이지에 초기 접속 시 팔로우 / 언팔로우 상태 반영이 제대로 되지 않는다.
원인 분석
useState
의 lazy Initializer는 첫 렌더링에만 평가된다. 따라서 비동기 fetch 결과인 myFollowingData
는 첫 렌더링 시점에는 undefined → Boolean(undefined)
가 되어 false
로 값이 고정된다. useEffect
를 사용하면 간단히 해결 가능한 문제겠지만 useEffect
를 사용하면 후처리 및 로직 분산으로 인해 mutation
으로 공용 훅을 만든 의미가 퇴색된다고 생각해 useEffect
는 배제하고 해결 방법을 생각했다.
const { data: myFollowingData } = useGetFollowing({ member_id: user.user_id });
const isFollow = Boolean(myFollowingData?.data.some((f) =>
f.following_id === prompter.user_id));
const { mutate: mutateFollow } = usePatchFollow({ member_id: prompter.user_id });
const { mutate: mutateUnFollow } = useDeleteFollow({ member_id: prompter.user_id });
const handleFollow = () => {
if (isFollow) {
mutateUnFollow({ member_id: prompter.user_id });
} else {
mutateFollow({ member_id: prompter.user_id });
}
};
문제 상황
만약 useState
를 쓰지 않고 myFollowingData
와 prompter.user_id
로부터 그때그때 계산되는 값으로 isFollow를 관리한다면 초기값 문제가 해결가능하다고 생각했다.
구현 결과
로그인 후 isFollow
의 초기값이 false로 고정되는 문제는 해결되었다. 하지만 팔로우 버튼을 사용자가 클릭한 후 화면이 리랜더링 되어야만 isFollow
상태가 반영된다
해결방법
검색창, 정렬, 필터링 유지 문제로 인해 강제로 화면을 리랜더링 시키는 것은 UX 측면에서 좋은 방법이 아니라고 생각했다. useEffect도, 리랜더링도 사용하지 않고 초기값 문제와 isFollow 상태 반영 문제를 해결하기 위해 Optimistic Update
방법을 떠올리게 되었다.
해결 방법
Optimistic Update를 활용해 UI 변경이 안되는 문제와 useState문제를 동시에 해결해보고자 하였다.
onMutate: async (variables) => {
await queryClient.cancelQueries({ queryKey });
const previous = queryClient.getQueryData<FollowingListResponse>(queryKey);
queryClient.setQueryData<FollowingListResponse>(queryKey, (prev) => {
const prevData = prev?.data ?? [];
return {
message: prev?.message ?? 'Optimistic update',
statusCode: prev?.statusCode ?? 200,
data: prevData.filter((f) => f.following_id !== variables.member_id),
};
});
return { previous };
},
작동 영상
문제점
Optimistic Update의 가장 큰 특징인 서버 반영 이전 캐시를 활용한 UI 업데이트라는 특성을 충분히 살리기 어려웠다. onMutate
, onError
, onSettled
를 이용해 Optimistic Update의 로직은 구현하였지만 팀원 간 협업 구조로 인해 onSettled
에서 쿼리를 다시 fetch
하면서 UI의 즉각 반응 효과가 제한적이기 때문이였다. 향후 데모데이 전까지 쿼리키 구조를 최적화하여 온전한 옵티미스틱 UI를 구현할 예정이다.