Flutter/Dart 코드 리뷰 모범 사례
Flutter/Dart 애플리케이션을 검토하기 위한 포괄적이고 라이브러리에 구애받지 않는 체크리스트입니다. 이러한 원칙은 사용되는 상태 관리 솔루션, 라우팅 라이브러리 또는 DI 프레임워크에 관계없이 적용됩니다.
1. 일반적인 프로젝트 상태
- 프로젝트가 일관된 폴더 구조(기능 우선 또는 계층 우선)를 따릅니다.
- 책임이 적절히 분리되어 있습니다: UI, 비즈니스 로직, 데이터 레이어
- 위젯에 비즈니스 로직이 없습니다. 위젯은 순수하게 표현만 담당합니다.
-
pubspec.yaml이 깔끔합니다. 사용하지 않는 종속성이 없고, 버전이 적절히 고정되어 있습니다. -
analysis_options.yaml이 엄격한 린트 세트와 엄격한 분석기 설정을 포함합니다. - 프로덕션 코드에
print()문이 없습니다. 대신dart:developer의log()또는 로깅 패키지를 사용합니다. - 생성된 파일(
.g.dart,.freezed.dart,.gr.dart)이 최신 상태이거나.gitignore에 포함되어 있습니다. - 플랫폼 특정 코드가 추상화 뒤에 격리되어 있습니다.
2. Dart 언어 함정
- 암시적 dynamic: 타입 애노테이션이 누락되어
dynamic이 발생합니다.strict-casts,strict-inference,strict-raw-types를 활성화하세요. - Null 안전성 오용: 적절한 null 검사 또는 Dart 3 패턴 매칭(
if (value case var v?)) 대신 과도한!(bang 연산자)를 사용합니다. - 타입 승격 실패: 로컬 변수 승격으로 해결될 수 있는 곳에
this.field를 사용합니다. - 너무 광범위하게 catch:
on절 없이catch (e)를 사용합니다. 항상 예외 유형을 지정하세요. -
Errorcatch:Error하위 유형은 버그를 나타내므로 catch해서는 안 됩니다. - 사용되지 않는
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줄을 초과하는 단일 위젯이 없습니다. - 위젯이 캡슐화와 변경 방식(재빌드 경계)에 따라 분할됩니다.
- 위젯을 반환하는 비공개
_build*()헬퍼 메서드는 별도의 위젯 클래스로 추출됩니다(요소 재사용, const 전파, 프레임워크 최적화 가능). - 변경 가능한 로컬 상태가 필요하지 않은 경우 Stateful 위젯보다 Stateless 위젯을 선호합니다.
- 재사용 가능한 경우 추출된 위젯은 별도의 파일에 있습니다.
Const 사용:
- 가능한 모든 곳에서
const생성자가 사용됩니다. 불필요한 재빌드를 방지합니다. - 변경되지 않는 컬렉션에
const리터럴을 사용합니다(const [],const {}). - 모든 필드가 final인 경우 생성자가
const로 선언됩니다.
Key 사용:
- 리스트/그리드에서 재정렬 시 상태를 유지하기 위해
ValueKey가 사용됩니다. -
GlobalKey는 정말로 트리 전체에서 상태에 접근해야 하는 경우에만 드물게 사용됩니다. -
UniqueKey는build()에서 사용되지 않습니다. 매 프레임 재빌드를 강제합니다. - ID가 단일 값이 아닌 데이터 객체를 기반으로 하는 경우
ObjectKey가 사용됩니다.
테마 및 디자인 시스템:
- 색상은
Theme.of(context).colorScheme에서 가져옵니다. 하드코딩된Colors.red또는 16진수 값이 없습니다. - 텍스트 스타일은
Theme.of(context).textTheme에서 가져옵니다. 원시 글꼴 크기를 사용한 인라인TextStyle이 없습니다. - 다크 모드 호환성이 확인되었습니다. 밝은 배경을 가정하지 않습니다.
- 간격 및 크기 조정은 매직 넘버가 아닌 일관된 디자인 토큰 또는 상수를 사용합니다.
빌드 메서드 복잡성:
-
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를 통해 다른 provider에 의존하는 provider는 허용됩니다. 순환 또는 지나치게 얽힌 체인만 플래그를 지정하세요. - 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의 효과 정리).
상태 모양 설계:
- 상호 배타적인 상태는 boolean 플래그(
isLoading,isError,hasData)가 아닌 sealed 유형, 공용체 변형 또는 솔루션의 내장 async 상태 유형(예: Riverpod의AsyncValue)을 사용합니다. - 모든 비동기 작업은 로딩, 성공, 오류를 별개의 상태로 모델링합니다.
- UI에서 모든 상태 변형이 완전히 처리됩니다. 조용히 무시되는 경우가 없어야 합니다.
- 오류 상태는 표시를 위해 오류 정보를 전달합니다. 로딩 상태는 오래된 데이터를 전달하지 않습니다.
- Nullable 데이터가 로딩 표시기로 사용되지 않습니다. 상태는 명시적입니다.
// 나쁨 — boolean 플래그 혼란으로 불가능한 상태가 발생할 수 있음
class UserState {
bool isLoading = false;
bool hasError = false; // isLoading && hasError가 표현 가능함!
User? user;
}
// 좋음 (불변 접근 방식) — sealed 유형으로 불가능한 상태를 표현 불가능하게 만듦
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);
}
// 좋음 (반응형 접근 방식) — 관찰 가능한 enum + 데이터, 변이는 반응형 API를 통해
// enum UserStatus { initial, loading, loaded, error }
// 솔루션의 관찰 가능/신호를 사용하여 상태와 데이터를 별도로 래핑
재빌드 최적화:
- 상태 소비자 위젯(Builder, Consumer, Observer, Obx, Watch 등)이 가능한 한 좁게 범위가 지정됩니다.
- Selector를 사용하여 모든 상태 방출 시가 아닌 특정 필드가 변경될 때만 재빌드됩니다.
-
const위젯을 사용하여 트리를 통한 재빌드 전파를 중지합니다. - 계산/파생 상태는 중복 저장되지 않고 반응적으로 계산됩니다.
구독 및 폐기:
- 모든 수동 구독(
.listen())은dispose()/close()에서 취소됩니다. - 스트림 컨트롤러는 더 이상 필요하지 않으면 닫힙니다.
- 타이머는 폐기 수명 주기에서 취소됩니다.
- 수동 구독보다 프레임워크 관리 수명 주기(선언적 빌더가
.listen()보다 선호)가 선호됩니다. - 비동기 콜백에서
setState전에mounted를 확인합니다. -
BuildContext는context.mounted(Flutter 3.7+)를 확인하지 않고await후에 사용되지 않습니다. 오래된 컨텍스트는 충돌을 유발합니다. - 위젯이 여전히 마운트되었는지 확인하지 않고 비동기 간격 후에 탐색, 대화 상자 또는 스캐폴드 메시지가 없습니다.
-
BuildContext는 싱글톤, 상태 관리자 또는 정적 필드에 저장되지 않습니다.
로컬 vs 글로벌 상태:
- 일시적인 UI 상태(체크박스, 슬라이더, 애니메이션)는 로컬 상태(
setState,ValueNotifier)를 사용합니다. - 공유 상태는 필요한 만큼만 위로 올려집니다. 과도하게 전역화되지 않습니다.
- 기능 범위의 상태는 기능이 더 이상 활성화되지 않을 때 적절히 폐기됩니다.
5. 성능
불필요한 재빌드:
-
setState()가 루트 위젯 수준에서 호출되지 않습니다. 상태 변경을 지역화합니다. -
const위젯을 사용하여 재빌드 전파를 중지합니다. -
RepaintBoundary가 독립적으로 다시 그리는 복잡한 하위 트리 주위에 사용됩니다. -
AnimatedBuilder의 child 매개변수가 애니메이션과 독립적인 하위 트리에 사용됩니다.
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생성자를 사용합니다. - Intrinsic 차원 위젯(
IntrinsicHeight,IntrinsicWidth)은 드물게 사용됩니다(추가 레이아웃 패스 발생).
6. 테스트
테스트 유형 및 기대 사항:
- 단위 테스트: 모든 비즈니스 로직(상태 관리자, 리포지토리, 유틸리티 함수)을 다룹니다.
- 위젯 테스트: 개별 위젯 동작, 상호 작용 및 시각적 출력을 다룹니다.
- 통합 테스트: 중요한 사용자 흐름을 종단 간 다룹니다.
- 골든 테스트: 디자인이 중요한 UI 구성 요소에 대한 픽셀 완벽 비교입니다.
적용 범위 목표:
- 비즈니스 로직에서 80% 이상의 라인 적용 범위를 목표로 합니다.
- 모든 상태 전환에 해당 테스트가 있습니다(로딩 → 성공, 로딩 → 오류, 재시도 등).
- 엣지 케이스가 테스트됩니다: 빈 상태, 오류 상태, 로딩 상태, 경계 값.
테스트 격리:
- 외부 종속성(API 클라이언트, 데이터베이스, 서비스)은 모의(mock)되거나 가짜(fake)로 대체됩니다.
- 각 테스트 파일은 정확히 하나의 클래스/단위를 테스트합니다.
- 테스트는 구현 세부 사항이 아닌 동작을 검증합니다.
- 스텁은 각 테스트에 필요한 동작만 정의합니다(최소 스터빙).
- 테스트 케이스 간에 공유되는 변경 가능한 상태가 없습니다.
위젯 테스트 품질:
-
pumpWidget및pump가 비동기 작업에 올바르게 사용됩니다. -
find.byType,find.text,find.byKey가 적절히 사용됩니다. - 타이밍에 의존하는 플래키 테스트가 없습니다.
pumpAndSettle또는 명시적인pump(Duration)을 사용합니다. - 테스트가 CI에서 실행되며 실패 시 병합이 차단됩니다.
7. 접근성
시맨틱 위젯:
-
Semantics위젯이 자동 레이블이 충분하지 않은 경우 화면 판독기 레이블을 제공하는 데 사용됩니다. - 순수 장식 요소에는
ExcludeSemantics가 사용됩니다. - 관련 위젯을 단일 접근 가능 요소로 결합하는 데
MergeSemantics가 사용됩니다. - 이미지에
semanticLabel속성이 설정됩니다.
화면 판독기 지원:
- 모든 대화형 요소는 포커스 가능하며 의미 있는 설명이 있습니다.
- 포커스 순서가 논리적입니다(시각적 읽기 순서를 따릅니다).
시각적 접근성:
- 텍스트 대비 배경의 명암비가 4.5:1 이상입니다.
- 탭 가능한 대상의 크기는 최소 48x48픽셀입니다.
- 색상이 상태의 유일한 표시기가 아닙니다(아이콘/텍스트를 함께 사용).
- 텍스트가 시스템 글꼴 크기 설정에 따라 조정됩니다.
상호 작용 접근성:
- 아무 작업도 하지 않는
onPressed콜백이 없습니다. 모든 버튼은 작업을 수행하거나 비활성화됩니다. - 오류 필드는 수정 사항을 제안합니다.
- 사용자가 데이터를 입력하는 동안 컨텍스트가 예기치 않게 변경되지 않습니다.
8. 플랫폼 특정 고려 사항
iOS/Android 차이점:
- 필요에 따라 플랫폼 적응형 위젯이 사용됩니다.
- 뒤로 가기 탐색이 올바르게 처리됩니다(Android 뒤로 가기 버튼, iOS 스와이프하여 뒤로 가기).
- 상태 표시줄 및 안전 영역이
SafeArea위젯을 통해 처리됩니다. - 플랫폼 특정 권한이
AndroidManifest.xml및Info.plist에 선언됩니다.
반응형 디자인:
- 반응형 레이아웃을 위해
LayoutBuilder또는MediaQuery가 사용됩니다. - 중단점이 일관되게 정의됩니다(폰, 태블릿, 데스크탑).
- 작은 화면에서 텍스트가 넘치지 않습니다.
Flexible,Expanded,FittedBox를 사용합니다. - 가로 방향이 테스트되거나 명시적으로 잠겨 있습니다.
- 웹 특정: 마우스/키보드 상호 작용이 지원되고 호버 상태가 있습니다.
9. 보안
안전한 저장소:
- 민감한 데이터는 안전한 저장소 메커니즘(flutter_secure_storage, 키체인, 키스토어)을 사용하며, SharedPreferences나 일반 텍스트 파일을 사용하지 않습니다.
- API 토큰, 비밀번호 및 기타 자격 증명이 소스 코드에 하드코딩되지 않습니다. 환경 변수 또는 빌드 구성에서 안전하게 로드됩니다.
- 앱이 포그라운드에서 백그라운드로 전환될 때 민감한 데이터가 메모리에 명확한 텍스트로 유지되지 않습니다. 가능하면 캐시를 지우거나 화면을 흐리게 처리합니다.
네트워크 보안:
- 모든 네트워크 요청이 HTTPS를 사용하며, HTTP로의 폴백이 없습니다.
- API 요청 및 응답이 적절히 검증됩니다. 서버의 응답을 신뢰하지 않습니다.
- 인증 토큰이 안전하게 저장되고 전송됩니다.
입력 검증:
- 사용자 입력이 적절히 삭제되고 검증됩니다. 텍스트 필드에 최대 길이 제한이 적용됩니다.
- HTML 또는 JavaScript 주입 공격을 방지하기 위해
dangerousRawHtml과 같은 위젯이 사용되지 않습니다(필요한 경우 신중하게 사용).
10. 종속성 및 패키지
-
pubspec.yaml이 사용되지 않는 종속성 없이 깔끔합니다. - 종속성 버전이 적절히 고정되거나(록 파일) 범위가 좁습니다.
- 사용된 패키지가 유지 관리되고 널리 사용됩니다(인기, 업데이트 빈도, 문제 수 확인).
- 라이선스 호환성이 확인되었습니다.
-
flutter pub outdated가 정기적으로 실행되어 사용되지 않는 패키지를 식별합니다.
11. 문서화
- 공개 API에 대한 문서 주석(
///)이 있습니다. - 복잡한 위젯 및 상태 관리자에 대한 간결한 설명이 제공됩니다.
- README 파일이 프로젝트 설정, 종속성 및 시작 지침을 설명합니다.
- 코드베이스의 특이한 결정이나 복잡한 논리에 대한 인라인 주석이 있습니다.
- 변경 로그가 유지 관리됩니다.
이 체크리스트는 포괄적이지만 완전한 것은 아닙니다. 조직의 특정 요구 사항에 맞게 조정하고 새로운 개발 패턴과 모범 사례를 반영하도록 정기적으로 업데이트하세요.