사주고사 제작기 09. SEO 전방위 강화 — JSON-LD, 사이트맵, 메타데이터
'사주고사'를 검색해도 노출이 안 돼서, 할 수 있는 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 보강
siteName과 url을 추가했다. 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 강화 — 구조화 데이터, 사이트맵 확장, 메타데이터 보강남은 과제
- 네이버 서치어드바이저 등록 (한국 검색 트래픽 대부분)
- Google Search Console에서 사이트맵 재제출
- 추리 결과 Supabase 저장 (통계/리더보드)
- 추리 결과 전용 URL (공유 링크에서 직접 결과 확인)
- generated-questions analysis 콘텐츠 품질 보강
- 문제/인물 데이터 Supabase 이관