Claude Code로 Flutter/Dart 개발하기: 프로젝트 파악, 상태, 테스트, 빌드
Claude Code로 Flutter/Dart 프로젝트를 읽고 pubspec, 위젯, 상태, 테스트, 빌드와 기기 검증까지 진행하는 방법.
Flutter/Dart 개발에서 Claude Code의 장점은 위젯 하나를 빠르게 만드는 데서 끝나지 않습니다. 실제 변경은 lib/뿐 아니라 pubspec.yaml, assets, 테스트, Android와 iOS 설정, Web 제약, 에뮬레이터 확인, 빌드 명령까지 이어집니다. Widget은 화면을 구성하는 부품, state는 화면 표시를 바꾸는 데이터, pubspec은 SDK 제약과 의존성, 리소스를 적는 설정 파일이라고 이해하면 됩니다.
이 글은 작은 장바구니 화면을 예로 들어 Claude Code에 안전하게 일을 맡기는 방법을 정리합니다. harness는 “에이전트가 안전하게 일하기 위한 발판”입니다. 즉, 수정 가능한 파일, 금지 사항, 확인 명령, 리뷰 기준을 함께 주는 작업 틀입니다. 공식 정보는 Flutter CLI, Flutter testing, Widget testing, Flutter pubspec options, Dart pubspec, Platform channels, Claude Code common workflows를 기준으로 삼습니다. 함께 보면 좋은 글은 React Native 개발, 테스트 전략, CLAUDE.md 모범 사례입니다.
먼저 프로젝트 지도를 만든다
처음부터 “장바구니 화면을 만들어 줘”라고 요청하면 Claude Code는 상태 관리, 폴더 구조, 테스트 방식을 추측합니다. 먼저 읽기 전용 조사를 시키면 기존 규칙을 벗어나는 변경을 줄일 수 있습니다.
이 Flutter 프로젝트를 먼저 읽어 주세요.
아직 파일을 수정하지 마세요.
확인할 항목:
1. pubspec.yaml의 SDK 제약, 의존성, assets 설정
2. lib/ 아래 UI, 상태, data 계층 구조
3. test/와 integration_test/의 유무 및 기존 테스트 스타일
4. android/ ios/ web/ macos/ windows/ linux/ 중 실제 대상
5. flutter analyze, flutter test, build 명령의 사용 가능 여부
마지막에 수정해도 되는 파일과 건드리지 말아야 할 파일을 나눠 보고해 주세요.
| 영역 | Claude Code가 할 일 | 사람이 결정할 일 |
|---|---|---|
| 프로젝트 지도 | 구조, 의존성, 테스트 스타일 파악 | 수정 범위 |
| Widget | UI 부품, 접근성, 반응형 세부 조정 | 문구와 디자인 판단 |
| state | 기존 setState, ChangeNotifier, Riverpod, Bloc 방식에 맞추기 | 앱 전체 상태 전략 |
| pubspec | 의존성 및 assets 변경 제안 | 새 패키지 채택 여부 |
| platform | Android/iOS/Web/Desktop 차이 조사 | 지원 대상과 스토어 요건 |
| test/build | widget test, integration test, 명령 확인 | 병합과 릴리스 판단 |
pubspec 변경은 작게 유지한다
pubspec.yaml은 작아 보이지만 의존성, assets, SDK 제약, 테스트 패키지를 통제합니다. Claude Code가 편의상 패키지를 추가하면 lockfile이 바뀌고 다른 작업자의 화면까지 영향을 받을 수 있습니다.
name: cart_ai_demo
description: A small Flutter cart screen used to verify Claude Code prompts.
publish_to: "none"
environment:
sdk: ">=3.4.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter:
uses-material-design: true
assets:
- assets/images/
새 샌드박스라면 다음 명령으로 기준선을 만들 수 있습니다. 기존 저장소에서는 현재 설정에 맞춘 명령을 먼저 제안하게 해야 합니다.
flutter create cart_ai_demo
cd cart_ai_demo
flutter pub get
flutter analyze
flutter test
실패 사례는 분명합니다. assets 들여쓰기를 깨서 이미지가 로드되지 않거나, pubspec.lock을 보지 않고 업그레이드하거나, 작은 화면 하나 때문에 새 상태 관리 패키지를 넣는 경우입니다.
pubspec.yaml을 확인하고 장바구니 변경에 새 의존성이 필요한지 판단해 주세요.
수정 전에 이유, 대안, 영향 파일, 검증 명령을 먼저 보여 주세요.
승인 전에는 pubspec.yaml과 pubspec.lock을 수정하지 마세요.
Widget과 state를 함께 작게 바꾼다
다음 코드는 flutter create cart_ai_demo 직후의 lib/main.dart에 붙여 넣을 수 있습니다. Flutter SDK만 사용합니다. CartController가 상태 변경을 담당하고, Widget은 렌더링에 집중합니다.
import 'package:flutter/material.dart';
void main() => runApp(const CartDemoApp());
class CartDemoApp extends StatelessWidget {
const CartDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Cart AI Demo',
theme: ThemeData(colorSchemeSeed: Colors.teal, useMaterial3: true),
home: const CartSummaryPage(),
);
}
}
class CartLine {
const CartLine({required this.name, required this.price, required this.quantity});
final String name;
final int price;
final int quantity;
CartLine copyWith({int? quantity}) =>
CartLine(name: name, price: price, quantity: quantity ?? this.quantity);
}
class CartController extends ChangeNotifier {
final List<CartLine> _lines = const [
CartLine(name: 'Dart notebook', price: 1800, quantity: 1),
CartLine(name: 'Flutter sticker', price: 500, quantity: 2),
].toList();
List<CartLine> get lines => List.unmodifiable(_lines);
int get total => _lines.fold(0, (sum, line) => sum + line.price * line.quantity);
void increment(int index) {
final line = _lines[index];
_lines[index] = line.copyWith(quantity: line.quantity + 1);
notifyListeners();
}
void decrement(int index) {
final line = _lines[index];
if (line.quantity == 1) return;
_lines[index] = line.copyWith(quantity: line.quantity - 1);
notifyListeners();
}
}
class CartSummaryPage extends StatefulWidget {
const CartSummaryPage({super.key});
@override
State<CartSummaryPage> createState() => _CartSummaryPageState();
}
class _CartSummaryPageState extends State<CartSummaryPage> {
late final CartController controller;
@override
void initState() {
super.initState();
controller = CartController();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Cart summary')),
body: AnimatedBuilder(
animation: controller,
builder: (context, _) => ListView(
padding: const EdgeInsets.all(16),
children: [
for (final entry in controller.lines.indexed)
ListTile(
title: Text(entry.$2.name),
subtitle: Text('JPY ${entry.$2.price} x ${entry.$2.quantity}'),
trailing: Wrap(
spacing: 8,
children: [
IconButton(
tooltip: 'Decrease ${entry.$2.name}',
onPressed: () => controller.decrement(entry.$1),
icon: const Icon(Icons.remove_circle_outline),
),
IconButton(
tooltip: 'Increase ${entry.$2.name}',
onPressed: () => controller.increment(entry.$1),
icon: const Icon(Icons.add_circle_outline),
),
],
),
),
const Divider(),
Text('Total: JPY ${controller.total}',
style: Theme.of(context).textTheme.headlineSmall),
],
),
),
);
}
}
주의할 점은 build 안에서 controller를 새로 만들지 않는 것, notifyListeners()를 빠뜨리지 않는 것, 가격 계산을 여러 Widget에 흩뜨리지 않는 것입니다.
테스트와 기기 검증을 포함한다
프로젝트 이름이 cart_ai_demo라면 다음 파일을 test/cart_summary_test.dart에 둘 수 있습니다.
import 'package:cart_ai_demo/main.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('updates the cart total when quantity changes', (tester) async {
await tester.pumpWidget(const CartDemoApp());
expect(find.text('Total: JPY 2800'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add_circle_outline).first);
await tester.pump();
expect(find.text('Total: JPY 4600'), findsOneWidget);
await tester.tap(find.byIcon(Icons.remove_circle_outline).first);
await tester.pump();
expect(find.text('Total: JPY 2800'), findsOneWidget);
});
}
flutter analyze
flutter test
에뮬레이터나 실제 기기 확인은 integration test로 분리합니다.
import 'package:cart_ai_demo/main.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('cart can be changed on a real device', (tester) async {
await tester.pumpWidget(const CartDemoApp());
await tester.tap(find.byIcon(Icons.add_circle_outline).last);
await tester.pumpAndSettle();
expect(find.text('Total: JPY 3300'), findsOneWidget);
});
}
flutter devices
flutter test integration_test -d <device_id>
플랫폼별 함정과 빌드 확인
Flutter는 Dart 계층을 공유하지만 배포 환경까지 동일하게 만들지는 않습니다. Android는 manifest 권한과 minSdk, iOS는 Info.plist, CocoaPods, 서명, Web은 CORS와 브라우저 API, Desktop은 파일 권한과 네이티브 플러그인을 별도로 확인해야 합니다.
flutter analyze
flutter test
flutter build apk --debug
flutter build web --release
iOS 릴리스 빌드는 macOS와 Xcode가 필요합니다. 실행할 수 없는 환경에서 “iOS 검증 완료”라고 쓰게 하지 말고, 가능한 검증과 불가능한 검증을 프롬프트에 명확히 적습니다.
실용 유스케이스는 네 가지입니다. 기존 목록에 수량, 필터, 정렬 상태를 추가하는 작업, pubspec.yaml과 assets 정리, Android/iOS/Web 중 한 플랫폼에서만 나는 권한 또는 플러그인 문제 조사, 테스트가 없던 화면에 회귀 테스트를 추가하는 작업입니다. 각 요청에는 대상 파일, 기대 명령, 실패 시 보고 방식이 들어가야 합니다.
안전한 프롬프트와 검증 메모
조사: lib/, test/, pubspec.yaml, 플랫폼 디렉터리를 읽고 편집 없이 지도와 위험을 보고합니다.
구현: lib/features/cart만 수정하고 기존 상태 관리 방식을 따르며 의존성을 추가하지 않습니다.
pubspec: 의존성이 필요하면 멈추고 이유, 대안, 영향 범위, 검증 명령을 제시합니다.
검증: widget test를 추가하고 flutter analyze와 flutter test 결과를 보고합니다.
리뷰: dispose, 비동기 처리, build 부작용, 플랫폼 설정, 접근성, 패키지 변경, 누락 테스트를 비판적으로 확인합니다.
Masa의 실무 메모로는, Flutter 작업은 “먼저 지도, 다음 작은 차이, 마지막 테스트와 빌드” 순서가 가장 안정적이었습니다. 반대로 “좋은 Flutter 화면을 만들어 줘”라는 요청은 UI는 빨리 나오지만 build 안 상태 생성, 불필요한 패키지, 플랫폼 검토 누락이 생기기 쉬웠습니다. 팀 적용에는 무료 치트시트와 Claude Code 교육 및 도입 상담이 도움이 됩니다. AI 작업 경계를 더 깊게 설계하려면 harness engineering을 참고하세요.
이 예제는 flutter create cart_ai_demo의 표준 구조를 기준으로 작성했습니다. 현재 작성 환경에는 Flutter SDK가 없어 로컬에서 flutter test를 실행하지 못했습니다. 공개 전에는 실제 Flutter 환경에서 flutter analyze, flutter test, 대상 플랫폼 빌드와 기기 검증을 반드시 실행해야 합니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Obsidian 메모를 CLAUDE.md로 바꾸는 Claude Code 워크플로
Obsidian 작업 메모를 CLAUDE.md 운영 노트로 정리해 Claude Code 세션의 문맥 반복을 줄입니다.
Claude Code Revenue CTA Routing: 글에서 PDF, Gumroad, 상담으로 보내기
독자 의도에 따라 무료 PDF, Gumroad 상품, 상담으로 나누는 Claude Code CTA 설계입니다.
Claude Code 팀 인계 규칙: 리뷰 증거, 권한, 롤백, 수익 경로까지 넘기는 법
Claude Code 작업을 팀에 넘길 때 필요한 증거, 권한 규칙, 롤백, 무료 PDF, Gumroad, 상담 경로 체크리스트.