본문으로 건너뛰기

사주고사 제작기 09. SEO 전방위 강화 — JSON-LD, 사이트맵, 메타데이터

·12 min read·9 / 12

'사주고사'를 검색해도 노출이 안 돼서, 할 수 있는 SEO 조치를 전부 했다. JSON-LD 구조화 데이터, 사이트맵 확장, 키워드 보강, Twitter Cards, detective 페이지 동적 메타데이터.


1. 현황 진단

5일차에 robots.txt, sitemap.xml, 결과 페이지 generateMetadata를 넣었지만, 그 뒤로 서비스가 많이 바뀌었다. 인생 추리 모드(20개 캐릭터), 사주 챗봇, 파비콘 교체 — 이 변화들이 SEO에 전혀 반영되지 않고 있었다.

문제 목록:
1. 사이트맵에 루트 URL 1개만 등록 → 20개 detective 페이지 미노출
2. 구조화 데이터(JSON-LD) 전무 → 검색엔진이 사이트 성격 파악 불가
3. 키워드 6개로 부족 → '사주 게임', '무료 사주', 'AI 사주' 등 검색량 높은 키워드 누락
4. Twitter Cards 미설정 → SNS 공유 시 미리보기 빈약
5. detective 페이지에 메타데이터 없음 → 캐릭터별 고유 title/description 부재
6. manifest 테마 컬러가 흰색 → 브랜드 보라색과 불일치

2. JSON-LD 구조화 데이터

왜 필요한가

구조화 데이터는 검색엔진에게 "이 사이트가 뭔지"를 기계가 읽을 수 있는 형태로 알려준다. 일반 메타 태그가 "설명"이라면, JSON-LD는 "명세서"다. Google이 리치 결과(별점, 가격, 앱 정보 등)를 노출할 때 이 데이터를 참조한다.

스키마 선택

사주고사에 적합한 3가지 스키마를 @graph로 묶었다.

const jsonLd = {
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "WebSite",         // 사이트 기본 정보
      name: "사주고사",
      url: siteUrl,
      inLanguage: "ko",
    },
    {
      "@type": "WebApplication",  // 게임 앱으로 인식
      applicationCategory: "GameApplication",
      offers: { "@type": "Offer", price: "0", priceCurrency: "KRW" },
    },
    {
      "@type": "Organization",    // 브랜드 + 로고
      logo: { "@type": "ImageObject", url: `${siteUrl}/icon` },
    },
  ],
};

WebApplication + GameApplication이 핵심이다. Google에 "이건 무료 게임 웹앱"이라고 명시하면, 게임/퀴즈 관련 검색에서 노출 확률이 올라간다. offers.price: "0"으로 무료임을 강조한다.

삽입 위치

layout.tsx<body> 최상단에 <script type="application/ld+json">으로 삽입한다. Next.js의 Metadata API에는 JSON-LD 전용 필드가 없어서, dangerouslySetInnerHTML을 쓴다. 서버에서 정적으로 직렬화되니 XSS 위험은 없다.

<body className="antialiased">
  <script
    type="application/ld+json"
    dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
  />
  {/* ... */}
</body>

3. 사이트맵 확장

Before: 루트 1개

return [
  { url: baseUrl, priority: 1 },
];

After: 루트 + detective 20개

import { detectiveScenarios } from "@/lib/data/detective-scenarios";
 
const detectiveRoutes = detectiveScenarios.map((scenario) => ({
  url: `${baseUrl}/detective/${scenario.characterId}`,
  changeFrequency: "monthly" as const,
  priority: 0.7,
}));
 
return [
  { url: baseUrl, changeFrequency: "weekly", priority: 1 },
  ...detectiveRoutes,
];

시나리오 데이터에서 직접 매핑하니까, 캐릭터가 추가되면 사이트맵도 자동으로 늘어난다. priority: 0.7은 루트(1.0)보다 낮지만, 일반 페이지보다 높게 설정해서 크롤링 우선순위를 확보한다.

5일차에 robots.ts에서 /detective/를 disallow하지 않았던 게 지금 와서 다행이다. quiz와 result는 동적 콘텐츠라 차단했지만, detective는 고정 시나리오라 인덱싱 가치가 있다.


4. 키워드와 메타 설명 보강

키워드: 6개 → 19개

// Before
keywords: ["사주", "사주 퀴즈", "사주 테스트", "사주 실력", "사주팔자", "사주고사"]
 
// After
keywords: [
  "사주고사", "사주", "사주 퀴즈", "사주 테스트", "사주팔자",
  "사주 게임", "사주 추리", "사주풀이", "사주 무료", "오행",
  "운세 퀴즈", "운명 추리", "사주 실력", "명리학", "사주 보기",
  "무료 사주", "오늘의 운세", "사주 해석", "AI 사주",
]

추가한 키워드는 크게 세 축이다:

  • 게임/엔터테인먼트: 사주 게임, 사주 추리, 운세 퀴즈, 운명 추리
  • 무료/접근성: 사주 무료, 무료 사주
  • 기능 키워드: AI 사주, 오늘의 운세, 사주 해석, 사주 보기

메타 설명

Before: "허구 인물의 사주를 풀어보고, 당신의 사주 실력을 인증받으세요."
After:  "사주 힌트로 허구 인물의 운명을 추리하는 퀴즈 게임. 오행 분석, 사주팔자 해석,
         AI 사주풀이까지. 20명의 캐릭터 시나리오와 전문가 에세이 모드를 무료로 즐겨보세요."

"실력 인증"보다 "추리하는 퀴즈 게임"이 검색 의도에 가깝다. 구체적인 숫자("20명")와 기능("AI 사주풀이")을 넣어서 클릭률을 높이려 했다.

타이틀

Before: "사주고사 - 사주 실력 인증 퀴즈"
After:  "사주고사 - 사주 추리 퀴즈 | 사주팔자로 운명을 맞춰보세요"

파이프(|) 뒤에 부가 설명을 넣는 패턴. "사주팔자로 운명을 맞춰보세요"가 검색 결과에서 눈에 띄고, 롱테일 키워드("사주팔자로 운명")도 잡는다.


5. Twitter Cards + OpenGraph 보강

Twitter Cards 추가

twitter: {
  card: "summary_large_image",
  title: "사주고사 - 사주 추리 퀴즈",
  description: "사주 힌트로 한 사람의 운명을 추리하세요. 20명의 캐릭터, 오행 분석, AI 사주풀이.",
},

기존에 OG 태그만 있었다. Twitter/X는 OG 태그를 폴백으로 쓰긴 하지만, twitter:card가 없으면 summary(작은 썸네일)로 기본 표시된다. summary_large_image를 명시해야 큰 이미지 미리보기가 뜬다.

OpenGraph 보강

siteNameurl을 추가했다. siteName은 Facebook/카카오톡에서 출처 표시에 쓰이고, url은 canonical URL 역할을 한다.

googleBot 지시자

googleBot: {
  index: true,
  follow: true,
  "max-video-preview": -1,
  "max-image-preview": "large",
  "max-snippet": -1,
},

max-image-preview: large는 Google 검색 결과에서 큰 이미지 미리보기를 허용한다. max-snippet: -1은 스니펫 길이 제한을 없앤다. 검색 결과에서 더 많은 정보를 보여줄수록 클릭률이 올라간다.


6. detective 페이지 동적 메타데이터

20개 캐릭터 페이지에 고유한 title과 description을 부여했다.

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const { characterId } = await params;
  const scenario = getDetectiveScenarioByCharacterId(characterId);
  const character = getCharacterById(scenario.characterId);
 
  const title = `${character.name}의 운명 추리`;
  const description = `${character.name}(${character.gender}, ${character.birthYear}년생)의 사주를 분석하고 ${scenario.rounds.length}개 라운드를 풀어보세요. ${scenario.sajuProfile.summary}`;
 
  return { title, description, openGraph: { ... }, twitter: { ... } };
}

예시 렌더링:

title: "김하늘의 운명 추리 | 사주고사"
description: "김하늘(남, 1990년생)의 사주를 분석하고 7개 라운드를 풀어보세요.
              물이 넘치고 불이 부족한 사주 — 자유로운 영혼이지만 몸이 따라가지 못하는 운명"

캐릭터 이름, 성별, 출생연도, 라운드 수, 사주 프로필 요약까지 — 검색엔진이 각 페이지를 독립적인 콘텐츠로 인식할 수 있는 충분한 고유 정보가 들어간다.


7. manifest 테마 컬러

- background_color: "#ffffff",
- theme_color: "#ffffff",
+ background_color: "#0a0a0a",
+ theme_color: "#7c3aed",

사이트가 다크 테마 고정인데 manifest만 흰색이었다. theme_color를 브랜드 보라(#7c3aed)로, background_color를 다크 배경(#0a0a0a)으로 맞췄다. 모바일 브라우저 주소창 색상과 PWA 스플래시 스크린에 반영된다.


커밋 로그

dafa4e1 feat: SEO 강화 — 구조화 데이터, 사이트맵 확장, 메타데이터 보강

배운 것

  1. SEO는 한 번에 끝나는 게 아니다. 5일차에 기본 SEO를 했지만, 서비스가 진화하면 SEO도 따라가야 한다. detective 모드를 추가하고, 챗봇을 만들고, 캐릭터 20개로 확장했는데 사이트맵은 여전히 루트 1개였다. 기능 출시와 SEO 업데이트를 세트로 생각해야 한다.

  2. 구조화 데이터는 검색엔진에게 보내는 이력서다. meta 태그가 "나는 이런 사이트예요"라면, JSON-LD는 "이건 무료 게임 앱이고, 한국어로 되어 있고, 이 조직이 운영해요"라는 구조화된 명세다. 검색엔진이 리치 결과를 만들려면 이 명세가 있어야 한다.

  3. 키워드는 사용자의 검색어를 상상해서 고른다. "사주고사"를 직접 검색하는 사람은 이미 서비스를 아는 사람이다. 신규 유입을 원하면 "무료 사주", "사주 게임", "AI 사주"처럼 일반적인 검색어를 키워드에 넣어야 한다. 브랜드명보다 기능 키워드가 중요하다.

  4. 동적 페이지도 고유 메타데이터가 있어야 한다. 20개 캐릭터 페이지가 전부 같은 title/description이면 검색엔진은 중복 콘텐츠로 판단한다. generateMetadata로 캐릭터 이름, 라운드 수, 사주 요약을 넣으면 각각이 독립적인 콘텐츠가 된다.

  5. manifest의 theme_color는 브랜딩이다. PWA 관점에서만 보면 사소하지만, 모바일 Chrome 주소창 색상이 브랜드 색으로 바뀌면 앱처럼 느껴진다. 흰색 주소창 + 다크 콘텐츠는 이질감을 준다.


남은 과제

  • 네이버 서치어드바이저 등록 (한국 검색 트래픽 대부분)
  • Google Search Console에서 사이트맵 재제출
  • 추리 결과 Supabase 저장 (통계/리더보드)
  • 추리 결과 전용 URL (공유 링크에서 직접 결과 확인)
  • generated-questions analysis 콘텐츠 품질 보강
  • 문제/인물 데이터 Supabase 이관