OH SU JINFrontend Engineer

외부 서비스운영 중SEO 회귀 게이트

쏙쏙 컴퍼니

통신 상품을 비교하고 상담을 신청하는 사이트.

담당
프론트엔드 전담, SEO 게이트, 성능 최적화, 배포 파이프라인
팀
백엔드 1 · 프론트 1(본인) · 디자이너 1
기간
2026.06 – 진행 중

통신 상품 비교 사이트라 검색 유입이 곧 상담 신청이다. SEO를 이 프로젝트의 설계 축으로 잡고, API 응답이 메타·구조화 데이터에 프리렌더되도록 빌드타임 fetch를 택했다. 폴백은 전부 걷어내 API가 깨지면 빌드가 실패하게 했다 — 틀린 메타가 배포되느니 직전 배포본이 그대로 남는 편이 낫다.

스택
Next.js 15App Router정적 익스포트TypeScriptZodPlaywrightAWS Amplify

주요 작업

  • 정적 사이트에서는 데이터를 언제 가져오느냐가 곧 색인 여부다

    문제
    • FAQ 클라이언트 fetch → 프리렌더 HTML에서 누락 → SEO 게이트(DOM↔JSON-LD 1:1) 실패
    해결
    • 색인 대상(FAQ·GNB·요금제) → RSC 빌드타임 fetch로 HTML·JSON-LD에 프리렌더
    • 실시간 데이터(상담·방문 로그) → 클라이언트 런타임 분리
    • 가격 하이브리드는 JSON-LD가 빌드 값이라 화면과 구조화 데이터가 어긋나 기각. 신선도는 재배포 훅으로 처리
    • Schema-First — API 스펙을 프론트가 먼저 작성해 백엔드에 전달(데이터 계약 7종), zod로 응답을 1:1 미러링
    성과
    • API 6종을 붙이는 동안 클라이언트 번들 증가 0
    • 응답 필드 누락 시 빌드 단계에서 차단
    • JS 번들 902KB / 예산 950KB · 예산을 넘기면 빌드가 실패한다
  • SEO 100점은 측정한 순간의 스냅샷이다. 커밋 훅으로 고정했다

    문제
    • 메타·OG 경로가 바뀌면 SEO가 무경고로 깨지고 배포 후에야 발견
    • 초기 게이트는 Astro 산출물 diff 방식이라 기준선이 VCS 밖, 로컬에서만 동작
    해결
    • SEO 불변식 검사 스크립트를 작성해 pre-commit 훅에 연결
    • 검사: title·description 중복 · canonical↔og:url · 참조 이미지 실존 · JSON-LD @type · FAQ DOM↔JSON-LD 배열 비교
    • 산출물 자체 검증으로 전환. 홈 canonical에서 origin을 역산해 환경변수 불필요
    • 판정 로직은 I/O 없는 순수 함수 · 테스트 18개
    • 결함 주입(canonical slash · OG 이미지 삭제)으로 검출 실증
    • SEO 게이트 8라우트 × 불변식 12종 · 하나라도 깨지면 커밋이 생성되지 않는다
    • 테스트 단위 138 · E2E 24 · 시각 회귀 8종 · 픽셀 동일성 검증 포함
    성과
    • 커밋마다 build → test → verify:seo · 실패 시 커밋 차단
    • 로컬·CI 동일 결과
    • Lighthouse SEO 100 · 성능 작업 전후 무변. 접근성·SEO 회귀 없음
  • 폴백이 지키고 있던 건 사용자가 아니라 빌드 파이프라인이었다

    문제
    • 빌드 중단 방지용 하드코딩 폴백을 두고 있었는데, FAQ 문구가 이미 분기된 것을 발견 — API에서 빠진 단어(가입)가 폴백에만 남아 있었다
    • 빌드타임 전용 fetch라 폴백은 장애 방어가 아니라 스테일 데이터 배포 경로였음
    해결
    • 6개 fetch 전부 throw로 전환 · 폴백 상수 삭제
    • 테스트 12개를 폴백 검증 → throw 검증으로 반전
    • 잘못된 API URL로 빌드해 exit 1 실증
    성과
    • 빌드 실패 시 직전 배포본이 유지되어 스테일 데이터 배포 경로 제거
    • 백엔드 API 하드코딩 데이터 → 6종 · 폴백 0 · API가 죽으면 빌드가 실패한다
  • 폰트 5벌이 대역폭을 독점해 LCP 이미지가 굶고 있었다

    문제
    • 폰트 5벌 동시 요청(각 160KB) → 저속 4G에서 4초 대역폭 점유, LCP 이미지 대기
    • font-display swap이라 렌더 블로킹이 아닌 대역폭 경합. 차단 실험으로 Performance 31점 값 확인
    해결
    • 빌드 산출물 HTML에서 실사용 글리프 522자 추출 후 서브셋
    • 정규식 추출이 공백·& 누락 → 원본 5벌 전량 다운로드(925KB 개악) → jsdom 파싱으로 교체
    • 커닝 손실(1~4px 밀림)은 시각 회귀 픽셀 비교로만 검출 → harfbuzz 직접 호출로 kern 테이블 보존
    성과
    • 커닝 보존 비용은 weight당 3.8KB · 원본 대비 픽셀 차이 0
    • 웹폰트 전송량 796KB → 149KB (−81%) · 실사용 글리프 522자만 남긴 서브셋 5종
    • Lighthouse Performance 64 → 82 · 통제 A/B 조건(외부 위젯 차단·모바일·동일 로컬), 3회 중앙값
    • FCP 4.6s → 2.0s (−57%)
    • LCP 7.9s → 4.6s (−42%)
    • 페이지 총 전송량 1,157KB → 512KB (−56%)