notes
pageKey 발급 목적과 운영 가이드
pageKey 발급 목적·식별 기준과 운영 환경에서의 관리 가이드.
- 새로고침할 때마다 바뀌는 키로 노출, 클릭, 서버 로그를 한 뷰로 묶는다
- 쿼리와 헤더로 같이 넘겨서 프론트와 백엔드 로그를 조인한다
- canonical, CDN 캐시 키, 리다이렉트 루프는 같이 챙겨야 한다
발견한 계기
야놀자에서 숙소를 검색하다 보니 검색 결과 주소에 pageKey 라는 숫자가 붙어 있었다. 새로고침할 때마다 값이 바뀌었다. 뭐에 쓰는 건지 찾아보고 우리 검색 화면에도 적용할 수 있게 정리했다.
pageKey 는 페이지 뷰 한 번을 식별하는 임시 키다. 새로고침이나 재진입마다 새로 발급해서 로그, 실험, 캐시, 서버 상관관계에 쓴다.
왜 필요한가
가장 큰 이유는 노출과 전환을 묶는 것이다. 검색 결과가 노출되고, 카드를 클릭하고, 상세로 넘어간 흐름을 하나의 pageKey 로 묶으면 어떤 뷰에서 무엇이 몇 번 보였고 무엇이 눌렸는지 정확하게 볼 수 있다.
A/B 테스트도 같다. 실험군 배정과 노출, 클릭을 한 뷰 안에서 연결하고, 새로고침하면 새 뷰로 끊는다.
로그 추적에도 좋다. 프론트, BFF, API, 하위 서비스 로그에 같은 pageKey 를 넣어 두면 문제가 생겼을 때 그 뷰에서 일어난 호출을 한 번에 모아볼 수 있다.
캐시 우회에도 쓸 수 있다. 검색처럼 결과가 자주 바뀌는 화면은 쿼리에 키가 붙어 있으면 CDN 캐시를 쉽게 피한다. 대신 너무 많이 피하면 비용이 늘어서 라우트별로 골라서 적용한다.
같은 뷰에서 중복으로 나간 이벤트를 걸러낼 때도 보조 신호가 된다. 다만 결제나 주문의 멱등성 키로는 쓰면 안 된다. 거래는 별도 트랜잭션 키를 쓰고 pageKey 는 뷰 단위 상관관계에만 쓴다.
언제 새로 발급하나
새로고침, 하드 리로드, 검색 탭 이동처럼 같은 경로에 다시 들어오는 것까지 새 페이지 뷰로 보는 진입은 전부 새로 발급한다. 같은 뷰 안에서 스크롤하거나 필터를 바꾸는 건 키를 유지해서 노출과 클릭을 같은 뷰로 묶는다.
새로고침마다 바꾸는 이유는 로그와 실험을 페이지 뷰 단위로 끊어야 성과를 정확하게 볼 수 있어서다. 야놀자처럼 비슷한 서비스도 같은 방식으로 쓰는 것 같다.
어디서 어떻게 발급하나
라우트에 들어올 때 Next 미들웨어에서 발급하면 SSR 이든 CSR 이든 같은 자리에서 처리된다. Next 16 부터는 middleware.ts 가 proxy.ts 로 이름이 바뀌었고 기본 런타임이 Node.js 다.
키는 URL 쿼리 ?pageKey= 로 붙이고, 백엔드 요청에는 X-Page-Key 헤더로도 같이 보낸다. 그러면 서버 로그를 묶기가 쉬워진다.
형식은 Date.now() 에 3에서 6자리 랜덤을 붙인 숫자면 충분하다. 검증은 ^[0-9]{13,18}$ 정도로 한다. 로그 시스템이 요구하면 ULID 나 UUID 를 써도 된다.
미들웨어에서 키를 붙이려고 리다이렉트하면 루프가 돌 수 있다. 첫 주입 때 pk_rotated 같은 1회용 쿠키를 구우고, 다음 요청에서 바로 지운다.
// 발급: 라우트 진입 시
const key = `${Date.now()}${Math.floor(Math.random() * 1e6).toString().padStart(6, '0')}`;
url.searchParams.set('pageKey', key);
// 전파: API 호출 시
fetch('/api/search', { headers: { 'X-Page-Key': key } });
SEO, 공유, CDN
검색 페이지는 보통 색인에서 빼거나 canonical 을 고정한다. pageKey 때문에 주소가 매번 달라져도 canonical 은 키 없는 원래 주소로 둔다.
공유 링크에 pageKey 가 섞일 수 있다. 공유할 때 빼거나 집계 쪽에서 이 파라미터를 무시한다.
CDN 캐시 키에 pageKey 를 넣으면 항상 미스가 난다. 비용이 커지면 일부 경로에만 반영하거나, CDN 에서는 pageKey 를 무시하고 상관관계는 헤더로만 넘기는 정책으로 간다.
쿼리를 쓰는 이유는 눈에 보여서 프록시나 서버에서 바로 알아볼 수 있기 때문이다. 캐시 미스 비용이 부담되면 그때 헤더 전파로 바꾼다.
우리 프로젝트 기준
대상은 /search/* 라우트로 잡았다. 새로고침과 재진입 때는 항상 새 키를 발급하고, 미들웨어에서 한 번 리다이렉트한 뒤 pk_rotated 쿠키로 루프를 막는다. 형식은 String(Date.now()) 에 필요하면 랜덤 접미사를 붙인다.
키는 쿼리로 유지하고, 프론트에서 BFF 나 API 를 부를 때 X-Page-Key 헤더로 같이 보낸다. 프론트 노출, 클릭 이벤트와 서버 요청 로그에는 모두 pageKey 필드를 넣는다. GA4 같은 외부 분석과 묶을 때는 client_id, session_id 와 함께 저장한다.
남은 일은 발급 규칙 문서화, 미들웨어 가드 적용, 백엔드의 헤더 수용, 로그 스키마에 pageKey 추가, canonical 과 CDN 규칙 점검이다. URL 에 키를 남기지 않고 헤더와 메모리로만 쓰는 방식도 나중에 검토해 볼 만하다.