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정적 사이트
TTFB300–800ms (캐시 적용 후)20–50ms (CDN 엣지)
서버 비용Docker + DB + Redis 필요없음 (Cloudflare Pages 무료)
보안 위협PHP 취약점, 플러그인 공격 벡터정적 파일 — 공격 표면 없음
빌드없음 (런타임 렌더링)빌드 타임 렌더링
유지보수WP 코어 + 플러그인 + PHP 업데이트npm 패키지만

💡 Tip. WordPress가 나쁜 건 아니다. 나머지 3사이트(life, it, work)는 여전히 WordPress로 운영 중이다. 개인 칼럼처럼 개발자가 직접 관리하고, 커스텀이 많은 사이트에 Astro가 맞았다.

🔎 원인 분석: 왜 Astro인가

결국 🔎 원인 분석: 왜 Astro인가 문제의 범인은 이거였다
결국 🔎 원인 분석: 왜 Astro인가 문제의 범인은 이거였다

정적 사이트 생성기(SSG)는 여러 가지가 있다. Next.js, Gatsby, Hugo, Jekyll, Astro.

Astro를 고른 이유는 명확했다.

프레임워크 비교

기준AstroNext.jsHugo
빌드 속도⚡ 빠름보통⚡⚡ 매우 빠름
JS 번들0KB (기본)무거움0KB
MDX 지원✅ 네이티브✅ 설정 필요
컴포넌트Astro + React/VueReact 전용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에서는 상대 경로로 참조한다.

![이미지 설명](/images/posts/screenshot.webp)

⚠️ 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 ContentsTableOfContents.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.mjsvite.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로 오면서 선택지가 여러 개 생겼다.

  1. Giscus — GitHub Discussions 기반. 무료. 로그인이 GitHub 계정 필수.
  2. 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)
TTFB300–800ms20–50ms
페이지 크기~500KB~80KB
JS 번들~200KB (플러그인)~5KB (댓글 위젯만)
서버 비용Docker + DB + Redis0원 (Cloudflare Pages)
빌드 시간없음~40초
보안 업데이트PHP + WP + 플러그인npm만
SEO 도구Yoast (플러그인)프론트매터 + sitemap
댓글WP 네이티브Remark42 (셀프호스팅)
배포수동 or WP-CLIgit push → 자동

📝 정리

WordPress에서 Astro로 옮기는 건 생각보다 일이 많았다.

핵심은 세 가지다.

  1. URL을 보존하라. SEO 색인이 깨지면 복구에 몇 주가 걸린다.
  2. 콘텐츠 변환을 과소평가하지 마라. WordPress HTML → MDX는 자동화가 필요한 수준이다.
  3. 한 번에 다 옮기지 마라. 한 사이트씩 하는 게 안전하다.

지금은 jongmolee.com만 Astro다. 나머지 3사이트(life, it, work)는 WordPress를 유지하고 있다. 모든 사이트를 정적으로 만들 필요는 없다.

Astro가 맞는 사이트가 있고, WordPress가 맞는 사이트가 있다. 중요한 건 왜 전환하는지 명확히 아는 것이다.