--- description: Flutter 모바일 앱 개발 규칙 --- > 이 파일은 `.claude/commands/flutter.md`에 위치한다. > 사용법: `/flutter [요청 내용]` --- ## 언어 & 기본 원칙 - **Dart Sound Null Safety** 완벽 준수 (`?`, `!`, `late` 올바르게 사용) - `dynamic` 타입 사용 **지양** → 명시적 타입 선언 - 모든 주석은 **한국어**로 작성 --- ## 프로젝트 구조 ``` lib/ ├── main.dart ├── app.dart ← MaterialApp / 라우터 설정 ├── core/ │ ├── constants/ ← 상수 (색상, 크기, 텍스트 등) │ ├── theme/ ← 앱 테마 정의 │ ├── api/ ← Dio 인스턴스(dio_client), 에러 사전(error_lexicon), 인터셉터 │ ├── storage/ ← flutter_secure_storage 래퍼 (선택) │ └── utils/ ← 순수 유틸 함수 ├── features/ │ └── user/ │ ├── data/ │ │ ├── models/ ← JSON 직렬화 모델 │ │ └── repositories/ ← API 호출 구현체 │ ├── domain/ │ │ └── entities/ ← 순수 비즈니스 엔티티 │ └── presentation/ │ ├── pages/ ← 화면 단위 위젯 │ ├── widgets/ ← 재사용 위젯 │ └── providers/ ← 상태 관리 └── shared/ └── widgets/ ← 앱 공통 위젯 ``` --- ## 위젯 작성 원칙 ```dart // ✅ 좋은 예 — 작은 위젯으로 분리 class UserProfilePage extends StatelessWidget { const UserProfilePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: const _AppBar(), body: const _ProfileBody(), ); } } class _ProfileBody extends StatelessWidget { const _ProfileBody(); @override Widget build(BuildContext context) { return Column( children: [ const _AvatarSection(), const _InfoSection(), ], ); } } ``` - 위젯 트리 depth가 깊어지면 **커스텀 위젯으로 분리** - `build()` 메서드 내 비즈니스 로직 작성 **금지** - `const` 생성자 적극 활용 (불필요한 리빌드 방지) --- ## 반응형 UI ```dart // ❌ 하드코딩 금지 Container(width: 375, height: 200) // ✅ 반응형 처리 Container( width: MediaQuery.of(context).size.width * 0.9, child: ..., ) // ✅ 유연한 레이아웃 Expanded(flex: 2, child: LeftPanel()), Expanded(flex: 1, child: RightPanel()), ``` - `Expanded`, `Flexible`, `MediaQuery`, `LayoutBuilder` 적극 활용 - 하드코딩 픽셀값 사용 **금지** - 폰트 크기는 `Theme.of(context).textTheme` 활용 --- ## 상태 관리 ```dart // Riverpod 예시 (권장) @riverpod Future fetchUser(FetchUserRef ref, int id) async { final repo = ref.watch(userRepositoryProvider); return repo.getUser(id); } // 위젯에서 사용 class UserWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final userAsync = ref.watch(fetchUserProvider(userId)); return userAsync.when( data: (user) => Text(user.name), loading: () => const CircularProgressIndicator(), error: (e, _) => const ErrorWidget(), ); } } ``` - **Riverpod** 권장 (Provider는 레거시로 간주) - `StatefulWidget`은 애니메이션, 폼 등 로컬 UI 상태에만 제한적 사용 --- ## API 통신 (Dio + 인터셉터) ```dart // core/api/dio_client.dart class DioClient { late final Dio _dio; DioClient() { // flutter_dotenv 로 .env 로드, 미설정 시 로컬 기본값 final baseUrl = dotenv.maybeGet('API_URL') ?? 'http://localhost:3000/api'; _dio = Dio(BaseOptions(baseUrl: baseUrl)); _dio.interceptors.add(AuthInterceptor()); // 토큰 자동 첨부 _dio.interceptors.add(ErrorInterceptor()); // 에러 코드 처리 (error_lexicon 참조) } } // 에러 인터셉터 — CLAUDE.md 에러 맵 기준 한국어 메시지 처리 class ErrorInterceptor extends Interceptor { @override void onError(DioException err, ErrorInterceptorHandler handler) { // 에러 코드 파싱 후 SnackBar/Dialog 표시 } } ``` --- ## 보안 (토큰 저장) ```dart // flutter_secure_storage 사용 const storage = FlutterSecureStorage(); // 저장 (키 이름은 dio_client 인터셉터와 통일: jwt_token) await storage.write(key: 'jwt_token', value: token); // 읽기 final token = await storage.read(key: 'jwt_token'); // 삭제 (로그아웃) await storage.deleteAll(); ``` - JWT, 민감 정보는 반드시 `flutter_secure_storage` 사용 - `SharedPreferences`에 토큰 저장 **금지** --- ## 성능 & 최신 트렌드 추가 규칙 - **모델 직렬화**: `json_serializable` + `freezed` 사용 권장 (불변 모델) - **이미지**: `cached_network_image`로 네트워크 이미지 캐싱 - **목록**: 긴 목록은 `ListView.builder` 사용 (전체 렌더링 금지) - **에러 핸들링**: `Either` 패턴 고려 (`fpdart` 패키지) - **라우팅**: `go_router` 사용 권장 (딥링크, 중첩 라우팅 지원) - **환경 분리**: `--dart-define` 또는 `flutter_dotenv`로 dev/prod 분리