SEO
[!NOTE] AI 어시스턴트와 LLM에 맞게 최적화하려고 하나요? 생성형 엔진 최적화(GEO) 가이드를 참조하세요.
SEO란 실제로 무엇인가요?
SEO(검색 엔진 최적화)는 단순히 "Google에 노출되는 것"이거나 라이브러리가 마법처럼 제공할 수 있는 체크박스로 오해되는 경우가 많습니다. 실제로 SEO는 사람들에게 필요한 가치 있는 콘텐츠를 제공하고 이를 쉽게 찾을 수 있도록 하는 데 중점을 둔 광범위한 분야입니다.
기술적 SEO는 개발자가 가장 직접적으로 다루는 SEO의 하위 분야입니다. 검색 엔진, 크롤러, 순위 결정 시스템, 심지어 LLM의 기술적 요구 사항을 충족하는 도구와 API를 사용하는 작업이 포함됩니다. 어떤 프레임워크가 "SEO 지원이 우수하다"고 말할 때는 일반적으로 이 과정을 간단하게 만들어 주는 도구를 제공한다는 의미입니다.
TanStack Start는 포괄적인 기술적 SEO 기능을 제공하지만, 이를 효과적으로 활용하려면 여전히 직접 노력해야 합니다.
TanStack Start가 제공하는 기능
TanStack Start는 기술적 SEO를 위한 구성 요소를 제공합니다:
- 서버 측 렌더링(SSR) - 크롤러가 완전히 렌더링된 HTML을 수신하도록 보장합니다
- 정적 사전 렌더링 - 최적의 성능과 크롤링 가능성을 위해 페이지를 미리 생성합니다
- 문서 Head 관리 - 메타 태그, 제목, 구조화된 데이터를 완전히 제어합니다
- 성능 - 코드 분할, 스트리밍, 최적화된 빌드를 통해 빠른 로드 시간을 제공합니다
문서 Head 관리
라우트의 head 속성은 SEO를 위한 주요 도구입니다. 페이지 제목, 메타 설명, Open Graph 태그 등을 설정할 수 있습니다.
기본 메타 태그
// src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
head: () => ({
meta: [
{ title: 'My App - Home' },
{
name: 'description',
content: 'Welcome to My App, a platform for...',
},
],
}),
component: HomePage,
})
동적 메타 태그
로더 데이터를 사용하여 콘텐츠 페이지의 동적 메타 태그를 생성합니다:
// src/routes/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
return { post }
},
head: ({ loaderData }) => ({
meta: [
{ title: loaderData.post.title },
{ name: 'description', content: loaderData.post.excerpt },
],
}),
component: PostPage,
})
Open Graph 및 소셜 공유
Open Graph 태그는 페이지가 소셜 미디어에 공유될 때 표시되는 방식을 제어합니다:
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
return { post }
},
head: ({ loaderData }) => ({
meta: [
{ title: loaderData.post.title },
{ name: 'description', content: loaderData.post.excerpt },
// Open Graph
{ property: 'og:title', content: loaderData.post.title },
{ property: 'og:description', content: loaderData.post.excerpt },
{ property: 'og:image', content: loaderData.post.coverImage },
{ property: 'og:type', content: 'article' },
// Twitter Card
{ name: 'twitter:card', content: 'summary_large_image' },
{ name: 'twitter:title', content: loaderData.post.title },
{ name: 'twitter:description', content: loaderData.post.excerpt },
{ name: 'twitter:image', content: loaderData.post.coverImage },
],
}),
component: PostPage,
})
표준 URL
표준 URL은 중복 콘텐츠 문제를 방지하는 데 도움이 됩니다:
export const Route = createFileRoute('/posts/$postId')({
head: ({ params }) => ({
links: [
{
rel: 'canonical',
href: `https://myapp.com/posts/${params.postId}`,
},
],
}),
component: PostPage,
})
구조화된 데이터(JSON-LD)
구조화된 데이터는 검색 엔진이 콘텐츠를 이해하도록 돕고 검색에서 리치 결과를 활성화할 수 있습니다:
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
return { post }
},
head: ({ loaderData }) => ({
meta: [{ title: loaderData.post.title }],
scripts: [
{
type: 'application/ld+json',
children: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Article',
headline: loaderData.post.title,
description: loaderData.post.excerpt,
image: loaderData.post.coverImage,
author: {
'@type': 'Person',
name: loaderData.post.author.name,
},
datePublished: loaderData.post.publishedAt,
}),
},
],
}),
component: PostPage,
})
서버 측 렌더링
TanStack Start에서는 SSR이 기본적으로 활성화됩니다. 이를 통해 검색 엔진 크롤러가 완전히 렌더링된 HTML 콘텐츠를 수신하게 되며, 이는 SEO에 매우 중요합니다.
// SSR is automatic - your pages are rendered on the server
export const Route = createFileRoute('/about')({
component: AboutPage,
})
SSR이 필요하지 않은 라우트에서는 선택적으로 비활성화할 수 있습니다. 하지만 해당 페이지의 SEO에 영향을 줄 수 있다는 점에 유의합니다:
// Only disable SSR for pages that don't need SEO
export const Route = createFileRoute('/dashboard')({
ssr: false, // Dashboard doesn't need to be indexed
component: DashboardPage,
})
자세한 내용은 선택적 SSR 가이드를 참조합니다.
정적 사전 렌더링
자주 변경되지 않는 콘텐츠의 경우, 정적 사전 렌더링은 최적의 성능을 위해 빌드 시점에 HTML을 생성합니다:
Vite
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
export default defineConfig({
plugins: [
tanstackStart({
prerender: {
enabled: true,
crawlLinks: true,
},
}),
],
})
Rsbuild
import { defineConfig } from '@rsbuild/core'
import { tanstackStart } from '@tanstack/react-start/plugin/rsbuild'
export default defineConfig({
plugins: [
tanstackStart({
prerender: {
enabled: true,
crawlLinks: true,
},
}),
],
})
사전 렌더링된 페이지는 더 빠르게 로드되며 크롤링하기 쉽습니다. 구성 옵션은 정적 사전 렌더링 가이드를 참조하세요.
사이트맵
기본 제공 사이트맵 생성
링크 크롤링을 사용하는 사전 렌더링을 활성화하면 TanStack Start가 사이트맵을 자동으로 생성할 수 있습니다:
Vite
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
export default defineConfig({
plugins: [
tanstackStart({
prerender: {
enabled: true,
crawlLinks: true, // Discovers all linkable pages
},
sitemap: {
enabled: true,
host: 'https://myapp.com',
},
}),
],
})
Rsbuild
import { defineConfig } from '@rsbuild/core'
import { tanstackStart } from '@tanstack/react-start/plugin/rsbuild'
export default defineConfig({
plugins: [
tanstackStart({
prerender: {
enabled: true,
crawlLinks: true, // Discovers all linkable pages
},
sitemap: {
enabled: true,
host: 'https://myapp.com',
},
}),
],
})
사이트맵은 빌드 시 라우트에서 발견 가능한 모든 페이지를 크롤링하여 생성됩니다. 정적 또는 대부분 정적인 사이트에 권장되는 접근 방식입니다.
정적 사이트맵
간단한 사이트의 경우 정적 sitemap.xml 파일을 public 디렉터리에 배치할 수도 있습니다:
<!-- public/sitemap.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://myapp.com/</loc>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://myapp.com/about</loc>
<changefreq>monthly</changefreq>
</url>
</urlset>
이 접근 방식은 사이트 구조가 정해져 있고 자주 변경되지 않을 때 적합합니다.
동적 사이트맵
빌드 시 발견할 수 없는 동적 콘텐츠가 있는 사이트의 경우 서버 라우트를 사용하여 동적 사이트맵을 만들 수 있습니다. 성능을 위해 CDN에서 이를 캐시하는 것이 좋습니다:
// src/routes/sitemap[.]xml.ts
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/sitemap.xml')({
server: {
handlers: {
GET: async () => {
const posts = await fetchAllPosts()
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://myapp.com/</loc>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
${posts
.map(
(post) => `
<url>
<loc>https://myapp.com/posts/${post.id}</loc>
<lastmod>${post.updatedAt}</lastmod>
<changefreq>weekly</changefreq>
</url>`,
)
.join('')}
</urlset>`
return new Response(sitemap, {
headers: {
'Content-Type': 'application/xml',
},
})
},
},
},
})
robots.txt
정적 robots.txt
가장 간단한 접근 방식은 정적 robots.txt 파일을 public 디렉터리에 배치하는 것입니다:
// public/robots.txt
User-agent: *
Allow: /
Sitemap: https://myapp.com/sitemap.xml
이 파일은 /robots.txt에서 자동으로 제공됩니다. 대부분의 사이트에서 가장 일반적으로 사용하는 접근 방식입니다.
동적 robots.txt
더 복잡한 시나리오(예: 환경별로 다른 규칙)의 경우 서버 라우트를 사용하여 robots.txt 파일을 만들 수 있습니다:
// src/routes/robots[.]txt.ts
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/robots.txt')({
server: {
handlers: {
GET: async () => {
const robots = `User-agent: *
Allow: /
Sitemap: https://myapp.com/sitemap.xml`
return new Response(robots, {
headers: {
'Content-Type': 'text/plain',
},
})
},
},
},
})
모범 사례
성능이 중요합니다
페이지 속도는 순위 결정 요소입니다. TanStack Start는 다음 기능을 제공합니다:
- 자동 코드 분할 - 각 페이지에 필요한 JavaScript만 로드합니다
- 스트리밍 SSR - 브라우저로 HTML을 즉시 전송하기 시작합니다
- 사전 로딩 - 사용자가 라우트로 이동하기 전에 미리 가져옵니다
콘텐츠가 핵심입니다
기술적 SEO는 전체 퍼즐의 한 조각일 뿐입니다. 가장 중요한 요소는 다음과 같습니다:
- 고품질 콘텐츠 - 사용자에게 가치를 제공하는 콘텐츠를 만듭니다
- 명확한 사이트 구조 - 라우트를 논리적으로 구성합니다
- 설명적인 URL - 의미 있는 경로 세그먼트를 사용합니다(
/posts/my-great-article대신/posts/123) - 내부 링크 - 사용자와 크롤러가 콘텐츠를 발견하도록 돕습니다
구현을 테스트합니다
다음 도구를 사용하여 SEO 구현을 검증합니다:
- Google Search Console - 색인 생성과 검색 성능을 모니터링합니다
- Google Rich Results Test - 구조화된 데이터를 검증합니다
- Open Graph Debugger - 소셜 공유 카드를 미리 봅니다
- 브라우저 DevTools - 렌더링된 HTML과 메타 태그를 검사합니다
검색 순위를 추적합니다
시간 경과에 따른 SEO 성능을 모니터링하려면 Nozzle.io를 권장합니다. Nozzle은 무제한 키워드를 모니터링하고, SERP 기능을 추적하며, 경쟁사 대비 가시성을 분석할 수 있는 엔터프라이즈급 순위 추적 기능을 제공합니다. 기존 순위 추적 도구와 달리 Nozzle은 모든 쿼리의 전체 SERP를 저장하므로, 검색 결과에서 페이지의 성과를 파악하는 데 필요한 완전한 데이터를 제공합니다.