feat: cheerVS 언마운트 시 API 호출 보장#564
Conversation
There was a problem hiding this comment.
Code Review
This pull request replaces the custom useDebounce hook with es-toolkit's debounce and implements a mechanism to flush pending cheer counts using navigator.sendBeacon when the page is hidden or unloaded. It also introduces an inflightCount state to track pending requests. The review feedback highlights critical issues in the flushBeacon implementation, such as the incorrect use of a server-side environment variable in a client component, a UI state desynchronization bug where pendingCount is not reset, and a recommendation to use fetch with keepalive: true instead of navigator.sendBeacon. Additionally, the reviewer notes a potential UI flickering issue when decrementing inflightCount on success before the query refetch completes, suggesting React Query's optimistic updates as a cleaner alternative.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const flushBeacon = useCallback(() => { | ||
| const count = pendingCountRef.current; | ||
| if (count === 0) return; | ||
|
|
||
| pendingCountRef.current = 0; | ||
|
|
||
| const url = `${process.env.API_BASE_URL ?? '/api'}/games/${gameId}/cheer`; | ||
| const blob = new Blob([JSON.stringify({ cheerCount: count, gameTeamId })], { | ||
| type: 'application/json', | ||
| }); | ||
|
|
||
| navigator.sendBeacon(url, blob); | ||
| }, [gameId, gameTeamId]); |
There was a problem hiding this comment.
이 코드에는 세 가지 개선 및 수정 사항이 있습니다:
- 클라이언트 환경 변수 접근 오류: Next.js의 클라이언트 컴포넌트(
'use client')에서는NEXT_PUBLIC_접두사가 없는 환경 변수(process.env.API_BASE_URL)에 접근할 수 없습니다. 브라우저 환경에서는undefined로 평가되어 항상'/api'폴백만 사용하게 됩니다. 이를process.env.NEXT_PUBLIC_API_BASE_URL로 변경해야 합니다. - UI 상태 동기화 누락:
flushBeacon이 실행되어pendingCountRef.current가0으로 초기화될 때,pendingCount상태는 업데이트되지 않습니다. 이로 인해 사용자가 페이지를 이탈했다가 다시 돌아와 응원을 추가할 때 UI 카운트가 비정상적으로 급증하거나 초기화되는 데스싱크(Desynchronization) 현상이 발생합니다.setPendingCount(0)을 호출하여 상태를 동기화해야 합니다. sendBeacon대체:navigator.sendBeacon대신 현대적인fetch와keepalive: true옵션을 사용하는 것이 더 권장됩니다.Blob객체를 생성하는 번거로운 우회 방법을 피할 수 있고, 향후 인증 토큰 등 커스텀 헤더를 추가해야 할 때 유연하게 대처할 수 있습니다.
const flushBeacon = useCallback(() => {
const count = pendingCountRef.current;
if (count === 0) return;
pendingCountRef.current = 0;
setPendingCount(0);
const url = `${process.env.NEXT_PUBLIC_API_BASE_URL ?? '/api'}/games/${gameId}/cheer`;
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ cheerCount: count, gameTeamId }),
keepalive: true,
});
}, [gameId, gameTeamId, setPendingCount]);
| onSuccess: () => { | ||
| setInflightCount((prev) => prev - countToSubmit); | ||
| }, |
There was a problem hiding this comment.
뮤테이션이 성공한 직후 onSuccess에서 inflightCount를 즉시 차감하고 있습니다. 하지만 이 시점에는 invalidateQueries를 통한 쿼리 리페치(네트워크 요청)가 비동기로 진행 중입니다.
따라서 서버로부터 새로운 cheerCount 데이터를 받기 전까지 아주 짧은 시간 동안 UI에 이전의 오래된 cheerCount가 노출되어 숫자가 일시적으로 줄어들었다가 다시 늘어나는 깜빡임(Flicker) 현상이 발생할 수 있습니다.
권장 대안:
가장 이상적인 해결책은 컴포넌트 내부에서 inflightCount 상태를 직접 관리하는 대신, React Query의 낙관적 업데이트(Optimistic Updates) 패턴을 사용하는 것입니다. useUpdateGameCheer 훅의 onMutate, onError, onSettled를 활용해 쿼리 캐시를 직접 업데이트하면 깜빡임 없는 완벽한 사용자 경험을 제공할 수 있으며, 컴포넌트 코드도 훨씬 단순해집니다.
| if (count === 0) return; | ||
|
|
||
| pendingCountRef.current = 0; | ||
|
|
There was a problem hiding this comment.
여기서는 pendingCount 상태를 업데이트하지 않는 것 같은데 이유가 있나용?
There was a problem hiding this comment.
flushBeacon 함수는 페이지에서 이탈하여 컴포넌트가 언마운트 되거나, 백그라운드에서 처리되는 등의 상황에서 호출되는 거라 따로 처리할 필요가 없다고 판단했습니다!
There was a problem hiding this comment.
근데 확인해보니 document.visibilityState hidden과 unmount는 또 다르기 때문에 페이지가 실제로는 아직 살아있는 상태라고 하네요..! 그래서 상태를 우선 초기화 할 수 있도록 수정해두었습니다!
✅ 작업 내용
pagehide,visibilitychange이벤트로 탭 닫기, 새로고침, 페이지 이탈, 모바일 백그라운드, 탭 전환 시 API 호출♾️ 기타