Docsslug
27. 데이터베이스 락 이해하기DEV
26. Spring AOP CGLIB 프록시 핵심 정리DEV
25. 익명 댓글 커서 페이징 운영가이드DEV19
23. slug 기반 동적 라우팅 운영 정리DEV
22. Vite to Next 마이그레이션 정리DEV
20. Broken HTTPS 이슈 트래킹DEV
19. Spring 커스텀 어노테이션DEV
18. 홈랩 컨테이너 서비스 운영 WORK LOGDEV
17. 홈랩 컨테이너 서비스 운영 ROAD MAPDEV
16. 브라우저 히스토리 상태 복원DEV
page

slug 기반 동적 라우팅 운영 정리

목차

  • 배경
  • 기존 방식의 한계
  • 적용한 해결
  • 왜 서버 런타임이 필요한가
  • 이미지 용량 이슈와 최적화
  • 현재 운영 기준 파일 역할
  • 수동 배포 절차
  • 남은 체크포인트
  • SEO 메타데이터 동작
  • Sitemap / Robots 적용 상태
  • 백엔드 권장 쿼리

1) 배경

  • 요구사항
    • 문서 URL을 /doc/<slug> 형태로 유지
    • slug가 자주 추가되어도 재배포 없이 반영
    • 404 우회가 아닌 정식 라우트/SEO 처리

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 web 2서비스 분리
  • 요청 흐름
    • 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/static
    • public
  • 실행: node server.js

5.3 결과

  • 수동 배포 이미지 기준 ~2.59GB -> ~117MB 수준으로 축소

6) 현재 운영 기준 파일 역할

  • docker-compose.yml
    • web: Next 앱 컨테이너 (내부 3000)
    • nginx: TLS 종료/리버스 프록시 (80/443)
  • nginx.conf
    • / -> proxy_pass http://web:3000
    • /api/ -> 백엔드로 프록시
  • Dockerfile, Dockerfile.manual
    • Next standalone 런타임 이미지 빌드

7) 수동 배포 절차 (요약)

  1. 로컬에서 Dockerfile.manual로 이미지 빌드
  2. docker save로 tar 생성
  3. 서버로 전송 후 docker load
  4. 서버 docker-compose.yml의 web 이미지 태그 갱신
  5. docker compose up -d web
  6. 필요 시 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 수정 시 두 페이지에 동시에 반영
  • /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 = 1
    • slug IS NOT NULL
    • slug는 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();
}

댓글

0개
첫 댓글을 남겨보세요.