왜 NestJS + Prisma를 선택했나 — B2B SaaS 백엔드 기술 선택기
📚 교육용 풀스택 SaaS 개발기 시리즈 (23편)
Express vs NestJS, TypeORM vs Prisma. B2B SaaS 백엔드를 만들면서 기술 스택을 고르기까지의 과정과 첫 커밋 이야기.
2025년 11월 18일. nest new 명령어를 치고 첫 커밋을 올렸다.
git commit -m "initial commit"
이렇게 시작된 프로젝트가 4개월 뒤에는 커밋 1,283개, 테이블 30개, API 100개가 넘는 B2B SaaS 플랫폼이 될 줄은 몰랐다.
오늘은 그 첫날 이야기를 써보려고 한다. 기술 스택을 고르는 과정에서 뭘 고민했고, 왜 이 조합이 되었는지.
📋 프로젝트 배경
만들려는 건 B2B SaaS 플랫폼이었다. 핵심 요구사항을 정리하면:
- 멀티테넌트: 테넌트(고객사)마다 독립된 데이터
- 역할 기반 접근 제어: 관리자/운영자/엔드유저/외부 뷰어 — 4가지 역할
- 복잡한 도메인 로직: 등급 시스템, 진행 트랙, 성과 지표 추적
- 실시간성: 사용자 활동 기록, 실시간 상태 갱신
- 관리 포탈 2개: 시스템 관리용 + 테넌트 관리용
1인 개발이었다. 기획, 설계, 백엔드, 프론트엔드, QA까지 전부 혼자. 그래서 “혼자서도 빠르게 만들 수 있는 스택”이 중요했다.
🛠️ 후보군 비교
Express vs NestJS
사실 Express로 시작할 뻔했다. 가볍고, 자유롭고, 레퍼런스도 많으니까.
근데 프로젝트 스펙을 보면서 생각이 바뀌었다:
| 기준 | Express | NestJS |
|---|---|---|
| 구조 강제성 | 없음 (자유) | 있음 (모듈/컨트롤러/서비스) |
| DI (의존성 주입) | 직접 구현 | 내장 |
| 데코레이터 | 없음 | @Controller, @Injectable 등 |
| 테스트 | 직접 세팅 | Testing Module 내장 |
| Swagger | 미들웨어 | @nestjs/swagger 통합 |
Express의 자유도는 프로젝트가 커지면 독이 된다. 파일 하나에 라우터+비즈니스 로직+DB 쿼리가 다 들어가는 코드를 이미 겪어봤다.
NestJS는 “어디에 뭘 놓을지”를 프레임워크가 정해준다. 1인 개발에서 이건 엄청난 장점이다. 구조 고민에 쓸 시간을 기능 구현에 쓸 수 있으니까.
결정 요인: API 100개 이상 예상 + 4역할 인증 + 테스트 필수 → 구조가 잡힌 NestJS
TypeORM vs Prisma
ORM 선택은 좀 더 고민했다.
| 기준 | TypeORM | Prisma |
|---|---|---|
| 스키마 정의 | 데코레이터 (코드) | schema.prisma (선언형) |
| 마이그레이션 | 자동 생성 (불안정) | 명시적 (prisma migrate) |
| 타입 안전성 | 부분적 | 완전 (자동 생성) |
| 쿼리 빌더 | QueryBuilder | Fluent API |
| 러닝 커브 | 보통 | 낮음 |
TypeORM을 써본 적 있는데, 마이그레이션이 불안정한 게 가장 큰 문제였다. synchronize: true로 개발하다가 프로덕션에서 데이터 날린 경험이 있다면 공감할 거다.
Prisma는 schema.prisma 파일 하나에 모든 테이블이 선언형으로 정의된다. 이게 좋은 이유:
// schema.prisma — 테이블 구조가 한눈에 보인다
model User {
id Int @id @default(autoincrement())
email String @unique
role Role @default(USER)
tenant Tenant @relation(fields: [tenantId], references: [id])
tenantId Int
createdAt DateTime @default(now())
}
model Tenant {
id Int @id @default(autoincrement())
name String
code String @unique
users User[]
}
이걸 TypeORM으로 쓰면 데코레이터 떡칠이 되는데, Prisma는 읽기만 해도 관계가 보인다.
결정 요인: 30개 테이블 예상 + 마이그레이션 안정성 + 타입 자동 생성 → Prisma
🔨 첫날: initial commit
11월 18일. nest new로 프로젝트를 생성했다.
nest new server
cd server
pnpm add prisma @prisma/client
npx prisma init
첫 커밋의 파일 목록:
.gitignore
.prettierrc
README.md
eslint.config.mjs
nest-cli.json
package.json
pnpm-lock.yaml
src/app.controller.ts
src/app.module.ts
src/app.service.ts
src/main.ts
NestJS starter의 기본 파일들. 아직 아무것도 없는 상태다.
그런데 여기서부터 9일 후에 첫 번째 큰 결정을 내리게 된다.
📐 9일 후: 마스터 문서 3,000줄
11월 27일. 코드를 한 줄도 안 치고 문서만 썼다.
docs/마스터.md — 709줄
docs/MVP_EPR_정의.md — 132줄
docs/erd.md — 393줄
docs/필요_정의사항_명확화.md — 1,029줄
docs/우선순위정의.md — 223줄
총 2,486줄의 설계 문서. 코드보다 문서가 먼저였다.
왜 이렇게 했냐면, 클라이언트 머릿속에 있는 것을 하나씩 꺼내서 정리해야 했기 때문이다.
이전 프로젝트에서 겪은 교훈이 있었다. 설계 없이 바로 코딩에 들어가면 개발 중간에 “이건 이런 뜻이 아니었는데”가 반복된다. 요구사항이 바뀌는 게 아니라, 처음부터 서로 다르게 이해하고 있었던 거다. 그래서 이번엔 클라이언트와 비즈니스 관점 차이를 최대한 줄이고 시작하자는 원칙을 세웠다.
등급 30개, 성과 지표 5개, 진행 트랙, 작업 그룹 생성/완료/이어하기 — 머릿속으로만 돌리면 반드시 빠뜨리는 게 생긴다.
특히 필요_정의사항_명확화.md 1,029줄은 “이거 구현할 때 뭘 결정해야 하는지” 를 전부 리스트업한 문서다:
- 등급 조정은 실시간으로 할 건가, 배치로 할 건가?
- 진행 트랙 완료 기준은 누적인가, 연속인가?
- 외부 뷰어 인증은 토큰 기반인가, 세션 기반인가?
이 결정들을 코딩하면서 하면 코드를 뒤엎게 된다. 미리 하면 코드가 한 방향으로 간다.
4개월 후 회고하자면, 이 9일이 프로젝트에서 가장 가치 있는 시간이었다. 나중에 v2.0으로 전면 재작성할 때도 이 문서가 기준이 됐다.
다만, 솔직하게 말하면 — 문서화로 클라이언트의 비즈니스 관점을 정확히 옮겨 담는 데는 성공했다. 하지만 프로토타입을 보여주고 나면 새로운 아이디어가 떠오르고, 비즈니스 관점 자체가 바뀌는 건 막을 수 없었다. 그건 문서의 한계가 아니라 제품 개발의 본질이다.
💡 첫날 배운 것
-
프레임워크 선택은 프로젝트 규모로 결정한다. 작은 프로젝트면 Express, 큰 프로젝트면 NestJS. “익숙함”보다 “맞음”이 중요하다.
-
ORM은 마이그레이션 전략으로 고른다. 쿼리 빌더 문법은 익숙해지면 다 비슷한데, 마이그레이션은 한번 잘못 고르면 프로덕션에서 터진다.
-
설계 문서를 먼저 쓰면 코드가 빨라진다. 문서 2,486줄 쓰는 데 9일 걸렸지만, 그 덕에 첫 번째 API까지는 2주 만에 도달했다.
📊 오늘의 숫자
| 항목 | 값 |
|---|---|
| 커밋 | 1개 (initial commit) |
| 코드 | NestJS 기본 파일 12개 |
| 의존성 | NestJS 코어 + Prisma |
| 설계 문서 | 0줄 (9일 후 2,486줄) |
🔜 다음에 할 것
- 마스터 문서 기반으로 도메인 모델링 시작
- Prisma 스키마 설계 — 30개 테이블의 탄생기
- 유즈케이스 정의 — API가 될 것들
📚 교육용 풀스택 SaaS 개발기 시리즈 (23편)
- 1. 왜 NestJS + Prisma를 선택했나 — B2B SaaS 백엔드 기술 선택기
- 2. 도메인 모델링 첫날 — B2B SaaS의 핵심 엔티티 정의하기
- 3. 27개 테이블의 탄생 — Prisma 스키마 설계기
- 4. 권한 매트릭스 — Admin/운영자/사용자 3역할 설계
- 5. BigInt PK에서 Int PK로 — 첫 번째 스키마 리팩토링
- 6. Seed 데이터의 함정 — FK 삭제 순서 삽질기
- 7. DDD를 도입하기로 했다 — Repository/Domain/Application 3계층
- 8. 인터페이스 구현체로 바꾸는 날 — NestJS DI와 TypeScript의 간극
- 9. 단위 테스트 인프라 구축 — Jest 설정부터 Mock까지
- 10. E2E 테스트와 Cloud SQL의 고난 — 4/8 passing에서 8/8까지
- 11. REST API 첫 구현 — 6개 Controller, 21개 엔드포인트 완성
- 12. v1.0 완성, 그리고 갈아엎기로 결심한 날
- 13. 번들 구조를 통째로 바꿔야 했던 이유
- 14. Phase 1 문서 정비 — Use Case를 번들 기반으로 다시 쓰다
- 15. Phase 2 스키마 마이그레이션 — 데이터 안 날리고 구조 바꾸기
- 16. Phase 3-1·3-2 — Repository와 Domain 서비스로 36개 빌드 에러 잡기
- 17. Phase 3-3·3-4·3-5 — Application부터 Module까지, v2.0 마이그레이션 닫는 날
- 18. 코드를 박은 다음 날 — 4,658줄 DDD 문서를 24분 사이에 다시 쓴 하루
- 19. v2.1 Domain Layer — 도메인 서비스 1,682줄을 한 커밋에 박은 날의 설계 철학
- 20. v3.0 Application Layer 재작성 — 도메인 서비스 위에 얇은 막을 한 Phase에 박은 날
- 21. 갈아엎고 80일 — v2.0 마이그레이션 8편 메타 회고
- 22. 1인 다역으로 5일 만에 90% — Admin Portal MVP를 끌어올린 토글 한 줄
- 23. Mock에선 되던 게 REST에선 안 됐다 — 응답 포맷 한 칸 차이가 만든 하루