NEXT_PUBLIC_BASE_URL 도메인 정합 + SEO 점검 (algosu.kr → algo-su.com)

요약프로젝트 config의 NEXT_PUBLIC_BASE_URL이 죽은 도메인 algosu.kr(HTTP 000)을 가리키고 Dockerfile에 이 변수가 미주입되어, 프로덕션 빌드가 코드 폴백 algosu.kr을 번들에 인라인 → 라이브(algo-su.com)의 sitemap/robots/canonical/hreflang/OG가 죽은 도메인을 가리키는 SEO 실손해를 교정했다. 코드 폴백 4곳 + JSDoc 예제 + 테스트 단언 + .env.example을 algo-su.com으로 정합하고, GA4/AdSense 선례대로 Dockerfile에 ENV NEXT_PUBLIC_BASE_URL을 명시 주입(암묵 폴백 의존 제거)했다. 빌드 산출물 sitemap.xml/robots.txt가 algo-su.com을 출력함을 직접 검증.

날짜
영향도높음

목표

  • 실 라이브 도메인(algo-su.com)과 불일치하는 NEXT_PUBLIC_BASE_URL 기본값(algosu.kr)을 정합한다.
  • sitemap / robots / canonical / hreflang / Open Graph 등 SEO 산출물이 죽은 도메인을 가리키는 문제를 교정한다.
  • 프로덕션 빌드가 올바른 도메인을 번들에 인라인하도록 보장한다.

배경

Sprint 210 GA4 통합 작업 중, 실 서비스 도메인은 algo-su.com(HTTP 200)인 반면 프로젝트 설정 frontend/.env.exampleNEXT_PUBLIC_BASE_URLhttps://algosu.kr(HTTP 000, 응답 없음)로 되어 있는 불일치가 발견되었다 (sprint-210, project-deploy-and-domain).

조사 결과 두 가지 문제 층위가 확인되었다.

  1. 코드 폴백 드리프트process.env.NEXT_PUBLIC_BASE_URL ?? 'https://algosu.kr' 폴백이 4곳에 중복되어 있고 모두 죽은 도메인을 기본값으로 사용:
    • src/app/sitemap.ts:14 — sitemap URL + ko/en hreflang
    • src/app/robots.ts:13 — sitemap.xml 링크
    • src/app/[locale]/layout.tsx:32metadataBase (모든 OG/twitter URL 기준)
    • src/lib/i18n/metadata.ts:40 — canonical + hreflang alternates
  2. Dockerfile 미주입 — 프로덕션 frontend/DockerfileNEXT_PUBLIC_BASE_URL이 주입되지 않아, 빌드 타임에 코드 폴백 algosu.kr이 그대로 번들에 인라인됨. 즉 현재 라이브의 SEO 산출물이 죽은 도메인을 가리키고 있었다 (SEO 실손해 — canonical 권한 분산, sitemap/robots 비-라이브 도메인 크롤링 유도).

JSON-LD 구조화 데이터와 명시적 og:image는 코드에 없어 본 작업 범위 밖이다.

결정

D0. 폴백 교정 + Dockerfile 명시 주입 (Option B)

두 가지 접근이 검토되었다.

  • 옵션 A: 코드 폴백 기본값만 algo-su.com으로 교정. 프로덕션은 폴백을 통해 올바른 도메인을 얻지만 암묵적.
  • 옵션 B: 폴백 기본값 교정 + Dockerfile에 ENV NEXT_PUBLIC_BASE_URL 명시 주입. GA4(G-NMNVNCKW37)·AdSense 선례와 일관된 명시적 주입 + dev/test/비-Docker 빌드까지 안전한 기본값 보장.

결정: 옵션 B. 폴백만 고치면 프로덕션 도메인이 암묵적(Dockerfile만 봐서는 알 수 없음)이고, Dockerfile만 고치면 dev/test 기본값과 .env.example이 불일치한다. 두 변경 모두 저렴하므로 양쪽을 정합한다. NEXT_PUBLIC_BASE_URL은 공개 클라이언트 값(번들에 인라인되어 브라우저 노출)이므로 SealedSecret이 불요하다 — GA4 측정 ID·AdSense client ID와 동일한 공개값 선례.

D1. dev/demo 목 이메일은 범위 제외

src/contexts/AuthContext.tsx:107([email protected]src/components/layout/AppLayout.tsx:512([email protected])의 목 이메일은 SEO와 무관한 내부 식별자다. 데모 배너 노출 조건이 user?.email === '[email protected]' 동등 비교에 의존하므로, 한쪽만 수정하면 배너가 파손된다. 본 작업 범위에서 의도적으로 제외한다.

D2. 의존성 무변경 — npm install 금지

본 작업은 문자열 리터럴 교정 + Dockerfile ENV 1줄 추가뿐으로 신규 npm 패키지가 없다. sprint-210의 lockfile prune 재발을 차단하기 위해 npm install을 실행하지 않으며, package.json/package-lock.json을 변경하지 않는다. 검증은 npm ci로 CI 환경을 재현한다.

구현

Phase A — 코드 폴백 정합 (Architect)

'https://algosu.kr''https://algo-su.com':

  • src/app/sitemap.ts:14BASE_URL 폴백
  • src/app/robots.ts:13BASE_URL 폴백
  • src/app/[locale]/layout.tsx:32metadataBase 폴백
  • src/lib/i18n/metadata.ts:40baseUrl 폴백
  • src/lib/i18n/metadata.ts:27~31buildLocaleAlternates JSDoc 예제 (문서 정확성)

Phase B — 테스트 단언 정합 (Architect)

src/lib/i18n/__tests__/metadata.test.ts — 환경 변수 미설정 시 기본값 단언:

  • L32: expect(result?.canonical).toBe('https://algo-su.com/')
  • L38: x-default 단언 'https://algo-su.com/problems'

Phase C — 설정/빌드 정합 (Architect)

  • .env.example:5NEXT_PUBLIC_BASE_URL=https://algo-su.com
  • Dockerfile — GA4 ENV 블록 다음에 ENV NEXT_PUBLIC_BASE_URL=https://algo-su.com 추가 (빌드 타임 번들 인라인)

Phase D — Critic R1 후속: 법무 연락처 이메일 정합

초기 SEO grep은 src·.env.example·Dockerfile만 점검해 messages/ 디렉토리를 놓쳤다. Critic R1(Codex)이 messages/ko/legal.json:50·messages/en/legal.json:50의 개인정보처리방침 §7 연락처가 죽은 도메인 [email protected](메일 수신 불가)을 그대로 노출하고 있음을 포착했다. 이는 내부 식별자가 아닌 사용자 노출 법무 연락처이므로 도메인 정합 범위에 해당한다.

사용자 결정에 따라 단순 도메인 치환([email protected])이 아닌 실 운영 이메일 [email protected]으로 교체했다 (메일박스 존재가 보장된 주소 — 도메인 일치보다 실제 수신 가능성 우선). 내부 dev/demo 목 이메일 2건은 D1대로 범위 제외.

검증

Oracle 직접 검증 (npm ci 기반 CI 환경 재현):

  • npm ci → EXIT=0, 1064 패키지 (lockfile 무변경 — 의존성 드리프트 없음)
  • npx tsc --noEmit → EXIT=0, 오류 0건
  • npx next lint (raw) → EXIT=0. SEO 변경 파일(sitemap/robots/metadata/layout) 신규 warning 0건 (기존 UI 컴포넌트·hooks의 inline-style / exhaustive-deps warning만 잔존, 본 변경과 무관)
  • npx next build → EXIT=0, ✓ Compiled 8.4s
  • npx jest --coverage → EXIT=0, 1390 PASS / 0 FAIL, 글로벌 임계값(lines 83 / branches 71 / functions 82 / statements 81) 충족

빌드 산출물 도메인 검증 (직접)

.next/server/app/sitemap.xml.body:
  <loc>https://algo-su.com/</loc>
  <xhtml:link rel="alternate" hreflang="ko" href="https://algo-su.com/" />
  <xhtml:link rel="alternate" hreflang="en" href="https://algo-su.com/en/" />

.next/server/app/robots.txt.body:
  Sitemap: https://algo-su.com/sitemap.xml
  • SEO 산출물(sitemap/robots)에서 algosu.kr 잔존 0건, 모두 algo-su.com
  • 초기 grep은 SEO 코드만 점검했으나 Critic R1이 messages/{ko,en}/legal.json의 사용자 노출 법무 연락처를 추가 포착 → Phase D에서 정합. 최종적으로 frontendalgosu.kr 잔존은 dev/demo 목 이메일 2건만(D1 의도적 제외)

ADR 인덱스 게이트

  • node scripts/check-adr-index-count.mjs --strict → 영구 8 / 토픽 1 / sprint 150
  • node scripts/check-adr-en-coverage.mjs --lint159/159 (100%)
  • node scripts/check-doc-refs.mjs → 0 broken
  • node scripts/check-i18n-residue.mjs --strict → prose Hangul max 2.19% (임계 8% 이내)

신규 패턴

  • 공개 env 도메인 정합 패턴 — 라이브 도메인 변경 시 (a) 모든 코드 폴백 기본값 교정, (b) Dockerfile ENV 명시 주입, (c) .env.example 정합, (d) 빌드 산출물 grep으로 전수 검증 4단계를 함께 수행. 폴백·주입 한쪽만 고치면 암묵/불일치 드리프트.
  • SEO 산출물 빌드 검증 패턴 — sitemap/robots/canonical 변경 시 .next/server/app/sitemap.xml.body·robots.txt.body를 직접 읽어 출력 도메인·hreflang을 확인. 코드 레벨 검증(폴백 값)과 산출물 레벨 검증(실제 출력)을 분리.

Critic 교차 리뷰

R1 — P3 1건 (Codex, codex review --base ae25f51)

"[P3] Correct the remaining algosu.kr inventory — docs/adr/sprints/sprint-212.md:101. This verification note is inaccurate: frontend/messages/en/legal.json and frontend/messages/ko/legal.json still contain the user-facing [email protected] contact address, so the remaining frontend occurrences are not limited to the two dev/demo mock emails."

런타임 코드/설정 변경은 의도(도메인 전환)와 일관됨(no blocking). 유일한 지적은 SEO 코드 grep이 놓친 messages/{ko,en}/legal.json의 사용자 노출 법무 연락처가 죽은 도메인을 유지한다는 비블로킹 P3 1건. → Phase D에서 근본 해소(사용자 지정 운영 이메일로 교체) + 본 ADR 인벤토리 정정.

R2 — CLEAN (Codex, codex review --base ae25f51, Phase D 반영 후 재검토)

"The changes consistently update the public base URL fallbacks, Docker build-time environment, examples, and tests to the new domain. I did not find a discrete regression or blocking issue in the modified lines."

발견 Critical / High / Medium / Low 모두 0건. R1 P3가 Phase D에서 해소됨을 확인.