File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
'use client';
import { useEffect, useRef, useState } from 'react';
import { SplashScreen } from '@/components/ui/splash-screen';
import { SPLASH_SHOWN_STORAGE_KEY } from '@/lib/constants/splash';
/** 로딩 자체가 즉시 끝나도 스플래시가 깜빡이지 않도록 유지하는 최소 표시 시간(ms). */
const SPLASH_MIN_DISPLAY_MS = 700;
function hasShownSplashThisSession(): boolean {
try {
return sessionStorage.getItem(SPLASH_SHOWN_STORAGE_KEY) === '1';
} catch {
return false;
}
}
function markSplashShown(): void {
try {
sessionStorage.setItem(SPLASH_SHOWN_STORAGE_KEY, '1');
} catch {
// sessionStorage 접근 불가 환경(시크릿 모드 제한 등) — 매 로드마다 재표시되는 것으로 완화한다.
}
}
/**
* 스플래시 표시 게이팅 로직 — 비주얼(SplashScreen, design 소유)은 항상 서버 렌더와 동일하게
* 마운트해 hydration 불일치를 만들지 않는다. "이미 이번 세션에 표시했음"으로 인한 깜빡임 방지는
* `app/layout.tsx`의 `<head>` 동기 inline script + CSS가 첫 페인트 전에 `<html>`에 부여한
* data attribute가 담당한다(useEffect는 첫 페인트 이후에 실행되어 깜빡임을 막을 수 없다 —
* preventing-flash-before-hydration.md). 이 컴포넌트는 같은 판정을 렌더 중 ref에 1회 계산해두고,
* 이미 표시한 세션이면 effect에서 타이머 자체를 걸지 않는다(= state를 effect 본문에서 동기
* 호출하지 않아 cascading render를 만들지 않는다).
*/
export function SplashGate() {
const [isVisible, setIsVisible] = useState(true);
const [isExiting, setIsExiting] = useState(false);
const skipIntroRef = useRef<boolean | null>(null);
if (skipIntroRef.current === null) {
skipIntroRef.current =
typeof window !== 'undefined' && hasShownSplashThisSession();
}
useEffect(() => {
if (skipIntroRef.current) {
// 이미 <head> inline script + CSS로 숨겨진 상태 — 타이머/퇴장 애니메이션을 돌릴 필요가 없다.
return;
}
const timer = setTimeout(() => {
setIsExiting(true);
}, SPLASH_MIN_DISPLAY_MS);
return () => clearTimeout(timer);
}, []);
function handleExitAnimationEnd() {
markSplashShown();
setIsVisible(false);
}
if (!isVisible) {
return null;
}
return (
<div
data-splash-gate
className={isExiting ? 'animate-fade-out' : undefined}
onAnimationEnd={isExiting ? handleExitAnimationEnd : undefined}
>
<SplashScreen />
</div>
);
}