WordPress → Astro 마이그레이션 — 블로그 전환 실전 삽질기
WordPress에서 Astro 5로 블로그를 마이그레이션하면서 겪은 콘텐츠 변환, SEO 보존, 빌드 에러, 댓글 시스템 전환까지. Cloudflare Pages 배포와 MDX 컴포넌트 설계 과정을 기록한다.
WordPress로 블로그를 운영한 지 한 달. 글은 늘어나는데, 속도가 마음에 안 들었다.
플러그인 하나 추가할 때마다 로딩이 느려진다. PHP 업데이트하면 플러그인이 깨진다. 뭔가 하나 고치면 다른 게 터진다.
정적 사이트로 가자. 그렇게 Astro 마이그레이션이 시작됐다.
🔍 증상: WordPress가 느려지기 시작했다
Oracle ARM 서버에 Docker로 WordPress를 올렸다. 처음엔 괜찮았다.
글이 20개를 넘기자 관리자 페이지가 체감상 느려졌다. Redis 캐시를 달아도 TTFB가 500ms를 넘긴다.
결정적으로, Yoast SEO + Wordfence + WP Statistics를 동시에 쓰면 메모리 256MB로는 빠듯했다.
WordPress의 근본적 한계
| 항목 | WordPress | 정적 사이트 |
|---|---|---|
| TTFB | 300–800ms (캐시 적용 후) | 20–50ms (CDN 엣지) |
| 서버 비용 | Docker + DB + Redis 필요 | 없음 (Cloudflare Pages 무료) |
| 보안 위협 | PHP 취약점, 플러그인 공격 벡터 | 정적 파일 — 공격 표면 없음 |
| 빌드 | 없음 (런타임 렌더링) | 빌드 타임 렌더링 |
| 유지보수 | WP 코어 + 플러그인 + PHP 업데이트 | npm 패키지만 |
💡 Tip. WordPress가 나쁜 건 아니다. 나머지 3사이트(life, it, work)는 여전히 WordPress로 운영 중이다. 개인 칼럼처럼 개발자가 직접 관리하고, 커스텀이 많은 사이트에 Astro가 맞았다.
🔎 원인 분석: 왜 Astro인가

정적 사이트 생성기(SSG)는 여러 가지가 있다. Next.js, Gatsby, Hugo, Jekyll, Astro.
Astro를 고른 이유는 명확했다.
프레임워크 비교
| 기준 | Astro | Next.js | Hugo |
|---|---|---|---|
| 빌드 속도 | ⚡ 빠름 | 보통 | ⚡⚡ 매우 빠름 |
| JS 번들 | 0KB (기본) | 무거움 | 0KB |
| MDX 지원 | ✅ 네이티브 | ✅ 설정 필요 | ❌ |
| 컴포넌트 | Astro + React/Vue | React 전용 | Go 템플릿 |
| 학습 곡선 | 낮음 | 중간 | 중간 (Go 템플릿) |
| 콘텐츠 컬렉션 | ✅ Zod 스키마 | 직접 구현 | 자체 방식 |
Astro의 킬러 피처는 Island Architecture다. 기본적으로 JavaScript를 0바이트 전송한다. 인터랙션이 필요한 컴포넌트만 선택적으로 hydrate한다.
블로그에는 이게 완벽했다. 대부분의 페이지가 정적 콘텐츠이고, JS가 필요한 건 댓글 위젯이나 공유 버튼 정도다.
🛠️ 해결: 마이그레이션 실행
1단계: 프로젝트 세팅
pnpm create astro@latest jongmolee-blog
cd jongmolee-blog
pnpm add @astrojs/mdx @astrojs/sitemap
pnpm add -D @tailwindcss/vite @tailwindcss/typography tailwindcss
astro.config.mjs 설정이 깔끔하다.
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import sitemap from '@astrojs/sitemap';
import mdx from '@astrojs/mdx';
export default defineConfig({
site: 'https://jongmolee.com',
vite: {
plugins: [tailwindcss()]
},
integrations: [sitemap(), mdx()],
markdown: {
shikiConfig: {
theme: 'github-dark',
},
},
});
📌 Note. Tailwind CSS v4부터 Vite 플러그인 방식으로 변경됐다.
@tailwindcss/vite를 쓰면tailwind.config.js없이 CSS 파일에서 직접 설정한다.
2단계: Content Collections 스키마
Astro 5의 Content Collections는 Zod로 프론트매터를 검증한다. 타입 안전성이 확보되니 글 작성할 때 실수를 빌드 타임에 잡아준다.
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const posts = defineCollection({
loader: glob({
pattern: '**/*.{md,mdx}',
base: './src/content/posts',
}),
schema: z.object({
title: z.string(),
description: z.string().optional(),
date: z.coerce.date(),
tags: z.array(z.string()).default([]),
slug: z.string().optional(),
category: z.string().default('개인 칼럼'),
series: z.string().optional(),
seriesOrder: z.number().optional(),
status: z.enum(['draft', 'publish']).default('draft'),
}),
});
export const collections = { posts };
⚠️ Warning. Astro 5에서
glob로더가 새로 도입됐다. Astro 4의getCollection()방식과 import 경로가 다르니 공식 문서를 꼭 확인할 것.
3단계: WordPress 콘텐츠 변환 — 가장 큰 삽질
여기가 진짜 핵심이었다. WordPress의 HTML 콘텐츠를 MDX로 변환해야 한다.
WordPress REST API로 글 목록을 뽑았다.
curl "https://jongmolee.com/wp-json/wp/v2/posts?per_page=50" \
| jq '.[].title.rendered'
문제는 WordPress의 HTML이 지저분하다는 것이다.
<!-- wp:paragraph -->같은 Gutenberg 블록 주석- 인라인 스타일이 덕지덕지
- 이미지 경로가
/wp-content/uploads/2026/03/...형태 - 숏코드(
[shortcode])가 남아있는 경우
HTML → MDX 변환 전략
수동 변환을 택했다. 글이 10편 남짓이었으니 가능했다.
WordPress HTML → 수동으로 Markdown 정리 → MDX 프론트매터 추가
💡 Tip. 글이 50편 이상이면 자동화가 낫다.
turndown라이브러리(HTML→Markdown 변환)를 추천한다. 단, Gutenberg 블록 주석은 사전에 정규식으로 제거해야 한다.
// Gutenberg 블록 주석 제거
const cleaned = html.replace(/<!--\s*\/?wp:\w+.*?-->/g, '');
4단계: 이미지 마이그레이션
WordPress의 미디어 라이브러리에서 이미지를 다운로드했다.
# WordPress 미디어 목록 추출
curl "https://jongmolee.com/wp-json/wp/v2/media?per_page=100" \
| jq '.[].source_url' -r > media-urls.txt
# 일괄 다운로드
mkdir -p public/images/posts
while read url; do
filename=$(basename "$url")
curl -o "public/images/posts/$filename" "$url"
done < media-urls.txt
Astro에서는 public/ 디렉토리에 넣으면 그대로 서빙된다.
MDX에서는 상대 경로로 참조한다.

⚠️ Warning. WordPress가 자동 생성하는 썸네일 크기별 파일(-150x150, -300x300 등)은 가져올 필요 없다. 원본만 가져오고, Astro에서
<Image>컴포넌트로 최적화하는 게 맞다.
5단계: MDX 커스텀 컴포넌트
WordPress에서는 플러그인으로 했던 걸, Astro에서는 컴포넌트로 만든다.
EnvironmentBox — 환경 정보 표시
---
// src/components/EnvironmentBox.astro
interface Props {
env: Array<{ label: string; value: string }>;
}
const { env } = Astro.props;
---
<div class="environment-box">
<h3>🔧 환경</h3>
<table>
{env.map(({ label, value }) => (
<tr><td>{label}</td><td>{value}</td></tr>
))}
</table>
</div>
SeriesNav — 시리즈 네비게이션
같은 시리즈의 다른 글을 자동으로 보여주는 컴포넌트. WordPress에서는 이걸 플러그인 없이 하기 어려웠다.
---
// src/components/SeriesNav.astro
import { getCollection } from 'astro:content';
const { series, currentOrder } = Astro.props;
const allPosts = await getCollection('posts');
const seriesPosts = allPosts
.filter(p => p.data.series === series && p.data.status === 'publish')
.sort((a, b) => (a.data.seriesOrder ?? 0) - (b.data.seriesOrder ?? 0));
---
WordPress 플러그인 5개가 하던 일을 Astro 컴포넌트 3–4개로 대체했다.
| WordPress 플러그인 | Astro 대체 |
|---|---|
| Yoast SEO | 프론트매터 + sitemap 인티그레이션 |
| Table of Contents | TableOfContents.astro 컴포넌트 |
| Series 관리 | SeriesNav.astro + Content Collections |
| 코드 하이라이트 | Shiki (Astro 내장) |
| 댓글 | Remark42 (셀프호스팅) |
🧱 삽질 모음: 마이그레이션 중 만난 에러들
삽질 1: MDX에서 HTML 주석이 빌드를 깨뜨린다
Error: Unexpected character `!` (U+0021) before name
MDX는 HTML이 아니다.
<!-- 주석 --> 문법이 동작하지 않는다.
JSX 주석({/* 주석 */})을 써야 한다.
WordPress에서 가져온 콘텐츠에 Gutenberg 주석이 남아 있으면 빌드가 바로 터진다.
# 모든 MDX 파일에서 HTML 주석 제거
find src/content -name "*.mdx" \
-exec sed -i '' 's/<!--.*-->//g' {} +
삽질 2: 프론트매터 date 형식
ZodError: Expected date, received string
WordPress에서 내보낸 날짜가 "2026-03-08T15:30:00" 형태였다.
z.coerce.date()를 쓰면 문자열도 Date로 변환해준다.
처음에 z.date()로 했다가 전체 빌드가 실패했다.
삽질 3: Tailwind CSS v4 마이그레이션
Tailwind CSS v4는 설정 방식이 완전히 바뀌었다.
/* src/styles/global.css */
@import "tailwindcss";
@plugin "@tailwindcss/typography";
tailwind.config.js가 없다.
CSS 파일에서 직접 설정한다.
이전 버전의 가이드를 보고 따라하면 동작하지 않는다.
📌 Note. Tailwind v4 + Astro 조합은
@tailwindcss/vite플러그인이 필수다.astro.config.mjs의vite.plugins에 등록해야 한다.
삽질 4: 코드 블록 테마가 적용 안 됨
Astro의 Shiki는 기본 테마가 있다.
github-dark로 바꾸려면 astro.config.mjs에서 설정한다.
markdown: {
shikiConfig: {
theme: 'github-dark',
},
},
이걸 mdx 인티그레이션 옵션에 넣으면 안 된다.
최상위 markdown에 넣어야 MDX에도 적용된다.
삽질 5: slug 충돌
Astro 5의 glob 로더는 파일명에서 slug를 자동 생성한다.
프론트매터에 slug를 따로 지정하면 둘 다 유효하다.
URL 라우팅에서 어떤 slug를 쓸지는 [...slug].astro의 구현에 달렸다.
---
// src/pages/posts/[...slug].astro
export async function getStaticPaths() {
const posts = await getCollection('posts');
return posts
.filter(p => p.data.status === 'publish')
.map(post => ({
params: { slug: post.data.slug ?? post.id },
props: { post },
}));
}
---
WordPress의 퍼머링크(/%postname%/)와 동일한 URL을 유지하려면 프론트매터 slug를 맞춰줘야 한다.
⚠️ Warning. slug가 달라지면 기존 검색 엔진 색인이 404가 된다. 마이그레이션 시 URL 1:1 대응은 필수다.
🚀 배포: Cloudflare Pages
GitHub Actions + Cloudflare Pages 조합을 선택했다. (이 부분은 시리즈 #5에서 자세히 다뤘다.)
# .github/workflows/deploy.yml
- run: pnpm build
- uses: cloudflare/wrangler-action@v3
with:
command: pages deploy dist --project-name jongmolee-blog
빌드 → 배포까지 약 40초. WordPress 시절 페이지 로딩 500ms가 Astro에서는 50ms 이내로 줄었다.
🔁 댓글 시스템: Giscus → Remark42
WordPress의 네이티브 댓글을 쓰고 있었다. Astro로 오면서 선택지가 여러 개 생겼다.
- Giscus — GitHub Discussions 기반. 무료. 로그인이 GitHub 계정 필수.
- Remark42 — 셀프호스팅. 익명 댓글 가능. Go 바이너리.
처음에 Giscus를 달았다가 Remark42로 교체했다. 비개발자 독자가 GitHub 로그인을 하겠는가.
Remark42는 Oracle ARM 서버에 Docker로 올렸다.
comment.jongmolee.com으로 서브도메인을 붙였다.
# docker-compose.yml (발췌)
remark42:
image: umputun/remark42:latest
environment:
- REMARK_URL=https://comment.jongmolee.com
- SITE=jongmolee
- SECRET=<비밀키>
volumes:
- ./remark42:/srv/var
💡 Tip. Remark42의 admin 패널에서 댓글 관리가 가능하다. 스팸 필터도 내장되어 있어서 별도 플러그인이 필요 없다.
🛡️ 예방: 마이그레이션 체크리스트
다음에 또 마이그레이션할 일이 있다면 (없길 바라지만), 이 체크리스트를 따를 것이다.
📌 WordPress → SSG 마이그레이션 체크리스트
- URL 매핑표 작성 (기존 slug → 새 slug 1:1 대응)
- 301 리다이렉트 설정 (DNS 또는 _redirects 파일)
- sitemap.xml 제출 (Google Search Console)
- 이미지 원본 백업 + 최적화 (WebP 변환)
- HTML → MDX 변환 (Gutenberg 주석 제거 필수)
- 프론트매터 스키마 설계 (Zod 검증)
- SEO 메타데이터 이전 (title, description, canonical)
- 댓글 데이터 마이그레이션 (또는 새 시스템 도입)
- RSS 피드 확인
- 빌드 → 프리뷰 → 프로덕션 순서로 검증
📊 Before / After 비교
| 항목 | WordPress (Before) | Astro (After) |
|---|---|---|
| TTFB | 300–800ms | 20–50ms |
| 페이지 크기 | ~500KB | ~80KB |
| JS 번들 | ~200KB (플러그인) | ~5KB (댓글 위젯만) |
| 서버 비용 | Docker + DB + Redis | 0원 (Cloudflare Pages) |
| 빌드 시간 | 없음 | ~40초 |
| 보안 업데이트 | PHP + WP + 플러그인 | npm만 |
| SEO 도구 | Yoast (플러그인) | 프론트매터 + sitemap |
| 댓글 | WP 네이티브 | Remark42 (셀프호스팅) |
| 배포 | 수동 or WP-CLI | git push → 자동 |
📝 정리
WordPress에서 Astro로 옮기는 건 생각보다 일이 많았다.
핵심은 세 가지다.
- URL을 보존하라. SEO 색인이 깨지면 복구에 몇 주가 걸린다.
- 콘텐츠 변환을 과소평가하지 마라. WordPress HTML → MDX는 자동화가 필요한 수준이다.
- 한 번에 다 옮기지 마라. 한 사이트씩 하는 게 안전하다.
지금은 jongmolee.com만 Astro다. 나머지 3사이트(life, it, work)는 WordPress를 유지하고 있다. 모든 사이트를 정적으로 만들 필요는 없다.
Astro가 맞는 사이트가 있고, WordPress가 맞는 사이트가 있다. 중요한 건 왜 전환하는지 명확히 아는 것이다.
📚 1인 인프라 구축기 시리즈 (7편)
- 1. Oracle ARM + Docker로 WordPress 4사이트 운영하기
- 2. Cloudflare Full Strict SSL + Nginx 리버스 프록시 삽질 총정리
- 3. CouchDB + Obsidian LiveSync로 메모 동기화 구축하기
- 4. Umami 셀프호스팅 — Docker 설치부터 AdBlock 우회까지
- 5. GitHub Actions + Cloudflare Pages 자동 배포 — Astro 블로그 CI/CD
- 6. WordPress → Astro 마이그레이션 — 블로그 전환 실전 삽질기
- 7. Claude Max 플랜으로 API 호출하면 429가 뜨는 이유 — 인증 체계 5단계 완전 정리