compose 튜토리얼#
feature.yaml 한 장으로 풀스택 feature를 합성하고, 프론트·백엔드 모두 dart analyze 0 errors에 도달하는 전 과정을 따라갑니다.
이 가이드는 실모노레포(test_project)에서 gift_card feature를 합성해 프론트·백엔드 dart analyze 0 errors까지 실제로 도달한 E2E 실증 절차(2026-06-11)
를 그대로 옮긴 것입니다. 매니페스트, 가드 통과, codegen, 수동 단계 모두 실증에 사용된 원문 기준입니다.
전제: backend/{project_name}_server, core/dependencies 패키지, melos workspace를 갖춘 모노레포에서 실행합니다.
Step 1 — 매니페스트 작성#
프로젝트 루트에 feature.yaml을 작성합니다. 아래는 실증에 사용된 원문입니다.
feature: gift_card
project_name: test_project
targets: [application]
network: serverpod
frontend:
usecases: [get_gift_card_balance, get_gift_card_history, charge_gift_card]
blocs:
- { name: gift_card_summary, usecases: [get_gift_card_balance] }
- { name: gift_card_history, usecases: [get_gift_card_history] }
widgets: [gift_card_balance_card, gift_card_history_tile]
models:
entities:
# backend_feature의 endpoint/service 골격은 feature 동명 엔티티를 전제합니다.
- { name: gift_card, fields: { balance: int, ownerUserId: int } }
- { name: gift_card_transaction, fields: { amount: int, memo: String?, transactedAt: DateTime } }
backend:
cache: true
동명 엔티티(gift_card)가 들어간 이유: backend_feature 브릭의 endpoint/service/dto 골격은 feature와 동명인 protocol 클래스
{Feature}(여기서는 GiftCard)를 참조합니다. 이 클래스가 models.entities 선언, backend.entity: true(placeholder), 기존 spy 모델 중 어디서도 공급되지 않으면
백엔드 모델 pre-flight가 생성 전에 hard fail합니다. 그래서
gift_card 엔티티를 실제 필드와 함께 직접 선언했습니다.
이 선언의 파생 결과:
-
repository 메서드:
getGiftCardBalance/getGiftCardHistory/chargeGiftCard(usecase 1:1 이름 규약) -
entities를 선언했으므로
backend.entity는 자동false—GiftCard는 placeholder가 아닌 b-entity(gift_card.spy.yaml)로 생성 -
serverpod 모델:
GiftCard,GiftCardTransaction(b-entity),GiftCardStatus(enum), DTO 5종(GiftCardCreateRequest등) cache: true→gift_card_cache_constants.dart생성
Step 2 — compose 실행#
cob compose --manifest feature.yaml
파일을 하나라도 만들기 전에 실행 전 가드 4종이 순서대로 통과해야 합니다.
-
one-shot 가드 —
feature/application/gift_card와 백엔드lib/src/feature/gift_card가 아직 없음 → 통과 - git-clean 가드 — 작업트리가 깨끗함 → 통과 (git 저장소가 아니면 차단 없이 경고만 출력)
-
라우터 마커 pre-flight —
app_routes.dart라우트 배열 안에// 🔧 AUTO-REGISTER마커 존재 → 통과 -
백엔드 모델 pre-flight — 생성 예정 모델명(
GiftCard,GiftCardTransaction,GiftCardStatus, DTO 5종)이 매니페스트 내부·기존 spy 모델과 충돌 없음 → 통과
통과하면 atomic brick들이 N회 generate되고 다음이 만들어집니다.
-
feature 패키지
feature/application/gift_card/— domain(entity/usecase/repository 인터페이스/failure/exception), data(repository + serverpod mixin), presentation(bloc 3종/page/widget 2종/route) + 레이어별 테스트 골격, barrel(usecase.dart/entity.dart/bloc.dart/widget.dart) 재생성 -
backend feature
backend/test_project_server/lib/src/feature/gift_card/— endpoint/service/constant/exception/validation/helper +model/의 spy 모델(b-entity 2종, enum, DTO 5종) -
워크스페이스/라우터 등록 — 루트
pubspec.yaml의workspace:에- feature/application/gift_card추가,app_routes.dart의 마커 위치에GiftCardRouteName.base라우트 + import 자동 등록
마지막에 요약과 후속 가이드가 출력됩니다.
============================================================
📊 Compose 완료: gift_card (NN files)
============================================================
다음 단계 (순서대로 실행하세요):
1. cd backend/test_project_server && serverpod generate
2. melos run build # build_runner 산출물
3. melos run analyze
📎 serverpod_service getter 스니펫 (수동 추가 — getter 2계열이라 자동 추가하지 않습니다):
// package/network/serverpod_service의 클라이언트 래퍼에 추가:
EndpointGiftCard get giftCard => client.giftCard;
이 출력 순서대로 실행하면 한 번에 끝납니다. 아래 Step 3–4는 같은 작업을 프론트/백엔드로 나눠 검증하는 절차입니다.
Step 3 — 프론트 검증#
melos bootstrap # workspace 의존성 해석 (또는 feature 패키지에서 dart pub get)
melos run build # build_runner — 라우트 .g.dart 등 생성
cd feature/application/gift_card && dart analyze
기대 결과: 0 errors. serverpod mixin의 endpoint 호출이 주석 처리된 TODO stub이므로, 백엔드 codegen 없이도 프론트 feature 패키지는 이 시점에 analyze를 통과합니다.
Step 4 — 백엔드 마무리#
1) serverpod_service getter 추가 — compose가 출력한 스니펫을 package/network/serverpod_service의 클라이언트 래퍼에 붙여넣습니다. getter가 2계열이라 cob이 자동 추가하지 않습니다.
// package/network/serverpod_service의 클라이언트 래퍼에 추가:
EndpointGiftCard get giftCard => client.giftCard;
2) serverpod generate 실행 — spy 모델과 endpoint로부터 protocol 클래스·클라이언트 코드를 생성합니다.
cd backend/test_project_server
serverpod generate
생성 확인: lib/src/generated/에 GiftCard/GiftCardTransaction/GiftCardStatus/DTO protocol 클래스가,
test_project_client에 EndpointGiftCard 클라이언트 코드가 생겼는지 확인합니다 (1번의 getter도 이 시점부터 해석됩니다).
3) 백엔드 analyze
dart analyze # backend/test_project_server에서 — 기대 결과: 0 errors
Step 5 — 비즈니스 로직 구현 (생성기가 멈추는 경계)#
여기까지가 cob의 책임 범위입니다. 컴파일되는 골격까지만 만들고, 비즈니스 로직은 전부 stub + TODO 주석으로 남깁니다.
| 경계 | 위치 | stub 상태 |
|---|---|---|
| mixin 본문 (endpoint 호출) | data/repository/mixins/gift_card_serverpod_mixin.dart |
client.giftCard.… 호출이 주석 처리, Right(null) 반환 |
| mapper 본문 | mixin 주석의 _toEntity(response) 자리 |
protocol → entity 변환 직접 작성 |
| service 비즈니스 로직 | backend/.../service/gift_card_service.dart |
검증/처리/저장 자리에 UnimplementedError |
| bloc 상태 분기 | presentation/bloc/blocs/*/…_bloc.dart |
이벤트 핸들러가 usecase 호출 없이 비어 있음 |
| widget UI | presentation/widget/*.dart |
placeholder Text 한 줄 |
cob이 안 해주는 것: codegen 자동 실행(serverpod generate / build_runner), serverpod_service getter 추가, BLoC 와이어링(생성자 주입 전용 — getIt/injectable 미사용, 출력되는
BlocProvider 예시를 참고해 직접 연결), 그리고 위 표의 모든 본문 구현. 기존 feature에 부품을 더 얹는 것도 compose가 아닌 cob add의 몫입니다.
트러블슈팅#
| 증상 | 원인 | 해결 |
|---|---|---|
❌ serverpod 모델명이 기존 spy 모델과 충돌합니다 (전역 네임스페이스) |
생성 예정 모델명이 다른 feature의 spy 모델과 동일 | 아래 "출처별 해결 안내 읽는 법" |
❌ 대상 feature가 이미 존재합니다 (compose는 1회용) |
동일 feature 재compose | cob add로 전환 |
app_routes.dart에서 AUTO-REGISTER 마커 미발견 |
마커 삭제·누락 | 라우트 배열 안에 마커 복구 |
❌ git 작업트리가 깨끗하지 않습니다 | 미커밋 변경 존재 | 커밋/스태시 또는 --force |
백엔드 모델 pre-flight 거부 (전역 충돌)#
serverpod 모델명은 서버 전역 네임스페이스를 공유합니다. 예를 들어 기존 payments feature가 WalletTransaction/WalletStatus를 이미 보유한 서버에서
wallet feature를 compose하면, b-entity wallet_transaction과 enum WalletStatus가 충돌해 생성 전에 거부됩니다(실증 모노레포에서 실제 관측된 사례).
❌ serverpod 모델명이 기존 spy 모델과 충돌합니다 (전역 네임스페이스):
• WalletTransaction — 기존: lib/src/feature/payments/model/..., 생성 예정: b-entity (wallet_transaction.spy.yaml)
충돌 출처별 해결:
• entities 선언이 출처면: 이름을 변경하거나 선언을 제거하세요(기존 모델 재사용).
• backend_feature(placeholder/enum/dto)가 출처면: 해당 토글을 끄세요 — backend: { entity: false / enum: false / dto: false } — 또는 feature 이름을 변경하세요.
메시지의 각 줄은 모델명 — 기존: <서버 내 파일 경로>, 생성 예정: <매니페스트 쪽 출처> 형식입니다. "생성 예정" 출처가 무엇이냐에 따라 해결이 갈립니다:
b-entity (…)면 models.entities의 이름을 바꾸거나 선언을 제거해 기존 모델을 재사용하고, backend_feature entity placeholder/enum/dto면 해당 토글을 끄거나 feature 이름 자체를 바꿉니다.
one-shot 거부#
❌ 대상 feature가 이미 존재합니다 (compose는 1회용):
• feature/application/gift_card
compose는 빈 슬레이트 전제의 1회용 명령이라 --force로도 우회할 수 없습니다. 기존 feature에 부품을 추가하려면 cob add usecase <name> --feature gift_card처럼
cob add로 전환하세요. 처음부터 다시 합성하려면 feature 디렉토리(프론트+백엔드), 루트 pubspec의 workspace 항목,
app_routes.dart의 import·라우트 라인을 제거한 뒤 재실행합니다.
라우터 마커 부재#
라우터 마커 pre-flight가 app_routes.dart(console target은 console_routes.dart)에서 마커를 찾지 못하면 생성 전에 실패합니다. 라우트 배열
안에 아래 한 줄을 복구한 뒤 다시 실행하세요. 합성 성공 시 이 위치에 GiftCardRouteName.base가 자동 등록됩니다.
// 🔧 AUTO-REGISTER: 새 라우트를 여기에 추가하세요
git-clean 거부#
❌ git 작업트리가 깨끗하지 않습니다. compose 실패 시 복구를 위해
커밋/스태시 후 다시 실행하세요 (--force로 우회 가능).
compose 실패 시 git checkout으로 되돌릴 수 있도록 깨끗한 작업트리를 요구합니다. 커밋(또는 스태시)이 정석이고, 복구 수단을 포기하고 강행하려면
--force를 사용합니다(one-shot 가드는 여전히 적용).
관련 문서#
- cob compose — 실행 흐름, 옵션, 가드 4종, 생성 후 수동 단계
- cob add — 기존 feature에 부품 증분 추가
- feature.yaml 스키마 — 매니페스트 v1 전체 키 레퍼런스