
서론
[주제 소개]: 이번 작업의 핵심은 /blog?page=n, /youtube?page=n 같은 URL 계약은 그대로 유지하면서, 같은 탭에서 페이지 번호를 눌렀을 때만 우리가 직접 fetch 흐름을 제어하도록 바꾸는 것이었습니다. 원래는 Link 기반 App Router 전환에 많이 기대고 있었는데, 실제 사용 중에는 “기존 탭은 14페이지에서 오래 걸리는데, 같은 URL을 새 탭으로 열면 바로 뜬다” 같은 이상한 현상이 나왔습니다.
[왜 작성하였는가?]: 이런 문제는 초보 입장에선 “DB가 느린가?”로 보이기 쉽지만, 실무에서는 DB 쿼리, 클라이언트 라우팅, SEO, 뒤로가기, 새 탭, 광고/추적 스크립트가 한 번에 얽힌 문제인 경우가 많습니다. 이 글은 그 꼬인 덩어리를 어떻게 잘라서 보고, 어떻게 안전하게 고쳤는지 실전 관점으로 정리한 글입니다.
경고
“14페이지가 느리다”와 “기존 탭의 클라이언트 전환이 꼬였다”는 전혀 다른 문제입니다. 이 둘을 구분하지 않으면 쿼리만 손보다가 시간을 다 쓰게 됩니다.
Next.js SEO 유지형 페이지네이션: 핵심 개념 파헤치기
[SSR 첫 렌더]: 사용자가 처음 /blog?page=3에 들어왔을 때 서버가 해당 페이지의 실제 HTML을 바로 내려주는 방식입니다. 검색엔진과 새 탭 직접 진입, 링크 공유가 모두 이 단계에 기대고 있습니다. 왜 중요한가요? 첫 HTML이 비어 있으면 SEO와 공유 안정성이 바로 흔들립니다.
[실제 href 링크]: 페이지 번호 UI가 단순 버튼이 아니라 실제 href="/blog?page=4"를 가진 앵커라는 뜻입니다. 놓치기 쉬운 점: JS로만 페이지를 바꾸면 새 탭 열기, 링크 복사, 브라우저 기본 이동이 깨질 수 있습니다.
[직접 제어 페이지네이션]: 일반 좌클릭만 가로채서 /api/blog/list, /api/youtube/list를 직접 호출하고, 결과를 화면에 반영하는 방식입니다. 실무 적용 시 고려사항: cmd/ctrl 클릭, 우클릭, JS 실패 상황은 브라우저 기본 동작을 그대로 남겨야 합니다.
[요청 경쟁 제어]: 사용자가 13 -> 14 -> 15를 빠르게 누를 때, 14 응답이 늦게 와서 15 화면을 덮어쓰지 않도록 막는 기술입니다. 왜 중요한가요? 페이지네이션의 “불안정함”은 메모리 누수보다 이런 응답 순서 꼬임에서 더 자주 생깁니다.
[URL 동기화]: 화면 내용과 주소창이 항상 같은 페이지를 가리키게 만드는 작업입니다. 놓치기 쉬운 점: URL만 바뀌고 화면이 안 바뀌거나, 화면만 바뀌고 URL이 안 바뀌면 뒤로가기와 SEO가 둘 다 어색해집니다.
Next.js SEO 유지형 페이지네이션: 공식 가이드라인 & 권장 사항
TIP
Next.js 문서도 pushState와 replaceState가 라우터와 동기화될 수 있다고 설명합니다. 즉 “Next 기본 Link만 써야 한다”가 아니라, 목적에 맞는 제어권을 우리가 가져올 수 있다는 뜻입니다.
Next.js SEO 유지형 페이지네이션: 실무 적용 마스터 플랜
문제를 DB와 라우팅으로 분리해서 본다
무엇을 하는가? “깊은 페이지가 느리다”는 현상을 그대로 믿지 않고, 서버 생성이 느린지 클라이언트 전환이 꼬인 건지 먼저 분리합니다.
어떻게 하는가? 같은 ?page=14를 기존 탭 클릭과 새 탭 직접 열기로 각각 비교합니다. 기존 탭만 느리고 새 탭이 빠르면, DB보다 클라이언트 전환 경로를 먼저 의심합니다.
// Before
<Link href={`/youtube?page=${pageNum}`}>{pageNum}</Link>
// After 생각의 기준
// "URL은 유지하되, 같은 탭 좌클릭만 우리가 직접 제어한다"
성공 점검: “같은 URL인데 새 탭은 빠르다”는 사실을 확인하면, 문제의 초점이 훨씬 또렷해집니다.
실수 방지 팁: 처음부터 쿼리 최적화만 들어가면 원인 진단이 빗나갈 수 있습니다.
SSR 셸과 목록 API를 분리한다
무엇을 하는가? 서버 페이지는 첫 렌더만 책임지고, 이후 페이지 번호 이동에 필요한 데이터는 API가 주도록 역할을 나눕니다.
어떻게 하는가? /blog와 /youtube는 그대로 서버에서 렌더하되, /api/blog/list, /api/youtube/list를 따로 둡니다. 응답 shape도 items, totalCount, currentPage, totalPages, hasNextPage로 통일합니다.
// Before
// page.tsx 안에서 DB 조회 + 목록 렌더 + 페이지 전환까지 한 파일이 책임
// After
// page.tsx: 첫 HTML 렌더
// /api/.../list: 이후 페이지 데이터 응답
// client component: same-tab pagination 제어
성공 점검: 첫 요청 HTML에는 목록이 보이고, 같은 탭 번호 클릭 때는 _rsc 대신 /api/.../list가 호출되면 성공입니다.
실수 방지 팁: API 응답 구조가 페이지 렌더용 데이터와 다르면 타입이 금방 흐트러집니다.
페이지 번호는 실제 앵커로 두고, 좌클릭만 가로챈다
무엇을 하는가? 링크 복사, 새 탭, 우클릭 메뉴, JS 실패 대응을 살리기 위해 번호 UI를 진짜 <a href>로 남깁니다.
어떻게 하는가? metaKey, ctrlKey, shiftKey, altKey, target 여부를 확인하고, 평범한 좌클릭일 때만 preventDefault() 후 fetch로 처리합니다.
function shouldHandleClientNavigation(event: React.MouseEvent<HTMLAnchorElement>) {
if (event.defaultPrevented || event.button !== 0) return false
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return false
const target = event.currentTarget.getAttribute('target')
return !target || target === '_self'
}
성공 점검: 같은 탭 클릭은 부드럽게 넘어가고, cmd/ctrl + click은 새 탭이 정상 열리면 성공입니다.
실수 방지 팁: “모든 클릭을 JS로 막는 방식”은 생각보다 빨리 사용자 기대를 깨뜨립니다.
요청 취소와 마지막 응답만 반영하는 규칙을 넣는다
무엇을 하는가? 연속 클릭 때 오래 걸린 이전 응답이 나중 화면을 덮지 못하게 막습니다.
어떻게 하는가? AbortController로 이전 요청을 끊고, requestId를 둬서 마지막 요청만 유효하게 처리합니다.
abortControllerRef.current?.abort()
const controller = new AbortController()
const requestId = requestIdRef.current + 1
requestIdRef.current = requestId
const nextData = await fetchPage(page, controller.signal)
if (controller.signal.aborted || requestId !== requestIdRef.current) return
setData(nextData)
성공 점검: 13 -> 14 -> 15를 빠르게 눌러도 최종 화면이 15로 안정적으로 남아야 합니다.
실수 방지 팁: AbortController만 넣고 “늦게 도착한 응답 무시” 규칙을 빼먹으면 여전히 꼬일 수 있습니다.
URL, 뒤로가기, SEO 안전장치를 같이 맞춘다
무엇을 하는가? 화면 전환이 AJAX처럼 보여도 브라우저 역사와 SEO 계약은 서버 페이지처럼 유지합니다.
어떻게 하는가? 성공 시 pushState로 URL을 맞추고, popstate에서 다시 데이터를 복원합니다. 블로그의 noindex,follow, 유튜브의 canonical, JSON-LD는 그대로 둡니다.
성공 점검: 뒤로가기/앞으로가기 후에도 주소창과 실제 첫 카드가 같은 페이지를 가리키면 성공입니다.
실수 방지 팁: 필터와 검색까지 한 번에 AJAX로 다 바꾸려 들면 범위가 너무 커집니다. 이번처럼 “검색/카테고리는 서버 이동, 페이지 번호만 직접 제어”로 끊는 게 현실적입니다.
실전 포인트
이번 작업도 처음부터 무한스크롤로 바꾸지 않았습니다. 링크 계약과 SEO를 먼저 지키고, 같은 탭 UX만 개선하는 쪽이 훨씬 안전했습니다.
Next.js SEO 유지형 페이지네이션: 생생한 성공 & 실패 사례 분석
배경: 블로그와 유튜브 목록은 직접 링크, 새 탭, SEO가 모두 중요한 화면이었습니다.
적용 전략: 서버 첫 렌더는 유지하고, 페이지 번호 클릭만 /api/blog/list, /api/youtube/list를 직접 호출하도록 바꿨습니다. 실제 링크는 모두 href를 유지했고, AbortController와 popstate를 추가했습니다.
핵심 결과: 같은 탭에서 13 -> 14 -> 15, 뒤로가기/앞으로가기, 검색어/카테고리 유지 상태의 페이지 이동까지 안정적으로 동작했습니다. 새 탭과 직접 URL 진입도 그대로 유지됐습니다.
성공 요인 분석: “SEO 계약”과 “같은 탭 UX”를 분리해서 생각한 것이 컸습니다. 전부 AJAX로 바꾸지 않고, 필요한 부분만 직접 제어한 것이 오히려 안정성을 높였습니다.
배경: 초기 구조는 Link 중심의 페이지 번호 이동이었고, 사용자는 그냥 번호를 누르면 될 뿐이었습니다.
실패 요인: 기존 탭에서는 App Router의 클라이언트 전환 흐름이 상태, 외부 스크립트, 응답 타이밍과 섞이면서 체감이 불안정해졌습니다. 특히 “기존 탭은 오래 걸리는데, 새 탭으로 같은 URL을 열면 빠른” 현상이 대표적이었습니다.
얻은 교훈: “링크는 유지하되, 같은 탭 클릭 흐름은 우리가 직접 통제한다”는 중간 구조가 필요했습니다. 라우터에 다 맡기는 것도, 모든 걸 무한스크롤로 바꾸는 것도 답이 아니었습니다.
자주 묻는 질문(FAQ)
A1. 보통은 DB가 14페이지를 못 만드는 문제가 아니라, 기존 탭 안의 클라이언트 전환 흐름이 상태와 외부 요청들 사이에서 더 복잡하게 돌아가기 때문입니다.
A2. 맞습니다. 더 단순하고 안정적일 수 있습니다. 다만 이번처럼 UX도 챙기고 싶다면, URL 유지형 직접 제어 페이지네이션이 한 단계 더 정교한 해법입니다.
A3. 아닙니다. 오히려 실제 href를 가진 앵커는 SEO, 공유, 새 탭, 직접 진입 관점에서 더 기본기가 탄탄합니다. 중요한 건 첫 HTML과 메타데이터를 제대로 유지하는 것입니다.
A4. 꼭 그렇지 않습니다. 실무에서는 범위를 나누는 게 중요합니다. 이번처럼 검색 제출과 카테고리 변경은 서버 이동으로 남기고, 페이지 번호만 직접 제어해도 충분히 좋아집니다.
A5. 아닙니다. 이전 요청 취소와 함께 “마지막 요청만 반영” 규칙, URL 동기화, 뒤로가기 복원까지 같이 있어야 안정적입니다.
A6. 지금 데이터 규모에선 버틸 수 있습니다. 하지만 공식 문서도 offset이 커질수록 느려질 수 있다고 안내합니다. 깊은 페이지가 많아지면 커서 기반 전환을 검토해야 합니다.
A7. 피드형 서비스엔 좋을 수 있지만, 블로그 목록처럼 URL 공유와 페이지 직접 진입이 중요한 곳에서는 오히려 관리 난도가 올라갈 수 있습니다.
A8. 이 프로젝트의 핵심 SEO 대상은 목록이 아니라 개별 포스트 /post/[id]입니다. 목록 페이지는 탐색용이고, 상세 포스트가 색인 핵심이기 때문입니다.
Next.js SEO 유지형 페이지네이션: 실전 운영 팁 & 주의사항
[팁 1]: 페이지네이션 문제를 보면 바로 “쿼리가 느리다”로 가지 말고, 기존 탭 클릭과 새 탭 직접 진입을 꼭 비교해 보세요. 이 한 번의 비교가 원인 추적 속도를 크게 줄여줍니다.
[팁 2]: 첫 HTML에 목록이 실제로 들어 있는지 확인하세요. 브라우저에서 보인다는 것만으로는 부족하고, 서버 응답 HTML 안에도 제목과 페이지 링크가 들어 있어야 진짜 SSR입니다.
[팁 3]: 광고, 통계, 썸네일 같은 외부 요청은 페이지네이션 체감을 흐릴 수 있습니다. 체감이 이상할 때는 API 응답 시간과 외부 리소스 요청을 분리해서 봐야 합니다.
[팁 4]: skip + countDocuments() 조합은 데이터가 커질수록 다시 병목이 될 수 있습니다. 지금은 구조 안정화가 우선이지만, 이후에는 커서 페이지네이션과 count 전략 분리도 준비해 두는 편이 좋습니다.
[팁 5]: “페이지 번호 UI”와 “검색/필터 UI”를 한 번에 다 AJAX로 바꾸지 마세요. 페이지네이션 하나만 먼저 안정화해도 체감 품질은 크게 올라갑니다.
[팁 6]: 테스트는 꼭 2 -> 3 -> 4만 하지 말고 13 -> 14 -> 15, 뒤로가기, 앞으로가기, cmd/ctrl + click, 직접 URL 진입까지 포함해야 합니다. 페이지네이션은 정상 케이스보다 경계 케이스에서 본색이 드러납니다.
마지막 운영 메모
“SEO를 지키려면 무조건 서버”, “UX를 지키려면 무조건 SPA”라는 이분법이 제일 위험합니다. 실무에서는 URL 계약은 서버처럼 지키고, 같은 탭 전환만 클라이언트에서 직접 제어하는 중간 해법이 생각보다 가장 오래 갑니다.