Windsurf 사례 연구: 솔로 개발자가 레거시 PHP 이커머스를 Next.js로 2주 만에 마이그레이션한 방법
개요: 3개월 예상 작업을 2주로 단축한 AI 기반 마이그레이션
10년 된 레거시 PHP 이커머스 백엔드를 최신 Next.js 풀스택 앱으로 전환하는 것은 일반적으로 3개월 이상의 수동 작업이 필요합니다. 이 사례 연구에서는 솔로 개발자 김현우 씨가 Windsurf의 AI 기반 멀티파일 편집, 코드베이스 인식 리팩토링, 자동 테스트 생성 기능을 활용하여 단 2주 만에 전체 마이그레이션을 완료한 실제 워크플로우를 공개합니다.
프로젝트 배경
| 항목 | 기존 시스템 (Before) | 마이그레이션 후 (After) |
|---|---|---|
| 백엔드 | PHP 5.6 + MySQL | Next.js 14 App Router + Prisma |
| 프론트엔드 | jQuery + 서버 사이드 렌더링 | React 18 + Tailwind CSS |
| API | 커스텀 REST (비표준) | Next.js API Routes + tRPC |
| 테스트 | 없음 | Jest + Playwright (자동 생성) |
| 배포 | FTP 수동 업로드 | Vercel CI/CD |
Windsurf 설치
Windsurf는 VS Code 기반의 AI 네이티브 IDE로, 공식 사이트에서 다운로드하여 설치합니다.
# macOS (Homebrew)
brew install —cask windsurf
또는 공식 사이트에서 직접 다운로드
https://codeium.com/windsurf
프로젝트 초기화
# Next.js 프로젝트 생성
npx create-next-app@latest ecommerce-next --typescript --tailwind --app --src-dir
cd ecommerce-next
# 필수 패키지 설치
npm install prisma @prisma/client @trpc/server @trpc/client @trpc/next
npm install -D jest @testing-library/react playwright
Windsurf 프로젝트 설정 (.windsurfrules)
프로젝트 루트에 .windsurfrules 파일을 생성하여 AI에게 마이그레이션 컨텍스트를 제공합니다.
# .windsurfrules
Project: Legacy PHP e-commerce migration to Next.js 14
Database: MySQL → PostgreSQL via Prisma ORM
API Pattern: tRPC for type-safe API routes
Auth: NextAuth.js with credentials provider
Styling: Tailwind CSS (replacing Bootstrap 3)
Testing: Jest for unit, Playwright for E2E
Legacy source: ./legacy-php/ directory for reference
## 2단계: Cascade를 활용한 멀티파일 마이그레이션
Windsurf의 핵심 기능인 **Cascade**는 코드베이스 전체를 인식하는 AI 에이전트입니다. 단일 프롬프트로 여러 파일을 동시에 생성하고 수정할 수 있습니다.
PHP 모델을 Prisma 스키마로 변환
Cascade에 다음과 같이 지시합니다.
# Windsurf Cascade 프롬프트
레거시 PHP 디렉토리의 models/ 폴더에 있는 Product.php, Order.php,
User.php, Category.php 모델을 분석하고 Prisma 스키마로 변환해줘.
관계형 매핑을 유지하고 MySQL 특수 타입을 PostgreSQL 호환으로 변경해.
Cascade가 자동 생성한 Prisma 스키마 예시:
// prisma/schema.prisma
generator client {
provider = “prisma-client-js”
}
datasource db {
provider = “postgresql”
url = env(“DATABASE_URL”)
}
model Product {
id Int @id @default(autoincrement())
name String @db.VarChar(255)
slug String @unique
price Decimal @db.Decimal(10, 2)
stock Int @default(0)
categoryId Int
category Category @relation(fields: [categoryId], references: [id])
orderItems OrderItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Order {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id])
status OrderStatus @default(PENDING)
total Decimal @db.Decimal(12, 2)
items OrderItem[]
createdAt DateTime @default(now())
}
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}
API 라우트 자동 생성
# Cascade 프롬프트
legacy-php/api/ 디렉토리의 모든 PHP API 엔드포인트를 분석해서
tRPC 라우터로 변환해줘. 입력 검증은 Zod로 처리하고
에러 핸들링 패턴도 통일해.생성된 tRPC 라우터:
// src/server/routers/product.ts
import { z } from ‘zod’;
import { router, publicProcedure, protectedProcedure } from ’../trpc’;
export const productRouter = router({
list: publicProcedure
.input(z.object({
categoryId: z.number().optional(),
page: z.number().default(1),
limit: z.number().default(20),
}))
.query(async ({ ctx, input }) => {
const { categoryId, page, limit } = input;
return ctx.prisma.product.findMany({
where: categoryId ? { categoryId } : undefined,
include: { category: true },
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: ‘desc’ },
});
}),
create: protectedProcedure
.input(z.object({
name: z.string().min(1).max(255),
price: z.number().positive(),
stock: z.number().int().nonneg(),
categoryId: z.number(),
}))
.mutation(async ({ ctx, input }) => {
const slug = input.name.toLowerCase().replace(/\s+/g, ’-’);
return ctx.prisma.product.create({
data: { …input, slug },
});
}),
});
3단계: 자동 테스트 생성
Cascade의 코드베이스 인식 기능은 변환된 코드에 대한 테스트를 자동으로 생성합니다.
# Cascade 프롬프트
productRouter의 모든 프로시저에 대해 Jest 유닛 테스트를 생성해줘.
엣지 케이스와 에러 시나리오도 포함해.
// __tests__/server/product.test.ts import { createInnerTRPCContext } from '@/server/trpc'; import { productRouter } from '@/server/routers/product';describe(‘productRouter’, () => { const ctx = createInnerTRPCContext({ session: null }); const caller = productRouter.createCaller(ctx);
describe(‘list’, () => { it(‘기본 페이지네이션으로 상품 목록을 반환한다’, async () => { const result = await caller.list({ page: 1, limit: 10 }); expect(Array.isArray(result)).toBe(true); expect(result.length).toBeLessThanOrEqual(10); });
it('카테고리 필터링이 정상 동작한다', async () => { const result = await caller.list({ categoryId: 1 }); result.forEach(product => { expect(product.categoryId).toBe(1); }); });
}); });
# 테스트 실행
npx jest —coverage
npx playwright test
4단계: 환경변수 및 배포 설정
# .env.local
DATABASE_URL="postgresql://user:password@localhost:5432/ecommerce"
NEXTAUTH_SECRET="YOUR_SECRET_KEY"
NEXTAUTH_URL="http://localhost:3000"
STRIPE_SECRET_KEY="YOUR_API_KEY"
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="YOUR_API_KEY"# Prisma 마이그레이션 실행npx prisma migrate dev —name init npx prisma db seed
개발 서버 시작
npm run dev
마이그레이션 결과 요약
| 지표 | 수동 마이그레이션 (예상) | Windsurf 활용 (실제) |
|---|---|---|
| 총 소요 기간 | 약 12주 | 2주 |
| 변환된 파일 수 | - | 187개 파일 |
| 자동 생성 테스트 | 0개 (미작성 예상) | 94개 테스트 케이스 |
| 테스트 커버리지 | 0% | 78% |
| 프로덕션 버그 | - | 배포 후 첫 주 2건 |
@legacy-php/api/products.php처럼 직접 파일을 참조하면 AI가 해당 파일의 비즈니스 로직을 정확히 파악합니다.- **.windsurfrules 세분화:** 프로젝트 컨벤션, 네이밍 규칙, 금지 패턴을 상세히 기록하면 AI가 일관된 코드를 생성합니다.- **Supercomplete 활용:** 탭 자동완성 시 다음 동작을 예측하는 Supercomplete 기능으로 반복 작업을 대폭 줄일 수 있습니다.- **부분 마이그레이션 전략:** 전체를 한 번에 변환하지 말고 모델 → API → 프론트엔드 → 테스트 순서로 레이어별 마이그레이션하면 오류 추적이 쉬워집니다.
## Troubleshooting: 자주 발생하는 문제 해결
Cascade가 대규모 파일을 누락하는 경우
레거시 코드베이스가 너무 큰 경우 Cascade가 일부 파일을 인식하지 못할 수 있습니다.
# 해결: 디렉토리 단위로 분리하여 참조
# 전체 폴더 대신 개별 파일을 @로 직접 지정
Cascade: @legacy-php/models/Product.php @legacy-php/models/Order.php
이 두 파일의 관계를 분석하고 Prisma 스키마로 변환해줘
### Prisma 마이그레이션 충돌
# 마이그레이션 리셋 (개발 환경에서만 사용) npx prisma migrate reset마이그레이션 상태 확인
npx prisma migrate status
tRPC 타입 불일치 에러
PHP에서 변환 시 타입이 느슨하게 설정되는 경우가 있습니다. Zod 스키마를 명시적으로 검토하고 z.coerce.number() 등을 활용하여 PHP의 암시적 타입 변환을 처리합니다.
// PHP에서 문자열로 전달되던 숫자 필드 처리
price: z.coerce.number().positive(), // "19.99" → 19.99
stock: z.coerce.number().int().nonneg(),
### Cascade 응답이 중간에 끊기는 경우
복잡한 변환 요청 시 응답이 잘리는 경우, 프롬프트를 더 작은 단위로 분리하여 요청합니다. "계속해줘" 또는 "나머지 부분도 생성해줘"로 이어갈 수 있습니다. ## 자주 묻는 질문 (FAQ)
Q1: Windsurf 무료 버전으로도 이 수준의 마이그레이션이 가능한가요?
Windsurf 무료 티어에서도 Cascade의 기본 기능을 사용할 수 있지만, 대규모 코드베이스 마이그레이션에는 Pro 플랜이 권장됩니다. Pro 플랜은 무제한 Cascade 플로우와 더 긴 컨텍스트 윈도우를 제공하여 레거시 코드 전체를 한 번에 분석할 수 있습니다. 무료 버전에서는 일일 사용량 제한이 있어 2주 내 완료가 어려울 수 있습니다.
Q2: PHP 외에 다른 레거시 언어(Java, Ruby 등)에서 Next.js로의 마이그레이션에도 동일한 방법을 적용할 수 있나요?
네, Windsurf의 Cascade는 언어에 구애받지 않고 코드 분석이 가능합니다. Java Spring Boot, Ruby on Rails, Python Django 등 다양한 레거시 백엔드에서 Next.js로의 마이그레이션에 동일한 워크플로우를 적용할 수 있습니다. 핵심은 .windsurfrules 파일에 소스 언어와 타겟 스택을 명확히 정의하는 것입니다.
Q3: 자동 생성된 테스트의 품질을 어떻게 신뢰할 수 있나요?
Cascade가 생성하는 테스트는 초기 안전망 역할을 합니다. 자동 생성된 테스트를 기반으로 비즈니스 크리티컬 로직(결제, 재고 관리 등)에 대해서는 수동으로 엣지 케이스를 추가하는 것이 좋습니다. 이 사례에서도 94개 자동 생성 테스트 중 약 15개를 수동으로 보강하여 최종 프로덕션 안정성을 확보했습니다.