정의
앱 첫 진입에 네이티브 앱처럼 로고 스플래시를 첫 페인트부터 보여 주되, JavaScript를 실행하지 않는 검색 크롤러의 색인에는 그 스플래시가 들어가지 않게 하고 싶을 때가 있다. 이를 위해 서버 렌더(SSR) 마크업이나 User-Agent 분기가 아니라, HTML <body> 최상단에 인라인 블로킹 스크립트를 넣어 스플래시 노드를 DOM에 직접 만든다. 하이드레이션이 끝나면 클라이언트가 opacity로 걷어낸다.
인라인 블로킹 스크립트는 HTML에 직접 박혀 파서가 만나는 즉시 실행되는 <script>다. JS를 안 돌리는 크롤러는 이걸 실행하지 않으므로 스플래시가 봇이 보는 HTML엔 남지 않는다.
왜 필요한가
세 가지 대안이 각각 막혀서 이 패턴이 필요하다.
- SSR 마크업을 쓰면 스플래시가 첫 HTML에 그대로 실려 크롤러 색인에 샌다(원치 않는 로딩 텍스트가 검색결과에 노출).
- UA 게이트(서버에서 요청 헤더를 읽어 "봇이면 스플래시 스킵")는 공유 레이아웃에서
headers()를 읽는 순간, 그 아래 라우트의 정적 생성/ISR을 강제로 dynamic으로 바꿔 정적 캐시를 통째로 무력화한다. - mount 이후에만 뜨는 클라이언트 스플래시는 서버에서 초기 상태가 비어 첫 HTML이 텅 비므로, "로딩 첫 화면"으로는 첫 페인트 갭이 생겨 부적합하다.
인라인 스크립트 주입은 정적 렌더/ISR을 유지하면서 JS 실행 시점에만 스플래시를 그려 세 문제를 동시에 피한다. 여기서 ISR(Incremental Static Regeneration) 은 페이지를 미리 정적으로 구워 두고 주기적으로만 갱신하는 캐시 방식이고, dynamic 강제란 매 요청마다 서버가 새로 렌더하게 되는 것을 뜻한다.
동작 원리
- 최신 React(19)는 body/head 상단에 제3자(브라우저 확장·스크립트)가 끼워 넣은 예상 밖 노드를 하이드레이션에서 너그럽게 건너뛴다. 이 덕에 SSR 없이 주입해도 hydration mismatch가 나지 않는다.
- 스플래시 노드는 React 트리 밖(스크립트가 만든 것)이라, 클라이언트가 DOM에서 직접 제거해도 재조정 충돌이 없다.
실무 적용
하이드레이션 뒤 클라이언트가 .boot-splash에 opacity 0을 주고 transition 종료 시 제거한다.
트레이드오프
- 인라인 스크립트 주입: 정적 캐시/ISR을 지키고 봇 은닉·첫 페인트 노출을 모두 얻는다. 대신 문자열로 스크립트를 만들어 이스케이프·테스트에 신경 써야 한다.
- SSR 마크업: 구현이 가장 쉽지만 봇 색인 누수.
- UA 게이트: 봇 은닉은 되지만 정적 캐시를 깬다.
사용하면 안 되는 경우
- 봇 은닉이 필요 없는 경우 — 그냥 SSR/CSS 스플래시가 더 단순하다.
- 하위 라우트에 정적/ISR 캐시가 전혀 없는 순수 동적 앱 — UA 게이트를 써도 잃을 캐시가 없으므로 이 패턴의 이점이 줄어든다.
흔한 실수
- 공유 레이아웃에서
headers()/cookies()를 무심코 써서 하위 ISR 라우트를 dynamic으로 깨뜨림. - 인라인 스크립트에 넣는 문자열에
</script>가 섞여 스크립트가 조기 종료(이스케이프·textContent로 방지, 경계값 테스트로 검증). - 스플래시 배경색을 body와 다르게 둬 걷히는 순간 색 점프 발생.
관련 개념
- ssr-hydration-mismatch — 주입 노드를 하이드레이션이 어떻게 다루는지의 전제
- usesyncexternalstore-snapshot-identity — 서버에서 비는 mount-gated 클라이언트 상태
- critical-rendering-path — 블로킹 스크립트가 파싱·페인트에 미치는 영향