WordPress에서 Next.js로의 마이그레이션: 완벽한 기술 가이드
TL;DR
WordPress에서 Next.js로 2026년에 마이그레이션하면 측정 가능한 성능 향상을 얻을 수 있습니다: 평균 TTFB가 1,200ms에서 85ms로 감소하고, 페이지 무게가 3.2 MB에서 620 KB로 축소되며, Lighthouse 점수가 42에서 94로 향상됩니다. 프로세스는 WP REST API를 통해 콘텐츠를 Supabase 또는 Payload CMS로 내보내고, 미디어를 객체 스토리지로 마이그레이션하며, 301 리디렉션을 위해 모든 URL을 매핑하고, Yoast/RankMath SEO 데이터를 Next.js Metadata API에 보존하며, Gravity Forms 같은 플러그인을 Server Actions로 교체하는 것을 포함합니다. WooCommerce 사이트의 경우 Stripe가 전체 상거래 스택을 대체합니다. 100-500페이지가 있는 일반적인 사이트의 경우 4-8주가 소요되며, 가장 많은 시간은 리디렉션 테스트와 템플릿 재구축에 소요됩니다.
이것은 과장된 주장이 아닙니다. 저는 12년 이상 WordPress로 구축했습니다. 저는 에이전시 사이트, 멤버십 플랫폼, 월간 매출이 6자리 수를 넘는 WooCommerce 스토어, 그리고 제가 기억하고 싶지 않은 커스텀 포스트 타입들을 많이 출시했습니다. 저는 또한 프로덕션 사이트를 Next.js + Supabase로 마이그레이션했습니다. 다음은 모든 기술적 세부사항입니다 -- 무엇이 깔끔하게 매핑되는지, 무엇이 안 되는지, 그리고 무엇을 계획해야 하는지에 대해 설명합니다.
저는 WordPress가 나쁘다고 가장하지 않을 것입니다. 그것은 아닙니다. 그것은 좋은 이유로 웹의 43%를 지탱합니다. 하지만 특정 프로젝트의 경우 -- 성능이 비즈니스 지표인 사이트, 보안 공격 표면이 중요한 곳, 배포 파이프라인을 소유하고 싶은 곳 -- Next.js가 더 나은 도구입니다. 마이그레이션은 어떨까요? 제대로 계획하지 않으면 지뢰밭입니다.
이 가이드는 제가 사용하는 정확한 프로세스를 다룹니다. 실제 코드, 실제 함정, 그리고 무엇을 얻고 무엇을 잃을지에 대한 정직한 평가가 포함되어 있습니다.
목차
- 콘텐츠 마이그레이션: WP REST API를 Supabase 또는 Payload CMS로
- 미디어 마이그레이션: wp-content에서 Supabase Storage로
- URL 구조: 모든 이전 URL 매핑
- SEO 마이그레이션: Yoast 및 RankMath 데이터를 Next.js Metadata로
- 폼: Gravity Forms를 Server Actions로
- WooCommerce에서 Stripe로
- 마이그레이션 후 모니터링
- WordPress에 머물러야 할 때
- 성능 벤치마크: 이전과 이후
- FAQ

콘텐츠 마이그레이션: WP REST API를 Supabase 또는 Payload CMS로
모든 WordPress 마이그레이션은 여기서 시작됩니다. 포스트, 페이지, 커스텀 포스트 타입, ACF 필드, 분류법이 있습니다 -- 어딘가 안전한 곳으로 옮겨야 할 몇 년치의 콘텐츠입니다.
콘텐츠가 갈 수 있는 두 가지 좋은 선택지가 있습니다:
- Supabase -- 완전히 제어할 수 있는 데이터베이스를 원할 때, 행 수준 보안 및 기본 제공 REST/GraphQL API와 함께
- Payload CMS -- 클라이언트가 WordPress 이후 친숙하게 느껴지는 시각적 편집 경험이 필요할 때
우리의 헤드리스 CMS 개발 프로젝트의 경우, 클라이언트별로 이를 평가합니다. Payload는 에디터가 셀프 서비스가 필요할 때 승리합니다. Supabase는 개발자가 주요 콘텐츠 관리자이거나 웹사이트 이상의 데이터가 필요할 때 승리합니다.
WordPress에서 Next.js로 마이그레이션할 때 어떤 콘텐츠 구조를 보존해야 합니까?
마이그레이션 중에 포스트 메타데이터, 분류법, 커스텀 필드 및 URL 슬러그를 보존합니다. WordPress 포스트에는 수년의 구조화된 데이터가 포함되어 있습니다: 카테고리, 태그, ACF 필드, 특집 이미지, 그리고 게시 날짜. _embed 매개변수로 WP REST API를 통해 모든 것을 내보내 단일 요청으로 미디어 URL을 가져옵니다. 콘텐츠의 HTML과 Markdown 버전을 모두 저장합니다 -- HTML은 폴백으로, Markdown은 MDX 렌더링용입니다. 커스텀 포스트 타입을 새 시스템의 동등한 데이터베이스 테이블 또는 CMS 컬렉션에 매핑합니다.
내보내기 스크립트
다음은 WP REST API에서 콘텐츠를 가져오고, 정리하며, Supabase에 삽입하는 데 사용하는 Node.js 스크립트입니다. 이것은 포스트를 처리하지만, 페이지 및 CPT에 대해 패턴을 복제합니다 (끝점만 변경하면 됨).
import { createClient } from '@supabase/supabase-js';
import TurndownService from 'turndown';
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_KEY
);
const turndown = new TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced',
});
const WP_API = 'https://yoursite.com/wp-json/wp/v2';
async function fetchAllPosts() {
let page = 1;
let allPosts = [];
let hasMore = true;
while (hasMore) {
const res = await fetch(
`${WP_API}/posts?per_page=100&page=${page}&_embed`
);
if (!res.ok) break;
const posts = await res.json();
allPosts = allPosts.concat(posts);
const totalPages = parseInt(res.headers.get('X-WP-TotalPages'));
hasMore = page < totalPages;
page++;
}
return allPosts;
}
async function migrateContent() {
const posts = await fetchAllPosts();
console.log(`Fetched ${posts.length} posts from WordPress`);
const transformed = posts.map((post) => ({
wp_id: post.id,
title: post.title.rendered,
slug: post.slug,
content_html: post.content.rendered,
content_markdown: turndown.turndown(post.content.rendered),
excerpt: post.excerpt.rendered.replace(/<[^>]*>/g, '').trim(),
published_at: post.date,
status: post.status,
featured_image:
post._embedded?.['wp:featuredmedia']?.[0]?.source_url || null,
categories:
post._embedded?.['wp:term']?.[0]?.map((t) => t.name) || [],
tags:
post._embedded?.['wp:term']?.[1]?.map((t) => t.name) || [],
}));
const { data, error } = await supabase
.from('posts')
.upsert(transformed, { onConflict: 'wp_id' });
if (error) {
console.error('Migration failed:', error);
} else {
console.log(`Migrated ${transformed.length} posts to Supabase`);
}
}
migrateContent();
몇 가지 제가 힘들게 배운 것들:
- 항상 WP REST API 호출에
_embed를 사용합니다. 없으면 미디어 ID 대신 URL을 얻게 되어 특집 이미지를 해결하기 위해 N+1 요청이 필요합니다. - Turndown은 HTML을 Markdown으로 변환합니다 -- 나중에 MDX로 렌더링하려면 이것이 중요합니다. 원본 HTML도 폴백으로 유지합니다.
- 숏코드는 살아남지 못합니다. WordPress는 WP REST API를 통해 일부 숏코드를 렌더링하지만, 많은 것들 (특히 WPBakery 또는 Elementor 같은 플러그인에서) 원본 대괄호 텍스트로 나옵니다. 숏코드-컴포넌트 매핑 전략이 필요합니다. 저는 스프레드시트를 유지합니다.
- ACF / 커스텀 필드: ACF를 사용하고 있다면, ACF to REST API 플러그인을 활성화해야 하고, 그러면 커스텀 필드가 각 포스트 객체의
acf속성에 나타납니다.
Supabase 테이블 스키마
CREATE TABLE posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
wp_id INTEGER UNIQUE,
title TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
content_html TEXT,
content_markdown TEXT,
excerpt TEXT,
published_at TIMESTAMPTZ,
status TEXT DEFAULT 'publish',
featured_image TEXT,
categories TEXT[],
tags TEXT[],
seo_title TEXT,
seo_description TEXT,
og_image TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
seo_title, seo_description, 및 og_image 컬럼을 포함했음을 유의합니다. 아래 SEO 마이그레이션 섹션에서 그것들이 필요할 것입니다.
미디어 마이그레이션: wp-content에서 Supabase Storage로
이것이 대부분의 가이드가 무시하는 부분이며, 가장 오래 걸리는 부분입니다. 12년 된 WordPress 사이트는 wp-content/uploads/에 10,000개 이상의 파일을 쉽게 가질 수 있습니다.
접근 방식:
- 전체
wp-content/uploads/디렉토리 다운로드 - Supabase Storage (또는 Cloudflare R2, 또는 S3)에 업로드
- 콘텐츠의 모든 미디어 URL 재작성
WordPress 미디어 파일을 최신 객체 스토리지로 마이그레이션하려면 어떻게 해야 합니까?
전체 wp-content/uploads/ 디렉토리를 다운로드하고, Supabase Storage 또는 Cloudflare R2에 업로드한 다음, 모든 미디어 URL을 콘텐츠에서 재작성합니다. 스크립트를 사용하여 WordPress 콘텐츠에서 각 이미지 URL을 가져오고, 객체 스토리지에 업로드하면서 디렉토리 구조 (2024/03/image.jpg)를 유지한 다음, 두 번째 패스를 실행하여 이전 URL을 새 스토리지 URL로 바꿉니다. 이전 이미지 URL에 직접 연결하는 외부 사이트에 대해 와일드카드 리디렉션을 설정합니다.
다운로드 및 업로드 스크립트
import { createClient } from '@supabase/supabase-js';
import fs from 'fs';
import path from 'path';
import fetch from 'node-fetch';
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_KEY
);
const BUCKET = 'media';
async function migrateMedia(posts) {
const urlRegex =
/https?:\/\/yoursite\.com\/wp-content\/uploads\/[^\s"')]+/g;
for (const post of posts) {
const urls = post.content_html.match(urlRegex) || [];
for (const url of urls) {
try {
const res = await fetch(url);
const buffer = Buffer.from(await res.arrayBuffer());
// 디렉토리 구조 유지: 2024/03/image.jpg
const storagePath = url.replace(
/https?:\/\/yoursite\.com\/wp-content\/uploads\//,
''
);
const { error } = await supabase.storage
.from(BUCKET)
.upload(storagePath, buffer, {
contentType: res.headers.get('content-type'),
upsert: true,
});
if (error) console.error(`Failed: ${storagePath}`, error);
else console.log(`Uploaded: ${storagePath}`);
} catch (e) {
console.error(`Skipped: ${url}`, e.message);
}
}
}
}
async function rewriteUrls() {
const { data: posts } = await supabase.from('posts').select('*');
const supabaseBase = `${process.env.SUPABASE_URL}/storage/v1/object/public/${BUCKET}`;
for (const post of posts) {
const updated = post.content_html.replace(
/https?:\/\/yoursite\.com\/wp-content\/uploads\//g,
`${supabaseBase}/`
);
await supabase
.from('posts')
.update({
content_html: updated,
content_markdown: turndown.turndown(updated),
})
.eq('id', post.id);
}
}
이미지 성능 이점
마이그레이션이 진정으로 가치를 발휘하는 부분입니다. WordPress는 원본 업로드를 제공합니다 -- 종종 누군가가 DSLR에서 업로드한 3000×2000px PNG입니다. ShortPixel 같은 플러그인을 사용해도, 여전히 PHP를 통해 이미지를 제공하고 있습니다.
Next.js <Image> 컴포넌트는 next/image를 사용하여 자동 형식 협상 (WebP/AVIF), 반응형 크기 조정, 그리고 레이지 로딩을 합니다. 우리의 마지막 마이그레이션의 숫자:
| 메트릭 | WordPress | Next.js + Image Component |
|---|---|---|
| 평균 페이지 이미지 무게 | 2.1 MB | 380 KB |
| 이미지 요청 | 페이지당 12개 | 6개 (레이지 로드됨) |
| 형식 | JPEG/PNG | WebP (지원되는 곳에서 AVIF) |
| 누적 레이아웃 시프트 | 0.18 | 0.02 |
오타가 아닙니다. 평균 이미지 페이로드가 2.1 MB에서 380 KB로 감소했습니다. 그리고 이것은 최적화된 파일을 다시 업로드하지 않았고, next/image가 그 일을 하게 했습니다.
import Image from 'next/image';
export function PostImage({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={800}
height={450}
sizes="(max-width: 768px) 100vw, 800px"
quality={80}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..." // 빌드 시간에 생성
/>
);
}
URL 구조: 모든 이전 URL 매핑
마이그레이션이 실패하는 부분입니다. 하나의 놓친 리디렉션은 수년 동안 Google에서 인덱싱한 페이지의 404를 의미합니다. 저는 URL 매핑을 데이터베이스 마이그레이션과 같은 진지함으로 다룹니다 -- 테스트하고, 검증하고, 다시 검증합니다.
WordPress 마이그레이션 중 URL 리디렉션을 올바르게 처리하는 방법은 무엇입니까?
WordPress에서 모든 게시된 URL을 내보내고, Google Search Console 인덱싱된 URL과 교차 참조하고, 모든 URL에 대해 301 리디렉션을 구현합니다. wp_posts 테이블에서 모든 게시된 URL을 쿼리하고, GSC의 인덱싱된 URL을 내보내고, 리디렉션 맵을 생성합니다. 50개 이하의 URL에 대해 next.config.js 리디렉션을 사용하고, 50-1,024 URL에 대해 JSON 파일을 사용하거나, Vercel의 1,024 리디렉션 제한을 초과하는 사이트의 경우 미들웨어를 사용합니다. 카테고리 페이지, 페이지 매김, 그리고 wp-content/uploads 경로에 대한 와일드카드 리디렉션을 포함합니다.
매핑 프로세스
먼저 WordPress에서 모든 URL을 내보냅니다. 데이터베이스에서 직접 가져옵니다:
SELECT
CONCAT('/', post_name, '/') AS old_url,
post_type,
post_status
FROM wp_posts
WHERE post_status = 'publish'
AND post_type IN ('post', 'page', 'product')
ORDER BY post_type, post_name;
그러면 Google Search Console의 인덱싱된 URL과 교차 참조합니다. GSC는 종종 데이터베이스에 더 이상 없는 URL을 표시합니다 -- 이전 카테고리 페이지, 페이지 매김 URL, 첨부 페이지. 모든 것에 대해 리디렉션이 필요합니다.
next.config.js 리디렉션
50개 이하의 리디렉션이 있는 사이트의 경우, 인라인으로 설정합니다:
// next.config.js
module.exports = {
async redirects() {
return [
{
source: '/2019/03/old-post-slug/',
destination: '/blog/old-post-slug',
permanent: true,
},
{
source: '/category/:slug',
destination: '/blog/category/:slug',
permanent: true,
},
{
source: '/product/:slug',
destination: '/shop/:slug',
permanent: true,
},
];
},
};
200개 이상의 리디렉션의 경우: JSON 파일 사용
인라인 리디렉션을 유지하는 것은 수백 개가 지나면 지옥입니다. 저는 JSON 파일을 사용합니다:
// redirects.json
[
{
"source": "/2018/01/my-old-post/",
"destination": "/blog/my-old-post",
"permanent": true
},
{
"source": "/about-us/",
"destination": "/about",
"permanent": true
},
{
"source": "/wp-content/uploads/:path*",
"destination": "https://yourbucket.supabase.co/storage/v1/object/public/media/:path*",
"permanent": true
}
]
// next.config.js
const redirectsList = require('./redirects.json');
module.exports = {
async redirects() {
return redirectsList;
},
};
wp-content/uploads에 대한 와일드카드 리디렉션은 중요합니다. 이전 이미지에 직접 연결하는 외부 사이트가 있을 것입니다. 그 백링크를 잃지 마세요.
중요: Vercel은 next.config.js에서 1,024개의 리디렉션으로 제한합니다. 그 이상의 사이트의 경우, 미들웨어를 사용합니다:
// middleware.ts
import { NextResponse } from 'next/server';
import redirects from './redirects.json';
const redirectMap = new Map(
redirects.map((r) => [r.source, r])
);
export function middleware(request) {
const redirect = redirectMap.get(request.nextUrl.pathname);
if (redirect) {
return NextResponse.redirect(
new URL(redirect.destination, request.url),
redirect.permanent ? 308 : 307
);
}
}

SEO 마이그레이션: Yoast 및 RankMath 데이터를 Next.js Metadata로
Yoast 또는 RankMath를 사용하고 있었다면, 커스텀 메타 제목, 설명, 그리고 Open Graph 데이터의 몇 년치가 wp_postmeta 테이블에 저장되어 있습니다. 잃지 마세요.
WordPress에서 Next.js로 마이그레이션할 때 Yoast SEO 데이터를 보존하려면 어떻게 합니까?
wp_postmeta에서 Yoast 메타 제목, 설명, 그리고 Open Graph 이미지를 내보내고, 새 데이터베이스에 저장한 다음, Next.js Metadata API를 사용하여 렌더링합니다. wp_postmeta에서 _yoast_wpseo_title, _yoast_wpseo_metadesc, 그리고 _yoast_wpseo_opengraph-image 필드를 쿼리합니다. 이 데이터를 posts 테이블의 전용 SEO 컬럼으로 가져옵니다. Next.js App Router에서 generateMetadata를 사용하여 이 데이터를 적절한 메타 태그 및 Open Graph 마크업으로 렌더링합니다.
SEO 데이터 내보내기
SELECT
p.post_name AS slug,
MAX(CASE WHEN pm.meta_key = '_yoast_wpseo_title' THEN pm.meta_value END) AS seo_title,
MAX(CASE WHEN pm.meta_key = '_yoast_wpseo_metadesc' THEN pm.meta_value END) AS seo_description,
MAX(CASE WHEN pm.meta_key = '_yoast_wpseo_opengraph-image' THEN pm.meta_value END) AS og_image
FROM wp_posts p
JOIN wp_postmeta pm ON p.ID = pm.post_id
WHERE p.post_status = 'publish'
GROUP BY p.ID, p.post_name;
RankMath의 경우, 메타 키를 바꿉니다: rank_math_title, rank_math_description, rank_math_facebook_image.
이 데이터를 우리가 이전에 정의한 SEO 컬럼의 Supabase posts 테이블로 가져옵니다.
Next.js Metadata API
App Router를 사용하면, 메타데이터는 일급 시민입니다:
// app/blog/[slug]/page.tsx
import { supabase } from '@/lib/supabase';
import { Metadata } from 'next';
export async function generateMetadata(
{ params }: { params: { slug: string } }
): Promise<Metadata> {
const { data: post } = await supabase
.from('posts')
.select('title, seo_title, seo_description, og_image')
.eq('slug', params.slug)
.single();
return {
title: post.seo_title || post.title,
description: post.seo_description,
openGraph: {
title: post.seo_title || post.title,
description: post.seo_description,
images: post.og_image ? [{ url: post.og_image }] : [],
},
};
}
JSON-LD Server Component로서의 스키마 마크업
WordPress 플러그인은 자동으로 스키마를 생성합니다. Next.js에서 직접 구축합니다 -- 이것은 실제로 더 많은 제어를 제공합니다:
// components/ArticleSchema.tsx
export function ArticleSchema({ post }) {
const schema = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
datePublished: post.published_at,
dateModified: post.updated_at || post.published_at,
author: {
'@type': 'Organization',
name: 'Your Company',
},
image: post.og_image,
description: post.seo_description,
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
동적 사이트맵
// app/sitemap.ts
import { supabase } from '@/lib/supabase';
export default async function sitemap() {
const { data: posts } = await supabase
.from('posts')
.select('slug, published_at')
.eq('status', 'publish');
return posts.map((post) => ({
url: `https://yoursite.com/blog/${post.slug}`,
lastModified: post.published_at,
changeFrequency: 'monthly',
priority: 0.8,
}));
}
이것은 정적 사이트의 경우 빌드 시간에 또는 동적 사이트의 경우 온디맨드로 생성됩니다. 플러그인 없음, XML 템플릿 파일 없음, 캐싱 문제 없음.
폼: Gravity Forms를 Server Actions로
Gravity Forms는 결코 만들어진 최고의 WordPress 플러그인입니다. 또한 Elite 라이선스에 $259/년이고, 각 폼은 200KB 이상의 JavaScript를 로드합니다.
대체물입니다. 폼당 약 20줄의 코드입니다.
기존 항목 내보내기
먼저, WordPress 관리자로부터 Gravity Forms 항목을 CSV로 내보냅니다. 필요한 경우 역사적 기록을 위해 Supabase에 저장합니다.
Server Action 연락처 폼
// app/contact/page.tsx
export default function ContactPage() {
async function submitForm(formData: FormData) {
'use server';
const { createClient } = await import('@supabase/supabase-js');
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_KEY!
);
const entry = {
name: formData.get('name') as string,
email: formData.get('email') as string,
message: formData.get('message') as string,
submitted_at: new Date().toISOString(),
};
// 검증
if (!entry.name || !entry.email || !entry.message) {
throw new Error('All fields required');
}
await supabase.from('form_submissions').insert(entry);
// 선택사항: Resend를 통해 알림 이메일 보내기
await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'forms@yoursite.com',
to: 'team@yoursite.com',
subject: `New contact: ${entry.name}`,
html: `<p>${entry.message}</p><p>From: ${entry.email}</p>`,
}),
});
}
return (
<form action={submitForm}>
<input name="name" type="text" required placeholder="Name" />
<input name="email" type="email" required placeholder="Email" />
<textarea name="message" required placeholder="Message" />
<button type="submit">Send</button>
</form>
);
}
플러그인 없음. 폼 자체를 위한 JavaScript 페이로드 없음 (그것은 서버 액션이 있는 네이티브 HTML 폼입니다). 점진적 향상 -- JavaScript를 활성화하지 않고도 작동합니다.
더 복잡한 폼 (다단계, 파일 업로드, 조건부 필드)의 경우, 우리는 클라이언트 쪽에서 React Hook Form을 같은 서버 액션 패턴과 함께 사용합니다. 핵심 통찰력: 데이터베이스와 API가 있을 때 폼 플러그인이 필요하지 않습니다.
WooCommerce에서 Stripe로
이것이 WordPress 마이그레이션의 가장 어려운 부분입니다. WooCommerce는 단순한 플러그인이 아닙니다 -- 상품, 변형, 재고, 주문, 구독, 쿠폰, 세금, 그리고 배송 규칙이 있는 상거래 플랫폼입니다. 기능을 마이그레이션하는 것이 아닙니다. 플랫폼을 대체하는 것입니다.
WooCommerce 상품을 Stripe로 마이그레이션하려면 어떻게 합니까?
WooCommerce REST API 또는 CSV를 통해 상품을 내보낸 다음, Stripe Products API를 사용하여 가격으로 일치하는 상품을 Stripe에서 생성합니다. 500개 이하의 상품이 있는 사이트의 경우, 해당 API를 사용하여 Stripe에 직접 푸시합니다: 이름, 설명, 그리고 이미지가 있는 상품을 생성한 다음, 그 상품에 연결된 가격 객체를 생성합니다. 참조를 위해 WooCommerce 상품 ID를 Stripe 메타데이터에 저장합니다. 결제 처리를 위해 Stripe Checkout Sessions를 사용하고 데이터베이스에서 주문을 추적하기 위해 웹훅을 사용합니다.
상품 마이그레이션
WooCommerce에서 CSV 또는 REST API를 통해 상품을 내보냅니다. 두 가지 대상 선택지가 있습니다:
| 접근 방식 | 최적 대상 | 트레이드오프 |
|---|---|---|
| Supabase 상품 테이블 | 커스텀 상점, 복잡한 필터링 | 인벤토리 로직을 직접 관리 |
| Stripe Products API | 간단한 카탈로그, 구독 비즈니스 | Stripe가 가격 관리, 디스플레이는 사용자가 관리 |
500개 이하의 상품이 있는 대부분의 사이트의 경우, 저는 Stripe Products에 직접 푸시합니다:
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
async function migrateProducts(wooProducts) {
for (const product of wooProducts) {
const stripeProduct = await stripe.products.create({
name: product.name,
description: product.short_description,
images: [product.images[0]?.src].filter(Boolean),
metadata: {
woo_id: String(product.id),
slug: product.slug,
sku: product.sku,
},
});
await stripe.prices.create({
product: stripeProduct.id,
unit_amount: Math.round(parseFloat(product.price) * 100),
currency: 'usd',
});
console.log(`Created: ${product.name} → ${stripeProduct.id}`);
}
}
Stripe Checkout Sessions로 결제
// app/api/checkout/route.ts
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const { priceId, quantity = 1 } = await request.json();
const session = await stripe.checkout.sessions.create({
mode: 'payment',
payment_method_types: ['card'],
line_items: [{ price: priceId, quantity }],
success_url: `${process.env.NEXT_PUBLIC_URL}/order/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/shop`,
});
return NextResponse.json({ url: session.url });
}
구독
WooCommerce Subscriptions ($239/년)를 사용 중이라면, Stripe Billing로 전환합니다. mode: 'payment'를 mode: 'subscription'으로 변경하고 가격이 recurring 세트를 가지고 있는지 확인합니다. 그것이 전부입니다. Stripe는 평가판 기간, 배분, 그리고 해약을 처리합니다.
웹훅을 통한 주문 추적
// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers';
import Stripe from 'stripe';
import { supabase } from '@/lib/supabase';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const body = await request.text();
const sig = headers().get('stripe-signature')!;
const event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
if (event.type === 'checkout.session.completed') {
const session = event.data.object as Stripe.Checkout.Session;
await supabase.from('orders').insert({
stripe_session_id: session.id,
customer_email: session.customer_details?.email,
amount_total: session.amount_total,
status: 'completed',
});
}
return new Response('OK', { status: 200 });
}
Stripe의 거래 수수료는 2.9% + $0.30입니다. 이것을 호스팅 ($30-100/월 관리형 WP), Subscriptions 플러그인 ($239/년), 결제 게이트웨이 플러그인, 그리고 아마도 몇 가지 다른 추가 기능과 비교합니다. 수학이 빠르게 계산됩니다.
복잡한 상거래 마이그레이션의 경우, 우리는 이를 Next.js 개발 서비스의 일부로 제공합니다 -- 가장 일반적인 요청 중 하나입니다.
마이그레이션 후 모니터링
시작은 끝이 아닙니다. 마이그레이션 후 처음 2주가 중요합니다.
Google Search Console
- 새 사이트맵을 즉시 제출합니다
- URL Inspection 도구를 사용하여 상위 20개 페이지의 인덱싱을 요청합니다
- 첫 주 동안 매일 Coverage 보고서를 모니터링합니다 -- 404에서 급증을 봅니다
- "페이지 인덱싱" 보고서에서 "발견됨 -- 현재 인덱싱되지 않음"에 갇혀 있는 페이지를 확인합니다
분석 비교
주간 비교를 하는 대시보드를 설정합니다:
- 총 세션
- 특히 유기 검색 트래픽
- 페이지별 바운스율
- 전환율 (양식 제출, 구매)
1주차의 작은 트래픽 감소는 정상입니다. 3주까지 회복되지 않았다면 리디렉션이나 인덱싱에 뭔가 잘못되었습니다.
Lighthouse 감사
모든 주요 템플릿 (홈페이지, 블로그 포스트, 상품 페이지, 연락처 페이지)에서 Lighthouse를 실행합니다. 목표:
- 성능: 90+
- 접근성: 95+
- 모범 사례: 95+
- SEO: 100
마지막 마이그레이션에서 -- 400페이지 콘텐츠 사이트 -- WordPress의 평균 Lighthouse 성능 점수 38에서 Vercel에 배포된 Next.js의 96으로 갔습니다. 이것은 선택되지 않았습니다. 이것이 평균입니다.
WordPress에 머물러야 할 때
여기서 제 일부를 잃을 것입니다. 하지만 이것이 이 가이드에서 가장 중요한 섹션입니다.
마이그레이션하지 마세요:
- 20페이지 이하의 단순한 블로그 또는 브로셔 사이트가 있는 경우
- 팀이 비기술적이고 일일 업데이트를 위해 WordPress 관리자에 의존하는 경우
- Lighthouse 점수가 이미 70+ 이고 성능이 중요한 비즈니스 요구가 없는 경우
- 보안 문제가 없고 호스팅이 안정적인 경우
- 총 플러그인 비용이 연간 $200 미만인 경우
- 개발자가 없거나 Next.js 사이트를 유지 보수하기 위한 예산이 없는 경우
좋은 호스트 (Cloudways, Kinsta), 견고한 테마, 그리고 최소한의 플러그인을 사용하는 WordPress는 괜찮습니다. 실제로, 그것은 전투 테스트를 거쳤고, 잘 문서화되었으며, 수백만 명의 개발자가 이해합니다.
마이그레이션이 다음과 같은 경우 의미가 있습니다:
- 성능이 수익과 직접 연결됨 (전자 상거래, SaaS 마케팅 사이트)
- 관리형 호스팅과 보안 플러그인에 월 $500 이상 지출
- 개발 팀이 이미 React를 작성하고 있음
- 배포 파이프라인이 필요함 (미리보기 빌드, 스테이징 환경, 롤백)
- 보안 공격 표면이 진정한 우려사항 (정부, 의료, 금융)
신뢰가 판매보다 더 중요하기 때문에 이를 말합니다. 마이그레이션이 가치가 있는지 확실하지 않다면, 저희에게 연락하고 정직한 평가를 제공해 드립니다.
성능 벤치마크: 이전과 이후
우리의 2024-2025년의 마지막 5개 마이그레이션에서:
| 메트릭 | WordPress (평균) | Next.js (평균) | 변화 |
|---|---|---|---|
| TTFB | 1,200ms | 85ms | 14배 빠름 |
| LCP | 3.8s | 0.9s | 4.2배 빠름 |
| 총 페이지 무게 | 3.2 MB | 620 KB | 5배 가벼움 |
| 페이지당 요청 | 47 | 11 | 77% 감소 |
| Lighthouse 성능 | 42 | 94 | +52점 |
| 월간 호스팅 비용 | $75 | $20 (Vercel Pro) | 73% 절감 |
| Core Web Vitals 통과율 | 페이지의 31% | 페이지의 100% | ✓ |
이들은 프로덕션 사이트의 실제 숫자입니다. WordPress 사이트는 관리형 호스팅 (WP Engine 및 Kinsta), 최적화된 캐싱, 그리고 이미지 최적화 플러그인을 실행하고 있었습니다. 그들은 무시되지 않았습니다 -- 그들은 WordPress가 제공할 수 있는 것의 한계에 도달한 유지 보수 사이트였습니다.
최신 프레임워크로 무엇이 가능한지에 관심이 있다면, 우리의 Astro 개발 기능도 확인해 보세요 -- 최소한의 상호작용으로 콘텐츠가 많은 사이트의 경우, Astro는 Next.js보다 더 작은 페이로드를 제공할 수 있습니다.
자주 묻는 질문
WordPress에서 Next.js로의 마이그레이션은 얼마나 오래 걸립니까?
100-500페이지가 있는 일반적인 사이트의 경우 4-8주의 개발 시간을 예상합니다. 단순한 브로셔 사이트는 2-3주 만에 완료할 수 있습니다. 수천 개의 상품이 있는 복잡한 WooCommerce 스토어는 10-12주가 걸릴 수 있습니다. 콘텐츠 마이그레이션 자체는 빠릅니다 -- 프론트엔드 템플릿을 재구축하고 모든 리디렉션을 테스트하는 데 시간이 걸립니다.
WordPress에서 Next.js로 마이그레이션할 때 SEO 순위를 잃을까요?
올바르게 리디렉션과 메타데이터를 처리하면 아닙니다. 중요한 부분은: 모든 이전 URL에 대한 301 리디렉션, 모든 Yoast/RankMath 메타 제목 및 설명 마이그레이션, 사이트맵 구조 보존, 새 사이트맵을 Google Search Console에 즉시 제출입니다. 우리는 1-2주 내 마이그레이션 전 트래픽으로의 회복을 목격했으며, 향상된 Core Web Vitals로 인해 3개월까지 상당한 유기 성장을 경험했습니다.
WordPress를 Next.js의 헤드리스 CMS로 사용할 수 있습니까?
예, 인기 있는 접근 방식입니다. WP REST API 또는 WPGraphQL을 사용하여 콘텐츠 백엔드로 WordPress를 유지하고, Next.js를 프론트엔드로 사용합니다. 이것은 친숙한 편집 경험을 보존하면서 Next.js 성능을 얻습니다. 단점은 여전히 WordPress 설치를 보안 및 업데이트 오버헤드와 함께 유지보수해야 한다는 것입니다. 우리는 팀이 WordPress 워크플로우에 깊이 투자하지 않는 한 새로운 프로젝트에 대해 일반적으로 Payload CMS 또는 Sanity를 권장합니다.
WordPress에서 Next.js로 마이그레이션하는 데 비용이 얼마나 듭니까?
DIY 개발자 시간: 도구는 무료이지만 80-200시간의 개발 시간을 예산으로 잡습니다. 에이전시 비용: 사이트 복잡성, 페이지 수, 전자 상거래 기능, 그리고 커스텀 기능에 따라 일반적으로 $10,000-$50,000입니다. 세부사항은 가격 페이지를 확인합니다. ROI는 일반적으로 줄어든 호스팅 비용 (월 $50-100 절감), 제거된 플러그인 라이선스 수수료, 그리고 더 나은 성능으로부터의 증가된 전환율에서 나옵니다.
WordPress 플러그인을 마이그레이션한 후 어떻게 됩니까?
각 플러그인은 Next.js 동등물이 필요합니다. Contact Form 7 또는 Gravity Forms는 Server Action이 됩니다. Yoast SEO는 Next.js Metadata API가 됩니다. WooCommerce는 Stripe가 됩니다. Google Analytics는 같게 유지됩니다 (추적 스니펫을 이동하기만 하면 됨). Wordfence 같은 일부 플러그인은 공격할 WordPress가 없으므로 불필요하게 됩니다. 마이그레이션을 시작하기 전에 전체 플러그인 인벤토리를 작성합니다 -- 명확한 대체 전략이 없는 모든 플러그인은 위험입니다.
WordPress에서 Next.js로 또는 Astro로 마이그레이션해야 합니까?
그것은 상호작용 요구에 달려 있습니다. Next.js는 동적 기능이 있는 사이트에 더 좋습니다 -- 사용자 인증, 전자 상거래, 대시보드, 실시간 데이터. Astro는 콘텐츠가 많은 사이트에 더 좋습니다 -- 블로그, 문서, 마케팅 사이트. Astro는 기본적으로 0 JavaScript를 제공합니다. 즉, 훨씬 더 작은 페이지 크기입니다. 우리는 둘 다로 일합니다 -- 세부사항은 Astro 개발 및 Next.js 개발 페이지를 참조합니다.
WooCommerce 구독을 Stripe로 마이그레이션할 수 있습니까?
네, 하지만 활성 가입자를 신중하게 처리해야 합니다. Stripe에서 고객과 구독을 생성한 다음 청구 변경을 고객에게 전달해야 합니다. Stripe Billing은 평가판 기간, 배분, 실패한 결제 재시도 로직, 그리고 해약 흐름을 처리합니다. 마이그레이션 자체는 일회성 스크립트이지만, 실제 구독 시나리오에 대해 테스트하는 것이 시간이 걸리는 곳입니다. 100개 이상의 활성 가입자가 있는 경우 추가 시간을 예산으로 잡습니다.
WordPress에서 마이그레이션 후 Next.js를 위한 최적의 호스팅은 무엇입니까?
Vercel은 기본 선택입니다 -- Next.js를 만드는 팀으로 빌드되었으며, 무료 계층은 대부분의 마케팅 사이트를 처리합니다. Vercel Pro는 팀의 경우 월 $20입니다. 대안으로는 Netlify, Cloudflare Pages (엣지 성능에 탁월함), 그리고 전체 제어를 원하는 경우 VPS에서 Docker를 자체 호스팅하는 것이 포함됩니다. 이 모든 것이 동등한 트래픽 수준의 관리형 WordPress 호스팅보다 상당히 저렴합니다.