Next.js, Gatsby 및 정적 사이트를 위한 쿠키 동의: 개발자를 위한 연동 가이드
정적 사이트에서의 동의(Consent) 문제
Next.js, Gatsby, Nuxt.js 같은 현대적인 JavaScript 프레임워크는 웹 페이지가 빌드되고 전달되는 방식에 패러다임 전환을 가져왔습니다. 페이지는 빌드 시점 또는 서버에서 미리 렌더링된 뒤, 클라이언트에서 하이드레이션(hydration)됩니다. 이로 인해 쿠키 동의에 독특한 문제가 생깁니다. 동의 배너는 어떤 추적 스크립트보다 먼저 준비되어야 하지만, 페이지 자체는 이미 렌더링되어 에지(Edge)에 캐시되어 있을 수 있기 때문입니다.
전통적인 CMP는 서버 렌더링 PHP나 단순 HTML 페이지처럼 문서가 위에서 아래로 선형적으로 로드되는 환경을 전제로 설계되었습니다. 코드 스플리팅, 지연 로딩(lazy loading), 스트리밍 서버 사이드 렌더링이 있는 프레임워크 환경에서는 이런 전제가 깨집니다. 이런 환경에서 ���바른 동의 처리를 구현하려면 렌더링 파이프라인을 이해해야 합니다.
생각보다 더 중요한 "타이밍"
일반적인 HTML 페이지에서는 다른 스크립트보다 먼저 <head> 안에 CMP 스크립트를 넣으면 됩니다. 하지만 Next.js App Router나 Gatsby에서는 상황이 훨씬 복잡합니다.
- 미리 렌더링된 HTML이 먼저 도착: 브라우저는 CDN이나 서버에서 완성된 HTML을 받습니다. 그 HTML 안에 인라인 스크립트나 서드파티 태그가 포함되어 있다면, 동의 로직이 로드되기 전에 실행될 수 있습니다.
- 하이드레이션 갭(hydration gap): React 하이드레이션은 HTML이 그려진 뒤에 일어납니다. 동의 배너가 React 컴포넌트라면, 하이드레이션이 끝나기 전까지는 기능적으로 존재하지 않습니다. 이 갭 동안 Google 태그나 애널리틱스 스크립트가 동의 없이 실행될 수 있습니다.
- 에지 캐싱의 복잡성: ISR(Incremental Static Regeneration)이나 에지 함수(edge functions)를 사용하면 HTML이 캐시됩니다. 클라이언트 측 메커니즘 없이 캐시된 HTML에 동의 여부에 따른 로직을 동적으로 주입할 수 없습니다.
핵심 원칙은 다음과 같습니다. 동의는 컴포넌트 레벨이 아니라 스크립트 레벨에서 확립되어야 한다는 점입니다. 동의 배너를 렌더링하는 React 컴포넌트는, 하이드레이션 이후에야 인터랙티브해진다면 이미 너무 늦습니다.
Next.js App Router 연동
App Router가 도입된 Next.js 13+에서는 스크립트를 다루는 새로운 방식이 생겼습니다. 동의 연동을 위한 권장 접근 방식은 다음과 같습니다.
1단계: 루트 레이아웃에서 CMP 스크립트 로드
루트 layout.tsx에서 Next.js의 Script 컴포넌트에 beforeInteractive 전략을 사용합니다. 이렇게 하면 Next.js가 하이드레이션이 시작되기 전에 초기 HTML 문서에 스크립트를 주입하도록 지시합니다.
beforeInteractive 전략은 매우 중요합니다. 기본값인 afterInteractive 전략은 하이드레이션 이후에 스크립트를 로드하므로, 동의 처리에는 너무 늦습니다. beforeInteractive를 사용하면 CMP 스크립트가 서버 렌더링된 HTML에 포함되어 페이지 로딩과 함께 실행됩니다.
2단계: Google 태그보다 먼저 기본 동의 상태 설정
Google Tag Manager나 gtag.js 스��펫보다 먼저, 기본 동의 상태를 설정하는 인라인 스크립트를 포함합니다. 이렇게 하면 CMP 배너가 나타나기 전에 GTM이 로드되더라도, 거부(denied)를 기본값으로 존중하게 됩니다.
이 인라인 스크립트는 루트 레이아웃의 <head> 안, CMP 및 GTM 스크립트보다 앞에 위치해야 합니다. Next.js에서는 레이아웃의 <head> 요소 안에 일반 <script> 태그를 사용해 이를 추가할 수 있습니다.
3단계: 라우트 변경 처리
싱글 페이지 애플리케이션(SPA) 내비게이션에서는 CMP 스크립트가 한 번만 로드되고, 라우트 변경 시 전체 페이지 리로드가 일어나지 않습니다. CMP는 클라이언트 측 내비게이션 전반에 걸쳐 상태를 유지해야 합니다. FlexyConsent는 이를 자동으로 처리합니다. 한 번 로드되면 재초기화 없이 모든 라우트 변경에서 계속 활성 상태를 유지합니다.
Next.js Pages Router 연동
여전히 Pages Router를 사용하는 프로젝트에서는, 접근 방식은 비슷하지만 루트 레이아웃 대신 _document.tsx를 사용합니다. 커스텀 Document 클래스의 <Head> 컴포넌트 안에 CMP 스크립트를 배치합니다. beforeInteractive 전략은 Pages Router에서도 동일하게 동작합니다.
주요 차이점은 _document.tsx가 서버에서만 렌더링된다는 점입니다. 따라서 이곳에 있는 동의 로직은 반드시 초기 HTML 페이로드 안에 포함됩니다.
Gatsby 정적 사이트 연동
Gatsby는 빌드 시점에 완전히 정적인 HTML을 생성합니다. 요청 시점의 서버 사이드 렌더링이 없기 때문에 일부 측면은 단순해지지만, 다른 부분은 더 복잡해집니다.
gatsby-ssr.tsx사용: 모든 페이지의<head>에 CMP 스크립트를 주입합니다.onRenderBodyAPI를 사용하면 모든 정적 HTML 파일의 head에 스크립트를 추가할 수 있습니다.- 동의를 지연 로딩하는 Gatsby 플러그인 피하기: 일부 커뮤니티 플러그인은 동의를 React 컴포넌트로 감싸 하이드레이션 이후에만 마운트되도록 합니다. 이는 앞서 설명한 타이밍 갭을 그대로 만들어냅니다.
- 기본 동의 상태를 인라인으로 배치:
gatsby-ssr.tsx에서setHeadComponents를 사용해 기본 동의 상태를 설정하는 인라인 스크립트를 추가합니다. 이 ���크립트는 정적 HTML 안에 포함되어 즉시 실행됩니다.
Gatsby의 빌드 타임 방식 덕분에 CDN에 있는 모든 HTML 파일��� 동의 스크립트가 포함됩니다. 이는 오히려 이상적인 구조입니다. 실패하거나 잘못 캐시될 수 있는 서버 로직이 없기 때문입니다.
Nuxt.js에서의 고려 사항
Nuxt.js(Vue 기반)는 자체적인 패턴을 가지고 있습니다. Nuxt 3에서는 useHead 컴포저블이나 nuxt.config.ts의 app head 설정을 사용해 CMP 스크립트를 전역으로 추가합니다. Nuxt는 스크립트를 head에 배치하는 body: false 옵션과, 논블로킹 로딩을 위한 async 속성을 지원합니다.
Nuxt의 서버 사이드 렌더링 모드에서도 원칙은 동일합니다. CMP 스크립트는 마운트 이후 Vue 컴포넌트가 동적으로 주입하는 것이 아니라, 초기 HTML 응답 안에 포함되어야 합니다.
레이아웃 시프트 피하기
동의 배너는 Cumulative Layout Shift(CLS)를 유발하기로 악명이 높습니다. CLS는 SEO 순위에 영향을 주는 Core Web Vital 중 하나입니다. 배너가 페이지 렌더링 후에 튀어나오면, 콘텐츠를 아래로 밀어내거나 예기치 않게 오버레이합니다.
동의 배너로 인한 CLS를 최소화하는 전략은 다음과 같습니다.
- 하단 배너 사용: 뷰포트 하단에 ���치한 배너는 페이지 콘텐츠를 밀어내지 않습니다. CLS 측면에서 가장 우호적인 방식입니다.
- 공간 예약: 상단 배너를 반드시 사용해야 한다면, CSS에서 세로 공간을 미리 예약해 페이지 레이아웃이 배너를 고려한 상태로 렌더링되도록 합니다.
- 로드 시 모달 오버레이 피하기: 페이지가 렌더링된 뒤 전체 화면을 덮는 동의 월(wall)은 레이아웃이 불안정하다는 인상을 줍니다. 반드시 월이 필요하다면, 초기 페이지 상태의 일부로 함께 렌더링되도록 합니다.
- head에서 CMP를 동기적으로 로드: CMP를 head의 렌더 블로킹 스크립트로 로드하면, 배너가 나중에 튀어나오는 대신 초기 페인트의 일부로 함께 나타날 수 있습니다.
FlexyConsent의 프레임워크 무관 접근 방식
FlexyConsent는 컴포넌트 레벨이 아니라 스크립트 레벨에서 동작하도록 설계되어, 어떤 프레임워크든 — 심지어 프레임워크가 전혀 없어도 — 동작합니다. 이것이 중요한 이유는 다음과 같습니다.
- 단일 async 스크립트 태그:
<head>안의 하나의<script>태그만 있으면 ���니다. npm 패키지를 설치할 필요도, 프레임워크별 래퍼나 빌드 설정을 추가할 필요도 없습니다. - 동의 기본값이 즉시 실행: 스크립트는 어떤 콜백이나 DOM 조작보다 먼저 Consent Mode V2 기본값을 설정합니다. 덕분에 Google 태그는 첫 1밀리초부터 동의를 존중합니다.
- DOM 비의존성: 동의 로직은 React, Vue, Svelte가 하이드레이션되기를 기다리지 않습니다. 프레임워크 라이프사이클과 독립적으로 동작합니다.
- SSG, SSR, ISR, CSR 모두 지원: 단순한 스크립트이기 때문에, 페이지가 정적 생성(SSG), 서버 렌더링(SSR), 점진적 재생성(ISR), 클라이언트 렌더링(CSR) 중 어떤 방식으로 만들어졌든 동일하게 동작합니다.
개발자 팁: CMP가 올바르게 연동되었는지 확인하는 가장 간단한 방법은 브라우저의 Network 탭을 열고, 도메인을 Google로 필터링한 뒤 페이지를 새로고침하는 것입니다. 콘솔에 동의 기본값 명령이 나타나기 전에 어떤 Google 요청도 발생해서는 안 됩니다. 만약 발생한다면, CMP가 너무 늦게 로드되고 있는 것입니다.
FlexyConsent의 무료 플랜은 페이지뷰 ���에 제한이 없으며, Next.js, Gatsby, Nuxt, Astro, SvelteKit, Remix, 순수 HTML과 함께 사용할 수 있습니다. 모든 환경에서 연동 방식은 동일합니다. 올바른 위치에 배치된 하나의 스크립트 태그면 충분합니다.