> ## Documentation Index
> Fetch the complete documentation index at: https://supastack.dev/llms.txt
> Use this file to discover all available pages before exploring further.
> For the full documentation corpus in one file, fetch: https://supastack.dev/llms-full.txt

# 기존 SaaS를 supastack으로 옮기기

동작 중인 SaaS — 인증, 테넌시, 결제, 작업 — 를 재작성도 다운타임 주말도 없이 supastack 기반 위로 마이그레이션하는 단계별 플레이북.

_게시일 2026-10-29._
_2026-10-29 업데이트됨._

> **기존 SaaS를 supastack으로 어떻게 마이그레이션하나요?**
> 코드베이스에 따라 다르지만 순서는 바뀌지 않습니다: 한 번의 재작성이 아니라 단계별로 마이그레이션하세요. 리포지토리를 제품 코드(라우트, 스키마, 도메인 로직 — 당신의 것으로 남습니다)와 인프라(인증, 테넌시, 결제, 작업, 이메일 — 킷이 대체합니다)로 인벤토리하세요. 모듈은 의존성 순서로 도입합니다: 구조, 인증, 워크스페이스, 결제, 그리고 백그라운드 작업. 데이터는 재실행 가능한 스크립트로 옮기고, Stripe 고객 ID를 유지하고, 인수 테스트가 통과한 뒤 전환하세요. 대부분의 웹 앱은 몇 주가 아니라 며칠 치 저녁이면 됩니다.

## 코드를 건드리기 전에 인벤토리부터

잘못되는 마이그레이션은 모두 같은 방식으로 시작합니다: 누군가 킷 리포지토리를 열고 파일을 지우기 시작하는 것이죠. 킷은 테마가 아니라 기반입니다 — 인프라를 대체하는 것이지, 당신의 제품에 대해 어떤 의견도 갖지 않습니다. 무엇이든 옮기기 전에 리포지토리를 정직하게 두 더미로 나누세요.

제품 코드란 고객이 당신에게 되설명할 수 있는 모든 것입니다: 라우트, 스키마, 도메인 로직, 카피, 요금 페이지. 인프라란 그들이 결코 보지 않는 모든 것입니다: 세션, 테넌시 검사, 결제 웹훅, 큐에 들어간 작업, 트랜잭션 메일. 첫 번째 더미는 그대로 이전합니다. 두 번째 더미는 교체됩니다 — 그것이 바로 요점입니다.

- 모든 라우트 그룹을 나열하고 제품인지 배관인지 라벨을 붙이세요 — 가차 없이. 웹훅 URL을 토글하는 것뿐인 설정 페이지는 배관입니다.
- 환경 변수를 나열하고 인프라를 구성하는 것(인증 시크릿, 프로바이더 키)과 제품 동작을 바꾸는 것을 표시하세요.
- 테이블을 나열하고 킷이 소유하게 될 것(사용자, 세션, 워크스페이스, 결제 상태)과 당신의 것을 표시하세요.
- 당신에게 돈을 가져다주는 사용자 여정 하나를 적어두세요 — 그 플로가 아래 모든 단계의 인수 테스트입니다.


## 각 이음새를 킷 모듈에 매핑하기

킷의 인프라는 계약으로 연결된 모듈의 집합입니다 — 당신의 일은 낡은 배관 하나하나를 이제 그것을 소유하는 모듈로 향하게 하는 것입니다. 대부분의 매핑은 일대일이며, 그렇지 않은 곳이 바로 진짜 결정을 내리는 곳입니다.

두 가지 매핑 선택은 기계적인 이식이 아니라 실제 사고가 필요합니다. `Prisma`에서 오는 경우, 킷은 Drizzle로 표준화되어 있습니다 — 전환 기간 동안 제품 테이블에는 Prisma를 유지할 수 있지만 수렴할 계획을 세우세요. 한 코드베이스에 두 개의 ORM은 복리로 불어나는 유지보수 비용입니다. [Prisma vs Drizzle 비교](https://supastack.dev/ko/blog/prisma-vs-drizzle-for-saas.md)가 무엇이 달라지는지 정리해줍니다.

그리고 관리형 인증 벤더에서 오는 경우, 전환 전에 벤더의 모든 기능을 복제하고 싶은 충동을 억제하세요. 관리형 인증은 제품이 쓰지 않을 수도 있는 것들 — 세션 목록, 조직 UI, 웹훅 동기화 — 을 조용히 묶어둡니다. 제품이 실제로 호출하는 것만 매핑하세요. [인증 비교](https://supastack.dev/ko/blog/better-auth-vs-nextauth-vs-clerk.md)가 중요한 기능 차이를 다룹니다.

| 현재 배관 | 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`를 채우고, 읽기와 쓰기에서 범위 지정을 강제합니다. [워크스페이스 경계 포스트](https://supastack.dev/ko/blog/workspace-boundaries.md)가 진가를 발휘하는 곳이 여기입니다.
- 4단계 — 결제: 체크아웃과 웹훅을 결제 모듈로 향하게 하고, 기존 구독을 다시 매핑하고, 테스트 모드의 실제 Stripe 데이터로 권한 검사를 검증합니다.
- 5단계 — 백그라운드 작업과 이메일: 작업을 JobRunner 계약으로 이식하고 트랜잭션 메일을 이메일 어댑터로 옮깁니다. 이것들이 마지막인 이유는 사용자와 워크스페이스가 이미 존재하는 것에 의존하기 때문입니다.


## 데이터는 정직하게 옮기세요

데이터 마이그레이션은 마이그레이션이 악명을 얻는 곳입니다. 지루함을 유지하는 규칙: 두 번 실행할 수 있는 스크립트를 작성하고, 전환 전까지 소스를 절대 변경하지 않고, 스크립트를 믿는 대신 개수를 검증하는 것.

사용자와 계정은 Better Auth 스키마에 도착합니다 — 이메일이 조인 키이고, 비밀번호 기반 계정은 결정이 필요합니다: 기존 해시 형식이 Better Auth가 검증할 수 있는 것인지, 아니면 전환 시 비밀번호 재설정 플로를 출시하고 강제 장치로 삼을지. 서드파티 OAuth 계정은 쉬운 케이스입니다 — 다음 로그인 때 프로바이더가 재발급합니다.

결제의 경우, 정직한 길은 행을 복사하는 것이 아니라 재도출입니다: Stripe 고객 ID를 유지하고, 킷의 웹훅 처리가 프로바이더의 진실에서 권한 상태를 다시 구축하게 한 뒤, 그 계산 결과를 기존 구독 레코드와 대조하세요. 킷의 [멱등 웹훅 원장](https://supastack.dev/ko/blog/idempotent-billing-webhooks.md)이 존재하는 이유는 바로 재생된 과거 이벤트가 무엇이든 이중으로 프로비저닝하지 않게 하기 위해서입니다.

```
// 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는 도입하거나 무시할 수 있는 표면입니다 — 라이선스는 기반이지, 플랫폼별로 앱을 출시해야 하는 의무가 아닙니다.
