slug 기반 동적 라우팅 운영 정리
목차
- 배경
- 기존 방식의 한계
- 적용한 해결
- 왜 서버 런타임이 필요한가
- 이미지 용량 이슈와 최적화
- 현재 운영 기준 파일 역할
- 수동 배포 절차
- 남은 체크포인트
- SEO 메타데이터 동작
- Sitemap / Robots 적용 상태
- 백엔드 권장 쿼리
1) 배경
- 요구사항
- 문서 URL을
/doc/<slug>형태로 유지 - slug가 자주 추가되어도 재배포 없이 반영
- 404 우회가 아닌 정식 라우트/SEO 처리
- 문서 URL을
2) 기존 방식의 한계 (output: "export")
- 정적 export + nginx 정적 파일 서빙 구조
- 빌드 시점에 없는 slug 경로는 정식 페이지로 존재 불가
/doc/*를 not-found 우회 렌더링으로 처리하면 라우팅/SEO 신호 불안정
요약: /doc/<slug>를 안정적으로 운영하려면 export 방식은 구조적으로 한계 존재
3) 적용한 해결
3.1 라우팅/렌더링
output: "export"제거- App Router 동적 라우트 사용:
/doc/[...slug] /doc/*를 정식 라우트로 처리
3.2 배포/서빙 구조
- 정적 단일 nginx 컨테이너 방식 ->
nginx + Next web2서비스 분리 - 요청 흐름
browser -> nginx(80/443) -> web:3000(Next)
/api/*는 기존 백엔드 프록시 유지
4) 왜 서버 런타임이 필요한가
- slug는 데이터(문서) 추가로 계속 늘어나는 동적 요구사항
- export는 빌드 산출물 중심이라 "미리 모르는 slug" 대응 불가
- 서버 런타임은 요청 시점에 slug를 받아 조회/렌더 가능
결과:
- 새 slug 즉시 반영 가능
/doc/<slug>정식 URL 유지 가능- 정상/404 분기와 메타 처리 일관성 확보 가능
5) 이미지 용량 이슈와 최적화
5.1 원인
- 초기 서버 런타임 전환 시
node_modules + .next를 그대로 런너 이미지에 포함 - 이미지가 대폭 증가
5.2 조치
next.config.mjs에output: "standalone"적용- 런너 이미지에 아래만 복사
.next/standalone.next/staticpublic
- 실행:
node server.js
5.3 결과
- 수동 배포 이미지 기준
~2.59GB -> ~117MB수준으로 축소
6) 현재 운영 기준 파일 역할
docker-compose.ymlweb: Next 앱 컨테이너 (내부3000)nginx: TLS 종료/리버스 프록시 (80/443)
nginx.conf/->proxy_pass http://web:3000/api/-> 백엔드로 프록시
Dockerfile,Dockerfile.manual- Next standalone 런타임 이미지 빌드
7) 수동 배포 절차 (요약)
- 로컬에서
Dockerfile.manual로 이미지 빌드 docker save로 tar 생성- 서버로 전송 후
docker load - 서버
docker-compose.yml의web이미지 태그 갱신 docker compose up -d web- 필요 시
docker compose up -d nginx(nginx.conf 변경 시)
8) 남은 체크포인트
.env.production/런타임 env 관리 정책 통일- health check/readiness 정책 확정
- 배포 파이프라인에서 image tag 정책(
version,latest, 임시태그) 명확화
9) SEO 메타데이터 동작 (현재 구현)
-
/와/doc- SSR metadata를 공통 변수
homeDocSeo로 관리 src/lib/metadata.ts의homeDocSeo.title,homeDocSeo.keywords수정 시 두 페이지에 동시에 반영
- SSR metadata를 공통 변수
-
/doc/[...slug]generateMetadata에서 slug 기준으로 문서 조회- 문서가 있으면
title = MdDoc.title,keywords = MdDoc.tags - 조회 실패/미존재 시
title = "Docs"fallback
-
/,/doc,/doc/[...slug]초기 데이터 조회- 홈 문서 초기 조회는 서버에서 먼저 수행(SSR) 후 클라이언트에 주입
- 클라이언트는 초기 렌더 시 주입값을 우선 사용하고, 이후 상호작용 시에만 추가 조회
- 관련 유틸:
src/lib/mdDocApi.ts,src/lib/mdDocHome.ts,src/lib/mdDocSlug.ts
-
URL 고정 상태에서 사이드바로 문서 전환
- 서버 metadata는 URL 기준이므로 재생성되지 않음
- 현재는 클라이언트에서
document.title을 추가로 바꾸지 않음 - 즉 URL이 안 바뀌면 title/keywords도 고정 유지
SEO 핵심 태그 정리
| 태그/필드 | 역할 | 현재 적용 위치 |
|---|---|---|
<title> | 검색 결과 제목, 브라우저 탭 제목, 클릭률(CTR)에 가장 큰 영향 | buildPageMetadata, /doc/[...slug]/generateMetadata |
<meta name="description"> | 검색 결과 설명문으로 노출될 수 있음, 페이지 요약 신호 | buildPageMetadata |
<link rel="canonical"> | 중복 URL 정규화, 대표 URL 지정 | buildPageMetadata (alternates.canonical) |
<meta name="robots"> | 인덱싱/링크 추적 허용 여부 제어 (index, follow) | buildPageMetadata |
<meta name="keywords"> | 키워드 보조 신호, 현대 SEO 영향은 낮음 | /, /doc는 homeDocSeo.keywords, /doc/[...slug]는 MdDoc.tags |
Open Graph (og:title, og:description, og:url, og:site_name) | SNS 공유 미리보기 품질, 외부 유입 품질에 영향 | buildPageMetadata.openGraph |
10) Sitemap / Robots 적용 상태
-
src/app/sitemap.ts/sitemap.xml동적 생성- 기본 URL:
/,/doc - 문서 URL: 백엔드
GET /md-doc/slugs응답(items: string[]) 기반 추가 - slug 원문(
test1)을/doc/${slug}URL로 조합 - slug에
/가 포함된 비정상 값은 제외 - URL 기준 dedupe 적용
-
src/app/robots.ts/robots.txt생성sitemap경로를${appUrl}/sitemap.xml로 노출- private/admin 성격 경로는
disallow처리
11) 백엔드 권장 쿼리(슬러그 전용 API)
- 목적: sitemap 생성용 경량 데이터 제공
- 권장 조건:
status = 1slug IS NOT NULLslug는 prefix 없는 값(test1,guide/start)이 아니라 단일 slug 토큰(test1) 기준으로 관리/포함값 배제(프론트 sitemap 규칙과 일치)- 필요 시
distinct
예시(QueryDSL):
@Transactional(readOnly = true)
public List<String> getActiveSlugs() {
return jpaQueryFactory
.selectDistinct(mdDocEntity.slug)
.from(mdDocEntity)
.where(
mdDocEntity.status.eq(1),
mdDocEntity.slug.isNotNull(),
mdDocEntity.slug.notLike("%/%")
)
.fetch();
}