Logocob

compose 튜토리얼

feature.yaml 작성부터 컴파일되는 풀스택 feature까지 E2E 가이드

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는 자동 falseGiftCard는 placeholder가 아닌 b-entity(gift_card.spy.yaml)로 생성
  • serverpod 모델: GiftCard, GiftCardTransaction(b-entity), GiftCardStatus(enum), DTO 5종(GiftCardCreateRequest 등)
  • cache: truegift_card_cache_constants.dart 생성

Step 2 — compose 실행#

cob compose --manifest feature.yaml

파일을 하나라도 만들기 전에 실행 전 가드 4종이 순서대로 통과해야 합니다.

  1. one-shot 가드feature/application/gift_card와 백엔드 lib/src/feature/gift_card가 아직 없음 → 통과
  2. git-clean 가드 — 작업트리가 깨끗함 → 통과 (git 저장소가 아니면 차단 없이 경고만 출력)
  3. 라우터 마커 pre-flightapp_routes.dart 라우트 배열 안에 // 🔧 AUTO-REGISTER 마커 존재 → 통과
  4. 백엔드 모델 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.yamlworkspace:- 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_clientEndpointGiftCard 클라이언트 코드가 생겼는지 확인합니다 (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 가드는 여전히 적용).

관련 문서#