기존 SaaS를 supastack으로 어떻게 마이그레이션하나요?
코드베이스에 따라 다르지만 순서는 바뀌지 않습니다: 한 번의 재작성이 아니라 단계별로 마이그레이션하세요. 리포지토리를 제품 코드(라우트, 스키마, 도메인 로직 — 당신의 것으로 남습니다)와 인프라(인증, 테넌시, 결제, 작업, 이메일 — 킷이 대체합니다)로 인벤토리하세요. 모듈은 의존성 순서로 도입합니다: 구조, 인증, 워크스페이스, 결제, 그리고 백그라운드 작업. 데이터는 재실행 가능한 스크립트로 옮기고, Stripe 고객 ID를 유지하고, 인수 테스트가 통과한 뒤 전환하세요. 대부분의 웹 앱은 몇 주가 아니라 며칠 치 저녁이면 됩니다.
코드를 건드리기 전에 인벤토리부터
잘못되는 마이그레이션은 모두 같은 방식으로 시작합니다: 누군가 킷 리포지토리를 열고 파일을 지우기 시작하는 것이죠. 킷은 테마가 아니라 기반입니다 — 인프라를 대체하는 것이지, 당신의 제품에 대해 어떤 의견도 갖지 않습니다. 무엇이든 옮기기 전에 리포지토리를 정직하게 두 더미로 나누세요.
제품 코드란 고객이 당신에게 되설명할 수 있는 모든 것입니다: 라우트, 스키마, 도메인 로직, 카피, 요금 페이지. 인프라란 그들이 결코 보지 않는 모든 것입니다: 세션, 테넌시 검사, 결제 웹훅, 큐에 들어간 작업, 트랜잭션 메일. 첫 번째 더미는 그대로 이전합니다. 두 번째 더미는 교체됩니다 — 그것이 바로 요점입니다.
- 모든 라우트 그룹을 나열하고 제품인지 배관인지 라벨을 붙이세요 — 가차 없이. 웹훅 URL을 토글하는 것뿐인 설정 페이지는 배관입니다.
- 환경 변수를 나열하고 인프라를 구성하는 것(인증 시크릿, 프로바이더 키)과 제품 동작을 바꾸는 것을 표시하세요.
- 테이블을 나열하고 킷이 소유하게 될 것(사용자, 세션, 워크스페이스, 결제 상태)과 당신의 것을 표시하세요.
- 당신에게 돈을 가져다주는 사용자 여정 하나를 적어두세요 — 그 플로가 아래 모든 단계의 인수 테스트입니다.
각 이음새를 킷 모듈에 매핑하기
킷의 인프라는 계약으로 연결된 모듈의 집합입니다 — 당신의 일은 낡은 배관 하나하나를 이제 그것을 소유하는 모듈로 향하게 하는 것입니다. 대부분의 매핑은 일대일이며, 그렇지 않은 곳이 바로 진짜 결정을 내리는 곳입니다.
두 가지 매핑 선택은 기계적인 이식이 아니라 실제 사고가 필요합니다. Prisma에서 오는 경우, 킷은 Drizzle로 표준화되어 있습니다 — 전환 기간 동안 제품 테이블에는 Prisma를 유지할 수 있지만 수렴할 계획을 세우세요. 한 코드베이스에 두 개의 ORM은 복리로 불어나는 유지보수 비용입니다. Prisma vs Drizzle 비교가 무엇이 달라지는지 정리해줍니다.
그리고 관리형 인증 벤더에서 오는 경우, 전환 전에 벤더의 모든 기능을 복제하고 싶은 충동을 억제하세요. 관리형 인증은 제품이 쓰지 않을 수도 있는 것들 — 세션 목록, 조직 UI, 웹훅 동기화 — 을 조용히 묶어둡니다. 제품이 실제로 호출하는 것만 매핑하세요. 인증 비교가 중요한 기능 차이를 다룹니다.
| 현재 배관 | supastack 모듈 | 무엇이 넘어오는가 |
|---|---|---|
| NextAuth.js / Auth.js 세션 | 인증(Better Auth) | 사용자와 계정은 이전하고, 세션은 전환 시 재발급됩니다 |
| Clerk 또는 관리형 인증 벤더 | 인증(Better Auth) | 사용자는 이메일로 이전하고, 서드파티 세션은 재생되지 않고 버려집니다 |
| 자체 조직/팀 테이블 | 워크스페이스 | 조직 행이 워크스페이스 + 멤버십 행이 됩니다 |
| 직접 만든 Stripe 웹훅 | 결제 | Stripe 고객 ID는 살아남고, 구독 상태는 재도출됩니다 |
| CRON + setTimeout 작업 | 작업(Trigger.dev) | 예약된 작업은 공유 JobRunner 계약 뒤로 이동합니다 |
| Nodemailer / 즉흥적인 메일 | 이메일 | 템플릿은 이전하고, 발송은 하나의 프로바이더 어댑터를 거칩니다 |
의존성 순서로 모듈을 도입하세요
마이그레이션 순서는 취향이 아니라 의존성 그래프입니다. 아래 각 단계는 개별적으로 출시 가능하며, 각각 다음 단계가 시작되기 전에 검증할 수 있는 동작하는 시스템을 만들어냅니다. 유혹은 한 주말에 전부를 끝내고 그 교차점을 디버깅하는 것입니다. 참으세요 — 단계별 마이그레이션은 마주치는 모든 버그가 정확히 하나의 모듈에 속한다는 뜻이며, 그것은 건너뛰기 전까지는 고마움을 모르는 디버깅 초능력입니다.
- 1단계 — 스켈레톤: 킷을 스캐폴딩하고, 환경을 구성하고, 제품 라우트를 프로젝트 구조에 떨궈 넣습니다. 아직 아무것도 배선되지 않았습니다. 앱은 부팅되고 페이지는 렌더링됩니다.
- 2단계 — 인증: 로그인을 Better Auth로 옮기고, 사용자/계정 테이블을 마이그레이션하고, 로그인 + 로그아웃을 처음부터 끝까지 검증합니다. 이후 모든 단계는 세션이 존재하는 것에 의존합니다.
- 3단계 — 테넌시: 워크스페이스 모델을 도입하고, 제품 테이블에
workspaceId를 채우고, 읽기와 쓰기에서 범위 지정을 강제합니다. 워크스페이스 경계 포스트가 진가를 발휘하는 곳이 여기입니다. - 4단계 — 결제: 체크아웃과 웹훅을 결제 모듈로 향하게 하고, 기존 구독을 다시 매핑하고, 테스트 모드의 실제 Stripe 데이터로 권한 검사를 검증합니다.
- 5단계 — 백그라운드 작업과 이메일: 작업을 JobRunner 계약으로 이식하고 트랜잭션 메일을 이메일 어댑터로 옮깁니다. 이것들이 마지막인 이유는 사용자와 워크스페이스가 이미 존재하는 것에 의존하기 때문입니다.
데이터는 정직하게 옮기세요
데이터 마이그레이션은 마이그레이션이 악명을 얻는 곳입니다. 지루함을 유지하는 규칙: 두 번 실행할 수 있는 스크립트를 작성하고, 전환 전까지 소스를 절대 변경하지 않고, 스크립트를 믿는 대신 개수를 검증하는 것.
사용자와 계정은 Better Auth 스키마에 도착합니다 — 이메일이 조인 키이고, 비밀번호 기반 계정은 결정이 필요합니다: 기존 해시 형식이 Better Auth가 검증할 수 있는 것인지, 아니면 전환 시 비밀번호 재설정 플로를 출시하고 강제 장치로 삼을지. 서드파티 OAuth 계정은 쉬운 케이스입니다 — 다음 로그인 때 프로바이더가 재발급합니다.
결제의 경우, 정직한 길은 행을 복사하는 것이 아니라 재도출입니다: Stripe 고객 ID를 유지하고, 킷의 웹훅 처리가 프로바이더의 진실에서 권한 상태를 다시 구축하게 한 뒤, 그 계산 결과를 기존 구독 레코드와 대조하세요. 킷의 멱등 웹훅 원장이 존재하는 이유는 바로 재생된 과거 이벤트가 무엇이든 이중으로 프로비저닝하지 않게 하기 위해서입니다.
// migration scripts are boring by design: upsert-shaped,
// re-runnable, and they verify counts instead of trusting themselves
for (const legacyUser of legacyUsers) {
await upsertAuthUser({
email: legacyUser.email.toLowerCase(),
name: legacyUser.displayName,
externalId: legacyUser.id, // preserved for one-shot joins
});
}
const migrated = await countAuthUsers();
if (migrated !== legacyUsers.length) {
throw new Error(`count mismatch: ${migrated} of ${legacyUsers.length}`);
}당신의 것으로 남는 부분
분명히 말해둘 가치가 있습니다. 사람들이 양쪽 방향으로 다 틀리는 부분이니까요: 당신의 제품 라우트, 스키마, 도메인 로직, 디자인은 바뀌지 않습니다. 킷 안에서 앱을 다시 만드는 것이 아니라 — 앱을 그 위에 올려놓는 것입니다.
반대 방향의 실수는 낡은 배관을 새 모듈 옆에 “임시로” 살려두는 것입니다. 두 번째 세션 검사, 두 번째 웹훅 핸들러, 두 번째 권한 테이블 — 모든 “임시” 병렬 시스템은 누군가 삭제할 때까지 영구적이며, 하나하나가 미래의 사고입니다. 교체하고, 검증하고, 삭제하세요. 어떤 단계가 아직 낡은 경로를 삭제할 수 없다면 그 단계는 끝난 것이 아닙니다.
전환 체크리스트
전환은 이벤트가 아니라 체크리스트입니다. 실제 날짜 전에, 프로덕션 형태의 데이터로 스테이징 환경에서 최소 한 번은 돌려보세요.
- 새 프로덕션 스냅샷에 대해 데이터 스크립트를 다시 실행하고, 개수와 스팟 체크가 통과하는지 확인합니다.
- 제품이 지원하는 모든 인증 프로바이더로 로그인 — 존재를 잊고 있던 것들까지 포함해서.
- Stripe 테스트 모드에서 자기 제품을 구매하고 웹훅이 권한까지 도달하는지 따라갑니다.
- 모든 트랜잭션 이메일을 한 번씩 보내고, 링크가 새 배포로 연결되는지 확인합니다.
- 결제 여정을 처음부터 끝까지 검증한 뒤 DNS/환경 변수를 전환하고 첫 실제 세션을 지켜봅니다.
- 이전 배포를 일주일간 계속 실행하세요 — 롤백은 마이그레이션이 아니라 DNS 변경입니다.
스타터 킷으로의 마이그레이션에 대해 자주 묻는 질문
누군가 이 일을 시작할 때마다 같은 몇 가지 질문이 나옵니다.
- 마이그레이션에 실제로 얼마나 걸리나요? — 표준 인증 + Stripe를 쓰는 단일 웹 앱이라면: 몇 주가 아니라 며칠 치 저녁입니다. 커스텀 결제가 있는 멀티테넌트 B2B는 3·4단계에서 시간이 늘어납니다 — 늘어나는 것은 데이터 작업이지 파일 이동이 아닙니다.
- 마이그레이션해야 할까요, 아니면 새로 시작해서 기능을 이식해야 할까요? — 제품 코드가 인프라 부채보다 작다면, 킷 위에서 새로 시작해 제품 라우트를 이식하는 편이 보통 더 빠릅니다. 스키마와 도메인 로직이 어려운 부분이라면 제자리에서 마이그레이션하세요.
- 전환 기간에 기존 ORM / 인증 라이브러리를 유지할 수 있나요? — 잠시라면, 네 — 한 번에 하나의 모듈 경계씩. 영구적으로는, 아니요: 인프라 계층에서 무엇이든 두 개인 것은 복리로 불어나는 비용입니다.
- 이미 날아가고 있는 Stripe 웹훅은 어떻게 하나요? — 4단계가 검증되면 기존 웹훅 엔드포인트를 킷의 핸들러로 향하게 하세요. 멱등 원장이 재생되거나 중복된 이벤트를 위험 대신 안전하게 만듭니다.
- 모든 모듈을 도입해야 하나요? — 아니요. 모바일, 데스크톱, 작업, AI는 도입하거나 무시할 수 있는 표면입니다 — 라이선스는 기반이지, 플랫폼별로 앱을 출시해야 하는 의무가 아닙니다.