본문으로 건너뛰기
Hooney
English
프로덕트

모듈 · 제품에서 사용 중

seo

페이지의 메타데이터와 구조화 데이터를 동적 변수로 채우는 SEO 엔진입니다.

4이 엔진으로 페이지 SEO를 정의하는 앱: 홈페이지, 블로그, Journey, ROUND

기간
2026.05 ~ 현재
소속
Firstage
역할
설계와 구현 (1인)
쓰임
페이지는 값만 넘기고, 같은 템플릿이 렌더 시점에 변수를 그 값으로 바꿔 메타데이터와 JSON-LD를 만듭니다.
핵심 작업과 결과
  • 페이지마다 site, page, hreflang처럼 이름공간으로 묶은 렌더 문맥을 만들고, 템플릿은 '{{page.title}}'처럼 그 경로를 가리킴
  • 변수 하나만 든 값은 원래 타입을 유지해, '{{page.imageWidth}}'가 문자열이 아닌 숫자로 나감
  • 같은 변수라도 메타데이터는 그대로, JSON-LD는 JSON에 안전하게 이스케이프하는 렌더러를 따로 둠
  • Firstage 홈페이지와 블로그, Journey, ROUND가 이 엔진으로 페이지 SEO를 정의
기술 스택
  • TypeScript
  • Mustache
  • Zod
  • schema.org

배경

Firstage는 여러 앱의 페이지를 한국어, 영어, 일본어로 내보냅니다. 페이지마다 제목과 설명, 대표 이미지, 언어별 주소, schema.org 구조화 데이터를 맞춰야 하는데, 앱마다 메타데이터 객체를 직접 조립하면 같은 규칙이 여러 곳에 흩어집니다. 한 곳을 고쳐도 다른 앱은 예전 규칙에 남습니다. 메타데이터의 모양은 템플릿 한 벌로 두고, 페이지마다 달라지는 값만 동적 변수로 바꿔 끼우려고 만든 엔진입니다.

동적 변수가 동작하는 방식

  1. 페이지 하나를 defineSeoModule 단위로 정의합니다. 이 모듈의 buildContext가 장소, 언어, 사이트 설정 같은 입력을 받아 렌더 문맥을 만듭니다. 문맥은 site, page, hreflang처럼 이름공간으로 묶여 있습니다.

  2. 템플릿은 코드가 아니라 데이터입니다. 메타데이터의 모양을 그대로 두고, 값 자리에 문맥의 경로를 변수로 씁니다. 아래는 Firstage 홈페이지가 쓰는 템플릿의 일부입니다.

    export const HOMEPAGE_SEO_METADATA_TEMPLATE = {
      title: '{{page.title}}',
      alternates: {
        canonical: '{{page.canonical}}',
        languages: {
          'x-default': '{{hreflang.xDefault}}',
          en: '{{hreflang.en}}',
          ja: '{{hreflang.ja}}',
          ko: '{{hreflang.ko}}',
        },
      },
      openGraph: {
        siteName: '{{site.name}}',
        images: [{ url: '{{page.imageUrl}}', width: '{{page.imageWidth}}', height: '{{page.imageHeight}}' }],
      },
    };
  3. 서버에서 페이지를 렌더할 때 Mustache가 변수를 문맥의 값으로 바꿉니다. 변수 하나만 든 값은 문맥 값의 원래 타입을 유지해, 이미지 너비 같은 숫자는 '{{page.imageWidth}}'에서도 숫자 그대로 나갑니다. '{{page.imageWidth}}px'처럼 글자와 섞이면 문자열이 됩니다.

  4. 같은 변수라도 출력마다 렌더러가 다릅니다. Next.js가 메타데이터를 직접 이스케이프하므로 메타데이터 렌더러는 이스케이프하지 않습니다. JSON-LD 렌더러는 JSON에 안전한 방식으로 이스케이프해서 {{var}}와 {{{var}}}가 똑같이 안전합니다. 전역 설정을 바꾸지 않으므로, 동시에 렌더되는 요청끼리 설정이 섞이지 않습니다.

  5. 개발과 테스트에서는 렌더 결과를 Zod 스키마로 다시 검사합니다. 스키마에 맞지 않는 결과가 나오면 그 자리에서 실패합니다.

결정과 구현

  • 글, 이동 경로, 단체, 상품, 서비스, 웹사이트 같은 schema.org 타입 11종에 빌더를 두었고, 모두 같은 @context로 출력됩니다.
  • 템플릿이 데이터라서, 나중에 관리자가 고친 템플릿을 저장해 기본값을 바꿔도 호출하는 코드는 그대로입니다. 이를 위해 관리 화면의 변수 선택기가 쓸 변수 카탈로그 타입(변수 설명, 타입에 맞는 입력, 미리보기 예시, 폐기 안내)을 먼저 정의했습니다.

현재 상태

Firstage 홈페이지의 장소, 코스, 매거진, 가이드 페이지와 Firstage 블로그, Journey, ROUND 첫 화면이 이 엔진으로 페이지 SEO를 만듭니다. 관리자가 템플릿을 고치는 화면과 변수 선택기는 아직 만들지 않았습니다.