Windsurf 사례 연구: 솔로 개발자가 레거시 PHP 이커머스를 Next.js로 10일 만에 마이그레이션한 방법
개요: 3개월 프로젝트를 10일로 압축한 AI 기반 마이그레이션
레거시 PHP 이커머스 백엔드를 현대적인 Next.js 풀스택으로 전환하는 작업은 일반적으로 3개월 이상의 수동 리라이트가 필요합니다. 이 사례 연구에서는 솔로 개발자 K씨가 Windsurf의 AI 기반 멀티파일 편집, Cascade 에이전트 워크플로우, 자동 테스트 생성 기능을 활용하여 이 작업을 단 10일 만에 완료한 과정을 상세히 다룹니다.
프로젝트 배경
| 항목 | 기존 시스템 (Before) | 새 시스템 (After) |
|---|---|---|
| 프레임워크 | PHP 5.6 + CodeIgniter 2 | Next.js 14 App Router |
| 데이터베이스 | MySQL 5.5 (Raw SQL) | PostgreSQL + Prisma ORM |
| API 구조 | 서버 렌더링 MVC | REST API + Server Actions |
| 테스트 커버리지 | 0% | 87% (자동 생성) |
| 파일 수 | 342개 PHP 파일 | 128개 TypeScript 파일 |
| 예상 소요 기간 | 3개월 (수동) | 10일 (Windsurf 활용) |
Windsurf IDE 설치
Windsurf는 VS Code 기반의 AI 네이티브 IDE로, 공식 사이트에서 직접 다운로드하여 설치합니다.
# Windsurf 설치 후 프로젝트 초기화
mkdir ecommerce-nextjs && cd ecommerce-nextjs
npx create-next-app@14 . —typescript —tailwind —app —src-dir
Prisma ORM 설정
npm install prisma @prisma/client
npx prisma init —datasource-provider postgresql
Windsurf 프로젝트 설정 파일
프로젝트 루트에 .windsurfrules 파일을 생성하여 AI 컨텍스트를 정의합니다.
# .windsurfrules
Project: Legacy PHP e-commerce migration to Next.js 14
Stack: Next.js 14, TypeScript, Prisma, PostgreSQL, Tailwind CSS
Conventions:
- Use App Router with Server Components by default
- Use Server Actions for mutations
- Follow REST API patterns in /api routes
- Korean locale support required (ko-KR)
Legacy Reference: ./legacy-php/ directory contains original PHP source
## 2단계: Cascade 에이전트를 활용한 스키마 마이그레이션
Windsurf의 Cascade는 멀티파일 컨텍스트를 이해하는 AI 에이전트입니다. 레거시 PHP 모델 파일을 분석하여 Prisma 스키마를 자동 생성했습니다.
Cascade 프롬프트 예시
// Windsurf Cascade (Cmd+L 또는 Ctrl+L)에 입력:
@legacy-php/models/ 디렉토리의 모든 PHP 모델 파일을 분석하여
Prisma schema로 변환해줘. 기존 MySQL 테이블 구조를 PostgreSQL에
맞게 최적화하고, 관계(relation)도 정확히 매핑해줘.
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
PAID
SHIPPED
DELIVERED
CANCELLED
}
# 스키마 적용
npx prisma db push
npx prisma generate
3단계: 멀티파일 편집으로 API 라우트 일괄 변환
Windsurf의 핵심 강점인 멀티파일 편집 기능을 사용하여 342개의 PHP 컨트롤러를 Next.js API Route로 변환했습니다. Cascade에 다음과 같은 워크플로우 프롬프트를 사용합니다.
// Cascade 멀티파일 편집 프롬프트:
@legacy-php/controllers/ProductController.php 와
@legacy-php/controllers/OrderController.php 를 참조하여
Next.js App Router API route로 변환해줘.
요구사항:
- src/app/api/products/route.ts (GET, POST)
- src/app/api/products/[id]/route.ts (GET, PUT, DELETE)
- src/app/api/orders/route.ts (GET, POST)
- Prisma Client 사용
- Zod로 입력 유효성 검사 추가
에러 핸들링 포함Cascade가 생성한 API 라우트 예시:
// src/app/api/products/route.ts import { prisma } from ’@/lib/prisma’ import { NextRequest, NextResponse } from ‘next/server’ import { z } from ‘zod’
const createProductSchema = z.object({
name: z.string().min(1).max(255),
price: z.number().positive(),
stock: z.number().int().min(0),
categoryId: z.number().int().positive(),
})
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const page = Number(searchParams.get(‘page’) ?? ‘1’)
const limit = Number(searchParams.get(‘limit’) ?? ‘20’)
const [products, total] = await Promise.all([
prisma.product.findMany({
skip: (page - 1) * limit,
take: limit,
include: { category: true },
orderBy: { createdAt: ‘desc’ },
}),
prisma.product.count(),
])
return NextResponse.json({ products, total, page, limit })
}
export async function POST(request: NextRequest) {
const body = await request.json()
const parsed = createProductSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json(
{ error: parsed.error.flatten() },
{ status: 400 }
)
}
const product = await prisma.product.create({
data: {
…parsed.data,
slug: parsed.data.name.toLowerCase().replace(/\s+/g, ’-’),
},
})
return NextResponse.json(product, { status: 201 })
}
4단계: 자동 테스트 생성
Cascade의 테스트 생성 기능을 활용하여 전체 API에 대한 테스트를 자동으로 작성했습니다.
# 테스트 프레임워크 설치
npm install -D vitest @testing-library/react @testing-library/jest-dom
// Cascade 프롬프트:
@src/app/api/ 디렉토리의 모든 라우트에 대해
Vitest 기반 통합 테스트를 생성해줘.
Prisma를 사용한 실제 DB 테스트로 작성하고
각 HTTP 메서드별 성공/실패 케이스를 포함해줘.자동 생성된 테스트 예시:
// src/app/api/products/__tests__/route.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { GET, POST } from '../route'
import { prisma } from '@/lib/prisma'
import { NextRequest } from 'next/server'
describe(‘Products API’, () => {
beforeEach(async () => {
await prisma.product.deleteMany()
})
it(‘GET /api/products - 빈 목록 반환’, async () => {
const req = new NextRequest(‘http://localhost/api/products’)
const res = await GET(req)
const data = await res.json()
expect(res.status).toBe(200)
expect(data.products).toHaveLength(0)
})
it(‘POST /api/products - 유효하지 않은 데이터 거부’, async () => {
const req = new NextRequest(‘http://localhost/api/products’, {
method: ‘POST’,
body: JSON.stringify({ name: ” }),
})
const res = await POST(req)
expect(res.status).toBe(400)
})
})
# 테스트 실행
npx vitest run —coverage
10일 마이그레이션 타임라인
| 일차 | 작업 내용 | Windsurf 기능 활용 |
|---|---|---|
| 1-2일 | 프로젝트 설정, DB 스키마 마이그레이션 | Cascade 코드 분석 + 변환 |
| 3-5일 | API 라우트 변환 (48개 엔드포인트) | 멀티파일 편집 |
| 6-7일 | 프론트엔드 컴포넌트 구현 | Cascade 컨텍스트 참조 |
| 8-9일 | 테스트 작성 및 버그 수정 | 자동 테스트 생성 |
| 10일 | 배포 및 최종 검증 | Cascade 디버깅 지원 |
.windsurfrules 파일에 프로젝트 컨벤션을 상세히 기술하면 Cascade의 코드 품질이 크게 향상됩니다.- **단계별 프롬프트:** 한 번에 전체 변환을 요청하기보다 모듈별로 나누어 프롬프트하면 정확도가 높아집니다.- **레거시 코드 참조:** @파일경로 문법으로 레거시 코드를 직접 참조하면 Cascade가 비즈니스 로직을 더 정확히 이해합니다.- **Cascade Flow 활용:** 반복적인 변환 패턴은 Cascade Flow로 저장하여 재사용하세요.- **인라인 편집:** Cmd+I(macOS) / Ctrl+I(Windows)로 특정 코드 블록만 빠르게 수정할 수 있습니다.
## Troubleshooting: 자주 발생하는 문제 해결
Cascade가 레거시 파일을 인식하지 못할 때
# .windsurfrules에 레거시 디렉토리를 명시적으로 추가
Include Legacy Source: ./legacy-php/
File Types: .php, .sql, .ini
멀티파일 편집 시 컨텍스트 초과 오류
파일 수가 많으면 컨텍스트 윈도우를 초과할 수 있습니다. 모듈 단위로 분할하여 처리하세요.
# 모듈별로 나누어 변환
# 1차: 상품 관련 모듈만
@legacy-php/controllers/Product*.php
@legacy-php/models/Product*.php
2차: 주문 관련 모듈만
@legacy-php/controllers/Order*.php
@legacy-php/models/Order*.php
자동 생성 테스트의 DB 연결 실패
# .env.test 파일 생성
DATABASE_URL="postgresql://user:password@localhost:5432/ecommerce_test"
# 테스트 DB 초기화
npx prisma db push --force-reset
Prisma 스키마 마이그레이션 충돌
# 마이그레이션 리셋 후 재생성
npx prisma migrate reset
npx prisma migrate dev --name init
자주 묻는 질문 (FAQ)
Q1: Windsurf 무료 플랜으로도 이 규모의 마이그레이션이 가능한가요?
Windsurf 무료 플랜에서도 Cascade 기본 기능을 사용할 수 있지만, 대규모 프로젝트에서는 Pro 플랜의 확장된 컨텍스트 윈도우와 무제한 Cascade Flow 실행이 생산성에 큰 차이를 만듭니다. 342개 파일 규모의 마이그레이션에서는 Pro 플랜을 권장합니다.
Q2: Cascade가 생성한 코드의 품질을 어떻게 검증하나요?
세 가지 방법을 병행합니다. 첫째, 자동 생성된 테스트를 통한 기능 검증. 둘째, TypeScript 컴파일러의 타입 체크. 셋째, 레거시 시스템과의 API 응답 비교 테스트입니다. Cascade에게 기존 PHP API와 동일한 응답을 반환하는 비교 테스트를 작성하도록 요청하면 정확성을 높일 수 있습니다.
Q3: PHP 외의 레거시 스택(Java, Ruby 등)에도 같은 방법을 적용할 수 있나요?
네, Windsurf Cascade는 대부분의 프로그래밍 언어를 이해합니다. Java Spring, Ruby on Rails, Python Django 등 다양한 레거시 스택에서 Next.js로의 마이그레이션에 동일한 워크플로우를 적용할 수 있습니다. 핵심은 .windsurfrules에 소스와 타겟 스택을 명확히 정의하고, 레거시 코드를 참조 가능한 위치에 두는 것입니다.