Skip to content

feat: cheerVS 언마운트 시 API 호출 보장#564

Open
seongminn wants to merge 2 commits into
wip-v2from
cheer-vs-flush
Open

feat: cheerVS 언마운트 시 API 호출 보장#564
seongminn wants to merge 2 commits into
wip-v2from
cheer-vs-flush

Conversation

@seongminn

Copy link
Copy Markdown
Member

✅ 작업 내용

  • 응원탭 배치 처리 시, 컴포넌트가 언마운트 되면 API 호출 누락 문제 발생
  • pagehide, visibilitychange 이벤트로 탭 닫기, 새로고침, 페이지 이탈, 모바일 백그라운드, 탭 전환 시 API 호출
  • useEffect의 클린업 콜백으로 debounce의 flush 메서드를 바인딩

♾️ 기타

  • 추가로 필요한 작업 내용

@seongminn seongminn requested a review from sungwonnoh June 7, 2026 11:50
@seongminn seongminn self-assigned this Jun 7, 2026
@vercel

vercel Bot commented Jun 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spectator Ready Ready Preview, Comment Jun 7, 2026 11:51am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
manager Skipped Skipped Jun 7, 2026 11:51am

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +148 to +160
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]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

이 코드에는 세 가지 개선 및 수정 사항이 있습니다:

  1. 클라이언트 환경 변수 접근 오류: Next.js의 클라이언트 컴포넌트('use client')에서는 NEXT_PUBLIC_ 접두사가 없는 환경 변수(process.env.API_BASE_URL)에 접근할 수 없습니다. 브라우저 환경에서는 undefined로 평가되어 항상 '/api' 폴백만 사용하게 됩니다. 이를 process.env.NEXT_PUBLIC_API_BASE_URL로 변경해야 합니다.
  2. UI 상태 동기화 누락: flushBeacon이 실행되어 pendingCountRef.current0으로 초기화될 때, pendingCount 상태는 업데이트되지 않습니다. 이로 인해 사용자가 페이지를 이탈했다가 다시 돌아와 응원을 추가할 때 UI 카운트가 비정상적으로 급증하거나 초기화되는 데스싱크(Desynchronization) 현상이 발생합니다. setPendingCount(0)을 호출하여 상태를 동기화해야 합니다.
  3. sendBeacon 대체: navigator.sendBeacon 대신 현대적인 fetchkeepalive: 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]);

Comment on lines +131 to +133
onSuccess: () => {
setInflightCount((prev) => prev - countToSubmit);
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

뮤테이션이 성공한 직후 onSuccess에서 inflightCount를 즉시 차감하고 있습니다. 하지만 이 시점에는 invalidateQueries를 통한 쿼리 리페치(네트워크 요청)가 비동기로 진행 중입니다.

따라서 서버로부터 새로운 cheerCount 데이터를 받기 전까지 아주 짧은 시간 동안 UI에 이전의 오래된 cheerCount가 노출되어 숫자가 일시적으로 줄어들었다가 다시 늘어나는 깜빡임(Flicker) 현상이 발생할 수 있습니다.

권장 대안:
가장 이상적인 해결책은 컴포넌트 내부에서 inflightCount 상태를 직접 관리하는 대신, React Query의 낙관적 업데이트(Optimistic Updates) 패턴을 사용하는 것입니다. useUpdateGameCheer 훅의 onMutate, onError, onSettled를 활용해 쿼리 캐시를 직접 업데이트하면 깜빡임 없는 완벽한 사용자 경험을 제공할 수 있으며, 컴포넌트 코드도 훨씬 단순해집니다.

if (count === 0) return;

pendingCountRef.current = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서는 pendingCount 상태를 업데이트하지 않는 것 같은데 이유가 있나용?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flushBeacon 함수는 페이지에서 이탈하여 컴포넌트가 언마운트 되거나, 백그라운드에서 처리되는 등의 상황에서 호출되는 거라 따로 처리할 필요가 없다고 판단했습니다!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

근데 확인해보니 document.visibilityState hidden과 unmount는 또 다르기 때문에 페이지가 실제로는 아직 살아있는 상태라고 하네요..! 그래서 상태를 우선 초기화 할 수 있도록 수정해두었습니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants