Flutter/Dart 코드 리뷰 모범 사례
Flutter/Dart 애플리케이션을 검토하기 위한 포괄적이고 라이브러리에 구애받지 않는 체크리스트입니다. 이러한 원칙은 사용하는 상태 관리 솔루션, 라우팅 라이브러리 또는 DI 프레임워크에 관계없이 적용됩니다.
1. 일반 프로젝트 상태
- 프로젝트가 일관된 폴더 구조(기능 우선 또는 계층 우선)를 따릅니다.
- 적절한 관심사 분리: UI, 비즈니스 로직, 데이터 계층
- 위젯에 비즈니스 로직이 없으며, 위젯은 순전히 프레젠테이션용입니다.
-
pubspec.yaml이 깔끔합니다. 사용하지 않는 종속성이 없고, 버전이 적절하게 고정되어 있습니다. -
analysis_options.yaml에 엄격한 lint 세트와 엄격한 분석기 설정이 포함되어 있습니다. - 프로덕션 코드에
print()문이 없습니다.dart:developer의log()또는 로깅 패키지를 사용합니다. - 생성된 파일(
.g.dart,.freezed.dart,.gr.dart)이 최신 상태이거나.gitignore에 포함되어 있습니다. - 플랫폼별 코드가 추상화 뒤에 격리되어 있습니다.
2. Dart 언어 함정
- 암시적 동적 타입: 타입 어노테이션이 누락되어
dynamic이 발생합니다.strict-casts,strict-inference,strict-raw-types를 활성화하세요. - 널 안전성 오용: 적절한 널 검사 또는 Dart 3 패턴 매칭(
if (value case var v?)) 대신 과도한!(뱅 연산자) 사용. - 타입 승격 실패: 로컬 변수 승격이 작동할 때
this.field사용. - 너무 광범위하게 예외 캐치:
on절 없이catch (e)만 사용. 항상 예외 유형을 지정하세요. -
Error캐치:Error하위 유형은 버그를 나타내며 캐치되어서는 안 됩니다. - 사용되지 않는
async:await없이async로 표시된 함수. 불필요한 오버헤드입니다. -
late과도 사용: nullable 또는 생성자 초기화가 더 안전한 곳에late사용. 오류를 런타임으로 지연시킵니다. - 루프 내 문자열 연결: 반복적인 문자열 빌드에는
+대신StringBuffer를 사용하세요. -
const컨텍스트에서의 가변 상태:const생성자 클래스의 필드는 변경 가능해서는 안 됩니다. -
Future반환값 무시:await를 사용하거나 의도를 명확히 하기 위해 명시적으로unawaited()를 호출하세요. -
final이 작동하는 곳에var사용: 지역 변수에는final을, 컴파일 타임 상수에는const를 선호하세요. - 상대 가져오기: 일관성을 위해
package:가져오기를 사용하세요. - 가변 컬렉션 노출: 공개 API는 원시
List/Map이 아닌 수정 불가능한 뷰를 반환해야 합니다. - Dart 3 패턴 매칭 누락: 장황한
is검사 및 수동 캐스팅보다는 switch 표현식과if-case를 선호하세요. - 여러 반환값을 위한 일회용 클래스: 단일 용도 DTO 대신 Dart 3 레코드
(String, int)를 사용하세요. - 프로덕션 코드에서
print()사용:dart:developer의log()또는 프로젝트의 로깅 패키지를 사용하세요.print()에는 로그 레벨이 없어 필터링할 수 없습니다.
3. 위젯 모범 사례
위젯 분해:
-
build()메서드가 약 80-100줄을 초과하는 단일 위젯이 없습니다. - 캡슐화와 변경 방식(재빌드 경계)에 따라 위젯이 분할됩니다.
- 위젯을 반환하는 private
_build*()헬퍼 메서드는 별도의 위젯 클래스로 추출됩니다(요소 재사용, const 전파 및 프레임워크 최적화 활성화). - 가변 로컬 상태가 필요하지 않은 경우 Stateful 위젯보다 Stateless 위젯을 선호합니다.
- 재사용 가능한 경우 추출된 위젯은 별도의 파일에 있습니다.
Const 사용:
- 가능한 모든 곳에
const생성자가 사용됩니다. 불필요한 재빌드를 방지합니다. - 변경되지 않는 컬렉션에 대한
const리터럴 (const [],const {}). - 모든 필드가 final인 경우 생성자는
const로 선언됩니다.
Key 사용:
- 목록/그리드에서 재정렬 시 상태를 유지하기 위해
ValueKey가 사용됩니다. -
GlobalKey는 신중하게 사용됩니다. 트리 전체에서 상태에 접근하는 것이 정말로 필요할 때만 사용합니다. -
build()에서UniqueKey는 피합니다. 매 프레임 재빌드를 강제합니다. - 정체성이 단일 값이 아닌 데이터 객체에 기반할 때
ObjectKey가 사용됩니다.
테마 및 디자인 시스템:
- 색상은
Theme.of(context).colorScheme에서 가져옵니다. 하드코딩된Colors.red또는 16진수 값이 없습니다. - 텍스트 스타일은
Theme.of(context).textTheme에서 가져옵니다. 원시 글꼴 크기가 포함된 인라인TextStyle이 없습니다. - 다크 모드 호환성이 확인되었습니다. 밝은 배경을 가정하지 않습니다.
- 간격 및 크기 조정은 일관된 디자인 토큰이나 상수를 사용하며, 매직 넘버를 사용하지 않습니다.
Build 메서드 복잡성:
-
build()에서 네트워크 호출, 파일 I/O 또는 무거운 계산이 없습니다. -
build()에서Future.then()또는async작업이 없습니다. -
build()에서 구독 생성(.listen())이 없습니다. -
setState()는 가능한 가장 작은 하위 트리로 제한됩니다.
4. 상태 관리 (라이브러리 중립적)
이러한 원칙은 모든 Flutter 상태 관리 솔루션(BLoC, Riverpod, Provider, GetX, MobX, Signals, ValueNotifier 등)에 적용됩니다.
아키텍처:
- 비즈니스 로직은 위젯 계층 외부, 즉 상태 관리 컴포넌트(BLoC, Notifier, Controller, Store, ViewModel 등)에 있습니다.
- 상태 관리자는 내부에서 직접 생성하는 것이 아니라 주입을 통해 종속성을 받습니다.
- 서비스 또는 리포지토리 계층이 데이터 소스를 추상화합니다. 위젯과 상태 관리자는 API나 데이터베이스를 직접 호출해서는 안 됩니다.
- 상태 관리자는 단일 책임을 가집니다. 관련 없는 문제를 처리하는 "신" 관리자는 없습니다.
- 컴포넌트 간 종속성은 솔루션의 규칙을 따릅니다.
- Riverpod에서:
ref.watch를 통해 다른 프로바이더에 의존하는 것은 예상되며, 순환 또는 지나치게 복잡한 체인만 플래그 지정합니다. - BLoC에서: bloc은 다른 bloc에 직접 의존해서는 안 됩니다. 공유 리포지토리 또는 프레젠테이션 계층 조정을 선호합니다.
- 기타 솔루션: 컴포넌트 간 통신에 대한 문서화된 규칙을 따릅니다.
- Riverpod에서:
불변성 및 값 동등성 (불변 상태 솔루션: BLoC, Riverpod, Redux):
- 상태 객체는 불변입니다.
copyWith()또는 생성자를 통해 새 인스턴스가 생성되며, 제자리에서 변경되지 않습니다. - 상태 클래스는
==및hashCode를 적절히 구현합니다(비교에 모든 필드 포함). - 메커니즘은 프로젝트 전반에 걸쳐 일관됩니다. 수동 재정의,
Equatable,freezed, Dart 레코드 또는 기타. - 상태 객체 내의 컬렉션은 원시 가변
List/Map으로 노출되지 않습니다.
반응성 규율 (반응형 변이 솔루션: MobX, GetX, Signals):
- 상태는 솔루션의 반응형 API(MobX의
@action, Signals의.value, GetX의.obs)를 통해서만 변경됩니다. 직접 필드 변이는 변경 추적을 우회합니다. - 파생된 값은 중복 저장되지 않고 솔루션의 계산된 메커니즘을 사용합니다.
- 반응 및 디스포저는 적절히 정리됩니다(MobX의
ReactionDisposer, Signals의 효과 정리).
상태 모양 설계:
- 상호 배타적인 상태는 부울 플래그(
isLoading,isError,hasData)가 아닌 봉인된 유형, 유니온 변형 또는 솔루션의 내장 async 상태 유형(예: Riverpod의AsyncValue)을 사용합니다. - 모든 비동기 작업은 로딩, 성공 및 오류를 별개의 상태로 모델링합니다.
- 모든 상태 변형은 UI에서 철저히 처리됩니다. 조용히 무시되는 경우가 없습니다.
- 오류 상태는 표시를 위해 오류 정보를 전달합니다. 로딩 상태는 오래된 데이터를 전달하지 않습니다.
- Nullable 데이터는 로딩 표시기로 사용되지 않습니다. 상태는 명시적입니다.
// BAD — 부울 플래그 혼합으로 불가능한 상태 허용
class UserState {
bool isLoading = false;
bool hasError = false; // isLoading && hasError가 표현 가능!
User? user;
}
// GOOD (불변 접근 방식) — 봉인된 유형이 불가능한 상태를 표현 불가능하게 만듦
sealed class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
final User user;
const UserLoaded(this.user);
}
class UserError extends UserState {
final String message;
const UserError(this.message);
}
// GOOD (반응형 접근 방식) — 관찰 가능한 enum + 데이터, 반응형 API를 통한 변이
// enum UserStatus { initial, loading, loaded, error }
// 솔루션의 observable/signal을 사용하여 상태와 데이터를 별도로 래핑
재빌드 최적화:
- 상태 소비자 위젯(Builder, Consumer, Observer, Obx, Watch 등)은 가능한 한 좁은 범위로 지정됩니다.
- 특정 필드가 변경될 때만 재빌드하도록 선택기가 사용됩니다. 모든 상태 방출 시 재빌드하지 않습니다.
-
const위젯을 사용하여 트리를 통한 재빌드 전파를 중지합니다. - 계산/파생된 상태는 반응적으로 계산되며, 중복 저장되지 않습니다.
구독 및 폐기:
- 모든 수동 구독(
.listen())은dispose()/close()에서 취소됩니다. - 더 이상 필요하지 않으면 스트림 컨트롤러가 닫힙니다.
- 타이머는 폐기 수명 주기에서 취소됩니다.
- 수동 구독보다 프레임워크 관리 수명 주기가 선호됩니다(선언적 빌더 vs
.listen()). - 비동기 콜백에서
setState전에mounted확인. -
context.mounted확인 없이await후에BuildContext를 사용하지 않습니다(Flutter 3.7+). 오래된 컨텍스트는 충돌을 유발합니다. - 위젯이 여전히 마운트되었는지 확인하지 않고 비동기 갭 후에 탐색, 대화상자 또는 스캐폴드 메시지가 없습니다.
-
BuildContext는 싱글톤, 상태 관리자 또는 정적 필드에 저장되지 않습니다.
로컬 vs 전역 상태:
- 임시 UI 상태(체크박스, 슬라이더, 애니메이션)는 로컬 상태(
setState,ValueNotifier)를 사용합니다. - 공유 상태는 필요한 만큼만 높이 올립니다. 과도하게 전역화하지 않습니다.
- 기능 범위 상태는 기능이 더 이상 활성화되지 않을 때 적절히 폐기됩니다.
5. 성능
불필요한 재빌드:
-
setState()가 루트 위젯 수준에서 호출되지 않습니다. 상태 변경을 지역화합니다. -
const위젯을 사용하여 재빌드 전파를 중지합니다. - 독립적으로 다시 칠하는 복잡한 하위 트리 주위에
RepaintBoundary가 사용됩니다. - 애니메이션과 독립적인 하위 트리에 대해
AnimatedBuilder자식 매개변수가 사용됩니다.
build()에서의 비용이 많이 드는 작업:
-
build()에서 대규모 컬렉션 정렬, 필터링 또는 매핑이 없습니다. 상태 관리 계층에서 계산합니다. -
build()에서 정규식 컴파일이 없습니다. -
MediaQuery.of(context)사용은 구체적입니다(예:MediaQuery.sizeOf(context)).
이미지 최적화:
- 네트워크 이미지는 캐싱을 사용합니다(프로젝트에 적합한 모든 캐싱 솔루션).
- 대상 기기에 적절한 이미지 해상도(썸네일에 4K 이미지를 로드하지 않음).
-
Image.asset은cacheWidth/cacheHeight를 사용하여 표시 크기로 디코딩합니다. - 네트워크 이미지에 대해 플레이스홀더 및 오류 위젯이 제공됩니다.
지연 로딩:
- 크거나 동적인 목록의 경우
ListView(children: [...])대신ListView.builder/GridView.builder를 사용합니다(작고 정적인 목록에는 구체적인 생성자가 적합합니다). - 대규모 데이터 세트에 대해 페이지네이션이 구현되었습니다.
- 웹 빌드에서 무거운 라이브러리에 대해 지연 로딩(
deferred as)이 사용됩니다.
기타:
- 애니메이션에서
Opacity위젯을 피합니다.AnimatedOpacity또는FadeTransition을 사용합니다. - 애니메이션에서 클리핑을 피합니다. 이미지를 미리 클립합니다.
- 위젯에서
operator ==를 재정의하지 않습니다. 대신const생성자를 사용합니다. - 내부 치수 위젯(
IntrinsicHeight,IntrinsicWidth)은 드물게 사용합니다(추가 레이아웃 패스).
6. 테스트
테스트 유형 및 기대:
- 단위 테스트: 모든 비즈니스 로직(상태 관리자, 리포지토리, 유틸리티 함수)을 다룹니다.
- 위젯 테스트: 개별 위젯 동작, 상호 작용 및 시각적 출력을 다룹니다.
- 통합 테스트: 중요한 사용자 흐름을 종단 간 다룹니다.
- 골든 테스트: 디자인에 중요한 UI 구성 요소에 대한 픽셀 완벽한 비교.
적용 범위 목표:
- 비즈니스 로직에서 80% 이상의 라인 적용 범위를 목표로 합니다.
- 모든 상태 전환에 해당 테스트가 있습니다(로딩 → 성공, 로딩 → 오류, 재시도 등).
- 엣지 케이스 테스트: 빈 상태, 오류 상태, 로딩 상태, 경계 값.
테스트 격리:
- 외부 종속성(API 클라이언트, 데이터베이스, 서비스)은 모의 또는 가짜로 대체됩니다.
- 각 테스트 파일은 정확히 하나의 클래스/단위를 테스트합니다.
- 테스트는 구현 세부 사항이 아닌 동작을 검증합니다.
- 스텁은 각 테스트에 필요한 동작만 정의합니다(최소 스텁).
- 테스트 케이스 간에 공유 가변 상태가 없습니다.
위젯 테스트 품질:
-
pumpWidget및pump은 비동기 작업에 올바르게 사용됩니다. -
find.byType,find.text,find.byKey가 적절히 사용됩니다. - 타이밍에 의존하는 불안정한 테스트가 없습니다.
pumpAndSettle또는 명시적pump(Duration)을 사용합니다. - 테스트는 CI에서 실행되고 실패 시 병합이 차단됩니다.
7. 접근성
시맨틱 위젯:
- 자동 레이블이 충분하지 않은 경우
Semantics위젯을 사용하여 스크린 리더 레이블을 제공합니다. - 순전히 장식적인 요소에는
ExcludeSemantics를 사용합니다. - 관련 위젯을 단일 접근 가능한 요소로 결합하려면
MergeSemantics를 사용합니다. - 이미지에는
semanticLabel속성이 설정되어 있습니다.
스크린 리더 지원:
- 모든 대화형 요소는 포커스 가능하고 의미 있는 설명이 있습니다.
- 포커스 순서는 논리적입니다(시각적 읽기 순서를 따름).
시각적 접근성:
- 배경에 대한 텍스트의 대비 비율 >= 4.5:1.
- 탭 가능한 대상은 최소 48x48 픽셀입니다.
- 색상이 상태의 유일한 표시자가 아닙니다(아이콘/텍스트를 함께 사용).
- 텍스트는 시스템 글꼴 크기 설정에 따라 크기가 조정됩니다.
상호 작용 접근성:
- no-op
onPressed콜백이 없습니다. 모든 버튼은 작업을 수행하거나 비활성화됩니다. - 오류 필드는 수정 사항을 제안합니다.
- 사용자가 데이터를 입력하는 동안 컨텍스트가 예기치 않게 변경되지 않습니다.
8. 플랫폼별 고려 사항
iOS/Android 차이점:
- 적절한 경우 플랫폼 적응형 위젯이 사용됩니다.
- 뒤로 탐색이 올바르게 처리됩니다(Android 뒤로 버튼, iOS 스와이프하여 뒤로 가기).
- 상태 표시줄 및 안전 영역은
SafeArea위젯을 통해 처리됩니다. - 플랫폼별 권한이
AndroidManifest.xml및Info.plist에 선언되어 있습니다.
반응형 디자인:
- 반응형 레이아웃을 위해
LayoutBuilder또는MediaQuery가 사용됩니다. - 중단점이 일관되게 정의됩니다(휴대폰, 태블릿, 데스크탑).
- 작은 화면에서 텍스트가 오버플로되지 않습니다.
Flexible,Expanded,FittedBox를 사용합니다. - 가로 방향이 테스트되었거나 명시적으로 잠겨 있습니다.
- 웹 전용: 마우스/키보드 상호 작용 지원, 호버 상태 존재.
9. 보안
안전한 저장소:
- 민감한 데이터...