iBetter Books
수정

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 formatdart analyze를 개발 워크플로우에 통합합니다.

PART 03이 끝났습니다. 이제 개발 환경, 핵심 문법, 프로젝트 구조의 기반이 마련됐습니다. 다음 PART에서는 실전 CLI 도구를 직접 만들어봅니다.