Astro 사이트를 위한 SEO: 2026 완벽 가이드
Astro 사이트의 검색 순위를 끌어올리는 데 필요한 모든 것 — 메타데이터, sitemap, 구조화 데이터, i18n, 그리고 Core Web Vitals.
- Astro
- Technical SEO
자연 검색이 중요하다면 Astro는 선택할 수 있는 최고의 프레임워크 중 하나다 — 그리고 그것은 우연이 아니다. HTML을 내보내고, 기본적으로 JavaScript를 0으로 내보내고, 빌드 시점에 렌더링하는 Astro의 설계 철학 전체가 검색 엔진이 실제로 보상하는 지점과 거의 완벽하게 들어맞는다. 지금 읽고 있는 이 사이트도 Astro로 만들어졌으니, 아래의 모든 내용은 이론이 아니라 실전에서 검증된 것이다.
이 가이드는 Astro 프로젝트의 SEO 표면 전체를 훑는다: 메타데이터와 canonical, sitemap과 robots, JSON-LD 구조화 데이터, 다국어 라우팅, Core Web Vitals, 그리고 조용히 순위를 깎아먹는 함정들. 각 절에는 자신의 프로젝트에 그대로 붙여 넣을 수 있는 실제 코드가 함께 제공된다.
Astro가 SEO에 강한 이유
현대 사이트의 SEO 문제 대부분은 하나의 근본 원인으로 거슬러 올라간다: 크롤러가 다운로드하는 페이지가 사용자가 보는 페이지와 다르다는 것이다. React나 Next의 클라이언트 렌더링 라우트는 빈 <div id="root"></div>를 내보내고 실제 콘텐츠는 나중에 hydrate한다. Google이 JavaScript를 렌더링할 수는 있지만, 유한한 예산을 가진 지연된 두 번째 처리 단계에서 수행한다 — 그 파이프라인 내부에서 실제로 무슨 일이 벌어지는지는 JavaScript SEO와 렌더링 심층 분석을 참고하라.
Astro는 이 문제 전체를 우회한다:
| 고려 사항 | 일반적인 SPA | Astro (기본값) |
|---|---|---|
| 초기 HTML | 빈 셸 | 완전히 렌더링된 콘텐츠 |
| 내보내는 JavaScript | 앱 번들 전체 | 0, 명시적으로 선택하지 않는 한 |
| 크롤 시점에 보이는 콘텐츠 | 렌더 단계 이후 | 첫 요청 시 |
| Time to first byte | 서버 연산 | CDN의 정적 파일 |
Astro의 Islands 아키텍처는 인터랙티브함을 컴포넌트 단위로 선택하게 한다는 뜻이다. JavaScript가 필요한 검색창이나 캐러셀 하나만 hydrate하고, 나머지는 모두 크롤링 가능한 순수 HTML로 남는다. 기본 출력은 정적(SSG)이고, 시맨틱하며, 즉시 색인 가능하다.
🧑💻 개발자 관점: Astro에서 가장 큰 SEO 이점은 ‘결정하지 않음’에서 나온다. 아무것도 하지 않아도 클라이언트 번들 없는 서버 렌더링 HTML을 얻는다. 다른 프레임워크에서는 그것을 얻기 위해 싸워야 한다.
메타데이터와 canonical
title 태그, meta description, canonical URL, Open Graph 태그는 기본 중의 기본이다. 대부분의 팀이 저지르는 실수는 이들을 여러 페이지에 흩어 놓아 점점 동기화가 어긋나게 하는 것이다. 해결책은 props를 받아 모든 곳에서 일관된 <head> 마크업을 내보내는 단일 레이아웃이다.
src/layouts/BaseLayout.astro를 만든다:
---
interface Props {
title: string;
description: string;
image?: string;
}
const { title, description, image = "/og-default.png" } = Astro.props;
// Build an absolute canonical from the current URL + your site origin.
const canonical = new URL(Astro.url.pathname, Astro.site).href;
const ogImage = new URL(image, Astro.site).href;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={ogImage} />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
</head>
<body>
<slot />
</body>
</html>
Astro.site가 해석되려면 astro.config.mjs에 설정해야 한다:
export default defineConfig({
site: "https://example.com",
});
이제 모든 페이지는 데이터를 넘겨주기만 하면 된다:
---
import BaseLayout from "../layouts/BaseLayout.astro";
---
<BaseLayout
title="SEO for Astro Sites: The Complete 2026 Guide"
description="Make your Astro site rank with metadata, sitemaps, and more."
>
<h1>...</h1>
</BaseLayout>
💡 팁: 자기 참조 canonical(페이지가 자신의 깔끔한 URL을 가리키는 것)은 쿼리 문자열, 추적 파라미터, 끝 슬래시 변형에서 비롯되는 중복 콘텐츠 문제에 대한 가장 효과적인 단일 방어책이다.
Sitemap과 robots
sitemap은 검색 엔진에게 어떤 URL이 존재하며 언제 변경되었는지 알려준다. Astro에는 공식 통합 기능이 있으니 설치하고 등록하라:
npx astro add sitemap
이 명령은 설정을 다음과 같이 편집한다:
import sitemap from "@astrojs/sitemap";
export default defineConfig({
site: "https://example.com",
integrations: [sitemap()],
});
astro build를 실행하면 이제 dist/에 sitemap-index.xml과 페이지네이션된 하위 sitemap들이 생성된다. 이 통합은 site가 설정되어 있어야만 동작한다 — 그 origin이 모든 URL의 접두사가 된다.
robots.txt의 경우, 정적 파일은 건너뛰고 동적 엔드포인트를 사용해 sitemap URL이 설정과 항상 동기화되도록 하라. src/pages/robots.txt.ts를 만든다:
import type { APIRoute } from "astro";
const robots = (sitemapURL: URL) => `
User-agent: *
Allow: /
Sitemap: ${sitemapURL.href}
`.trim();
export const GET: APIRoute = ({ site }) => {
const sitemapURL = new URL("sitemap-index.xml", site);
return new Response(robots(sitemapURL), {
headers: { "Content-Type": "text/plain" },
});
};
⚠️ 주의: robots.txt의
Disallow는 색인이 아니라 크롤링을 막는다. 외부 링크가 걸린 disallow된 URL은 여전히 헐벗은 링크 형태로 검색 결과에 나타날 수 있다. 페이지를 색인에서 빼려면 크롤링은 허용하되 대신<meta name="robots" content="noindex">를 추가하라.
구조화 데이터 (JSON-LD)
구조화 데이터는 리치 결과 — 기사 작성자 표기, 빵 부스러기 내비게이션, FAQ 아코디언, 별점 — 를 얻는 방법이다. Google은 이를 script 태그 안의 JSON-LD로 읽는다. Astro에서의 깔끔한 패턴은 frontmatter에서 객체를 만들고 레이아웃에서 직렬화하는 것이다.
BaseLayout.astro에서 선택적 schema prop을 받아, 따옴표가 이스케이프되지 않도록 set:html로 내보낸다:
---
interface Props {
title: string;
description: string;
schema?: Record<string, unknown>;
}
const { title, description, schema } = Astro.props;
---
<head>
<!-- ...other tags... -->
{schema && (
<script
type="application/ld+json"
set:html={JSON.stringify(schema)}
/>
)}
</head>
블로그 글 페이지라면 Article 객체를 넘긴다:
---
const schema = {
"@context": "https://schema.org",
"@type": "Article",
headline: "SEO for Astro Sites: The Complete 2026 Guide",
datePublished: "2026-06-21",
author: { "@type": "Person", name: "Your Name" },
};
---
<BaseLayout title="..." description="..." schema={schema}>
💡 팁: schema 객체를 손으로 작성하는 것은 오류가 나기 쉽다. Schema 생성기는 흔히 쓰는 타입들에 대해 유효한 JSON-LD를 만들어 주므로 prop에 곧바로 붙여 넣을 수 있다. 출력은 항상 Google의 Rich Results Test에서 다시 확인하라.
다국어 (i18n)
여러 언어를 제공한다면 규칙은 단순하고 가차 없다: 페이지의 모든 언어 버전은 자기 참조를 포함해 다른 모든 버전을 hreflang으로 선언해야 한다. 이것을 틀리면 Google이 잘못된 언어를 제공하거나 변형들을 중복으로 취급한다.
astro.config.mjs에서 라우팅을 설정한다:
export default defineConfig({
site: "https://example.com",
i18n: {
defaultLocale: "en",
locales: ["en", "fr", "ja"],
routing: { prefixDefaultLocale: true },
},
});
prefixDefaultLocale: true를 쓰면 콘텐츠가 /en/, /fr/, /ja/ 아래에 존재한다 — 언어 경로와 경쟁하는 모호한 루트가 없다. 그런 다음 <head>에 hreflang 클러스터를 내보낸다:
---
const locales = ["en", "fr", "ja"];
// Strip the current locale prefix to get the shared path segment.
const path = Astro.url.pathname.replace(/^\/[a-z]{2}/, "");
---
{locales.map((loc) => (
<link
rel="alternate"
hreflang={loc}
href={new URL(`/${loc}${path}`, Astro.site).href}
/>
))}
<link
rel="alternate"
hreflang="x-default"
href={new URL(`/en${path}`, Astro.site).href}
/>
x-default 항목은 사용자에게 맞는 언어가 없을 때 어떤 버전을 보여줄지 Google에 알려준다. 위의 경로 계산이 단순하게 유지되도록 번역본은 병렬 파일(content/blog/en/post.mdx, content/blog/fr/post.mdx)로 두라.
Core Web Vitals
Astro의 정적 출력은 유리한 출발점을 준다: hydration 비용 없음, 레이아웃을 흔드는 클라이언트 프레임워크 없음, CDN에서 곧바로 제공되는 HTML. 이것만으로 Largest Contentful Paint(LCP)와 Interaction to Next Paint(INP)의 대부분이 공짜로 해결된다. 그래도 여전히 발목을 잡는 두 가지는 이미지와 폰트다.
Astro에 내장된 <Image /> 컴포넌트를 사용하라 — 최신 포맷을 생성하고, 크기를 조정하며, 치수를 강제한다:
---
import { Image } from "astro:assets";
import hero from "../assets/hero.png";
---
<Image
src={hero}
alt="Descriptive alt text"
width={1200}
height={630}
format="webp"
loading="eager"
/>
그린 존을 위한 짧은 체크리스트:
- 레이아웃 이동(CLS)을 막기 위해 모든 이미지에 명시적
width/height를 설정하라. - LCP 이미지에는
loading="eager"를, 폴드 아래의 나머지에는lazy를 사용하라. - 폰트를 직접 호스팅하고
font-display: swap을 추가해 폰트가 로드되기 전에 텍스트가 렌더링되게 하라. - 가장 중요한 단 하나의 폰트 파일만 preload하라; 전부 preload하지 마라.
- 첫 페인트에 보이는 콘텐츠에는 어떤
client:*디렉티브도 붙이지 마라.
| 지표 | 측정 대상 | Astro의 기본 동작 |
|---|---|---|
| LCP | 가장 큰 요소의 페인트 시간 | 강함 — 정적 HTML, CDN 제공 |
| CLS | 시각적 안정성 | 이미지에 치수가 있으면 강함 |
| INP | 입력 응답성 | 탁월함 — 기본적으로 hydration 없음 |
흔한 함정
로컬 테스트는 통과하면서도 순위를 무너뜨리는 실수들이다:
client:only콘텐츠는 크롤러에게 보이지 않는다.client:only컴포넌트는 서버에서 아무것도 렌더링하지 않는다 — 폴백 HTML이 없다. 중요한 콘텐츠가 그 안에 있으면 색인되지 않는다.client:load(여전히 서버 렌더링됨)를 쓰거나, 더 나아가 콘텐츠를 순수.astro에 두라.- 끝 슬래시 불일치. Astro의
trailingSlash옵션("always","never","ignore")은 호스트가 파일을 제공하는 방식 및 내부 링크가 사용하는 방식과 일치해야 한다. 불일치는/page와/page/를 두 개의 URL로 만들어 링크 자산을 분산시킨다. 하나를 고르고, 설정에 지정하고, 모든 내부 링크가 따르게 하라. site설정 누락.site가 설정되지 않으면 canonical과 sitemap이 조용히 깨지거나 상대 URL을 만든다. 이것이 비어 있거나 유효하지 않은 sitemap의 가장 흔한 원인이다.- 얇은 페이지에
noindex를 잊는 것. 태그 아카이브와 페이지네이션 페이지는 종종 아무 가치를 더하지 않는다. 크롤 예산을 희석시키게 두지 말고noindexmeta 태그를 추가하라. - 정규화를 리다이렉트에 의존하는 것. canonical 태그는 힌트이고, 301은 지시다. 진짜 중복(HTTP 대 HTTPS, www 대 non-www)에 대해서는 호스트나 CDN 수준에서 리다이렉트를 강제하라 — canonical 태그 하나에만 기대지 마라.
⚠️ 주의: 항상 dev 서버가 아니라 실제 산출물로 검증하라.
astro build를 실행하고dist/를 검사하라 — 생성된 HTML을 열어 콘텐츠, meta 태그, JSON-LD가 정적 출력에 실제로 들어 있는지 확인하라.astro dev는 렌더링 차이를 가릴 수 있다.
마무리
Astro는 대부분의 프레임워크가 손으로 만들게 하는 기반을 그냥 손에 쥐여 준다: 서버 렌더링 HTML, 기본 JavaScript 0, 그리고 크롤러와 Core Web Vitals 둘 다 좋아하는 빠른 정적 출력. 당신이 할 일은 그 위에 의도적인 신호를 쌓는 것이다 — 일관된 메타데이터와 canonical을 주입하는 단일 레이아웃, sitemap과 동적 robots 엔드포인트, 리치 결과를 위한 JSON-LD, 모든 언어를 위한 올바른 hreflang 클러스터, 그리고 절제된 이미지와 폰트 처리.
이것들을 잘 해내면, 검색 엔진이 당신을 더 낮게 평가할 수 있는 거의 모든 기술적 이유를 제거한 셈이다.
다음 단계:
- 기반을 제대로 구축하기: Build 레이어
- 크롤러가 JS를 어떻게 다루는지 이해하기: JavaScript SEO와 렌더링
- 유효한 마크업을 빠르게 생성하기: Schema 생성기