Effective Dart — 코딩 컨벤션
Dart 팀은 Effective Dart라는 공식 가이드를 제공합니다. 스타일, 문서화, 사용법, 설계 네 가지 영역을 다룹니다. 이 챕터에서는 실무에서 가장 중요한 규칙을 추려 정리합니다. 작은 컨벤션 차이가 팀 협업의 질을 크게 바꿉니다.
네이밍 규칙
Dart는 네 가지 표기법을 상황에 따라 씁니다.
| 표기법 | 형태 | 적용 대상 |
|---|---|---|
| UpperCamelCase | MyClass |
클래스, enum, typedef, 타입 매개변수 |
| lowerCamelCase | myVariable |
변수, 함수, 매개변수, 상수 |
| lowercase_with_underscores | my_package |
라이브러리, 패키지, 폴더, 파일명 |
| SCREAMING_CAPS | MAX_SIZE |
Dart에서는 사용하지 않음 |
// 클래스 — UpperCamelCase
class UserRepository {}
class HttpException implements Exception {}
typedef Predicate<T> = bool Function(T value);
// 변수, 함수 — lowerCamelCase
var userName = 'Alice';
const maxRetryCount = 3; // 상수도 lowerCamelCase
void fetchUserData() {}
// 파일명 — lowercase_with_underscores
// user_repository.dart
// http_exception.dart
// user_service_test.dart
// enum — UpperCamelCase (값은 lowerCamelCase)
enum ConnectionState { connecting, connected, disconnected }
좋은 이름 짓기
DO: 명확하고 설명적인 이름 사용
// 나쁨
int d = 0;
List<User> ul = [];
void proc(User u) {}
// 좋음
int daysSinceLastLogin = 0;
List<User> activeUsers = [];
void processUserRegistration(User user) {}
AVOID: 타입 정보를 이름에 포함
// 나쁨 (헝가리안 표기법)
String strName = 'Alice';
List<User> userList = [];
Map<String, int> scoreMap = {};
// 좋음
String name = 'Alice';
List<User> users = [];
Map<String, int> scores = {};
PREFER: 동사로 시작하는 함수 이름
// 동작을 명확히 표현
void saveUser(User user) {}
Future<User> fetchUser(int id) async {}
bool isValidEmail(String email) {}
List<User> filterActiveUsers(List<User> users) {}
// getter는 명사형
String get fullName => '$firstName $lastName';
bool get isEmpty => _items.isEmpty;
타입 선언
DO: 공개 API에는 타입을 명시
// 나쁨 — 공개 함수에서 타입 생략
fetchUser(id) async {
// ...
}
// 좋음
Future<User> fetchUser(int id) async {
// ...
}
PREFER: 지역 변수는 타입 추론 활용
// 필요 이상으로 타입 명시 (중복)
Map<String, List<User>> groupedUsers = groupBy(users, (u) => u.role);
// 간결하게 추론
final groupedUsers = groupBy(users, (u) => u.role);
AVOID: dynamic 사용
// 나쁨 — 타입 안전성 상실
dynamic parseResponse(String json) {}
// 좋음
Map<String, dynamic> parseResponse(String json) {}
// 또는 제네릭
T parseResponse<T>(String json, T Function(Map<String, dynamic>) fromJson) {}
문서화 주석 (///)
공개 API에는 반드시 문서화 주석을 답니다. ///로 시작하는 doc 주석은 dart doc이 API 문서를 생성할 때 사용합니다.
/// 사용자 정보를 관리하는 서비스 클래스.
///
/// 데이터베이스에서 사용자를 조회하고, 생성하고, 수정합니다.
/// 모든 메서드는 비동기로 동작합니다.
///
/// 예제:
/// ```dart
/// final service = UserService(database: db);
/// final user = await service.findById(1);
/// print(user.name);
/// ```
class UserService {
/// 주어진 [id]에 해당하는 사용자를 반환합니다.
///
/// 사용자를 찾지 못하면 [UserNotFoundException]을 던집니다.
///
/// - [id]: 조회할 사용자 ID. 1 이상의 양수여야 합니다.
Future<User> findById(int id) async {
// ...
}
/// 새 사용자를 생성하고 반환합니다.
///
/// [name]과 [email]은 비어 있으면 안 됩니다.
/// 이미 존재하는 이메일이면 [DuplicateEmailException]을 던집니다.
Future<User> create({required String name, required String email}) async {
// ...
}
}
규칙:
- 첫 문장은 마침표로 끝나는 한 줄 요약입니다.
- 두 번째 단락부터 상세 설명을 씁니다.
- 매개변수는
[name]형식으로 참조합니다. - 예외는 명시적으로 기록합니다.
API 설계 원칙
DO: 입력 검증은 명시적으로
// 나쁨 — 조용히 실패
void setAge(int age) {
if (age < 0) return; // 오류를 숨김
_age = age;
}
// 좋음 — 즉시 실패(fail-fast)
void setAge(int age) {
if (age < 0) throw ArgumentError.value(age, 'age', 'must be non-negative');
_age = age;
}
PREFER: 예외보다 nullable 반환
// 예외가 정상 흐름의 일부라면 nullable 반환
User? findByEmail(String email) {
return _users.cast<User?>().firstWhere(
(u) => u?.email == email,
orElse: () => null,
);
}
// 예외는 진짜 예외적인 상황에만
Future<User> findById(int id) async {
final user = await _db.query(id);
if (user == null) throw UserNotFoundException(id);
return user;
}
DO: 컬렉션을 빈 값으로 초기화
// 나쁨
List<User>? users;
// 좋음 — null 대신 빈 컬렉션
List<User> users = [];
PREFER: 불변 객체 설계
// 나쁨 — 가변 객체
class Config {
String host = 'localhost';
int port = 8080;
}
// 좋음 — 불변 객체
class Config {
final String host;
final int port;
const Config({this.host = 'localhost', this.port = 8080});
Config copyWith({String? host, int? port}) {
return Config(
host: host ?? this.host,
port: port ?? this.port,
);
}
}
코드 정리 습관
사용하지 않는 import 제거
# VS Code: Cmd+Shift+P → "Organize Imports"# 또는 저장 시 자동 정리 설정
// settings.json{ "editor.codeActionsOnSave": { "source.organizeImports": "explicit" }}
일관된 const 사용
// 가능하면 const 사용 — 성능 향상
const padding = EdgeInsets.all(16);
const colors = [Colors.red, Colors.blue, Colors.green];
// 컴파일 타임에 알 수 있는 객체는 const
const config = Config(host: 'localhost', port: 8080);
불필요한 this 생략
// 나쁨 — this 불필요
class User {
String name;
User(String name) {
this.name = name; // this 불필요
}
}
// 좋음 — 초기화 매개변수 사용
class User {
String name;
User(this.name);
}
실전 체크리스트
팀 코드 리뷰 전 확인합니다.
- 클래스와 공개 함수에
///문서화 주석이 있습니다. - 파일명이
lowercase_with_underscores형식입니다. - 변수명이 충분히 설명적입니다 (한 글자 변수는 루프 카운터만).
dynamic타입을 최소화했습니다.dart format .을 실행해 포맷이 정리됐습니다.dart analyze에서 경고가 없습니다.- 공개 컬렉션 API는 null 대신 빈 컬렉션을 반환합니다.
- 불변 객체 설계를 우선했습니다.
정리
이번 챕터에서 다룬 내용입니다.
- UpperCamelCase(클래스), lowerCamelCase(변수/함수), lowercase_with_underscores(파일)을 일관되게 사용합니다.
- 공개 API에는
///문서화 주석과 명확한 타입 선언이 필수입니다. dynamic대신 제네릭, 명확한 타입, sealed class로 타입 안전성을 확보합니다.- 가변 객체보다 불변 객체를 설계의 기본으로 삼습니다.
dart format과dart analyze를 개발 워크플로우에 통합합니다.
PART 03이 끝났습니다. 이제 개발 환경, 핵심 문법, 프로젝트 구조의 기반이 마련됐습니다. 다음 PART에서는 실전 CLI 도구를 직접 만들어봅니다.