'use client'; import { useEffect, useRef } from 'react'; import { useRouter } from 'next/navigation'; import { useFeedback } from '@/app/_hooks/use-feedback'; interface LogoutNoticeProps { /** 로그아웃 Server Action이 백엔드 호출 실패를 알리려고 실어 보낸 쿼리 파라미터가 있었는가. */ showError: boolean; } const LOGOUT_ERROR_MESSAGE = '로그아웃 처리 중 일부 오류가 있었지만 로그아웃되었습니다.'; /** * 화면에 그릴 UI가 없는 알림 트리거 컴포넌트 — `app/(protected)/_actions.ts`의 `logout()`이 * 백엔드 로그아웃 호출 실패를 알리려고 `/login`에 실어 보낸 쿼리 파라미터를 Page(Server * Component)가 읽어 `showError`로 넘겨주면, 마운트 시 1회 토스트로 보여준다. * * Server Action은 redirect() 이후 클라이언트에 값을 돌려줄 방법이 없다(redirect가 즉시 * 던져짐) — 그래서 이 화면에 도착해 마운트된 뒤(=redirect가 끝난 뒤)에만 알릴 수 있다. 이 * 컴포넌트는 오직 이 시점에만 반응하므로 redirect와 토스트 표시가 시점상 겹치지 않는다. * * 표시 후에는 쿼리 파라미터를 제거한다(router.replace) — 새로고침·북마크 재방문 시 지난 실패가 * 다시 뜨는 것을 막는다. * * `hasNotifiedRef`로 실행을 1회로 못박는다 — App Router는 기본 Strict Mode(next.config.ts에 * 별도 설정이 없으면 true, Next.js 13.5.1부터 기본값)라 개발 모드에서 effect가 mount→cleanup→ * remount로 2회 호출된다. 이 effect엔 cleanup이 없어 가드가 없으면 토스트가 두 번 뜬다 — * router.replace가 아직 반영되기 전(같은 커밋 내)에 두 번째 호출이 일어나 showError prop이 * 그대로 true이기 때문이다. 프로덕션은 원래 1회지만, 사용자에게 실제로 보이는 부수효과(알림)는 * Strict Mode 유무와 무관하게 항상 1회여야 한다. */ export function LogoutNotice({ showError }: LogoutNoticeProps) { const router = useRouter(); const { showToast } = useFeedback(); const hasNotifiedRef = useRef(false); useEffect(() => { if (!showError || hasNotifiedRef.current) { return; } hasNotifiedRef.current = true; // 내부 정보(백엔드 message 원문 등)는 노출하지 않는다 — 항상 이 고정 문구만 보여준다. showToast({ variant: 'warning', message: LOGOUT_ERROR_MESSAGE }); router.replace('/login'); }, [showError, showToast, router]); return null; }