Effective Dart: 스타일(Style)

Effective Dart: 스타일(Style)

일관되고 읽기 쉬운 코드를 위한 형식화와 명명 규칙을 살펴볼게요.

출처: 원문

본문

좋은 코드의 놀라울 만큼 중요한 부분 중 하나가 좋은 스타일이에요. 일관된 명명, 정렬, 형식화는 같은 코드가 같게 보이도록 해요. 이것은 우리 대부분이 시각 시스템에 가진 강력한 패턴 매칭 하드웨어를 활용해요. Dart 전체 생태계에서 일관된 스타일을 사용한다면, 우리 모두가 서로의 코드에서 배우고 서로의 코드에 기여하기 더 쉬워져요.

식별자(Identifiers)

Dart에는 세 가지 종류의 식별자가 있어요.

  • UpperCamelCase 이름은 첫 번째 단어를 포함해 각 단어의 첫 글자를 대문자로 써요.
  • lowerCamelCase 이름은 첫 번째 단어(약어더라도 항상 소문자)를 제외한 각 단어의 첫 글자를 대문자로 써요.
  • lowercase_with_underscores 이름은 약어도 포함해 소문자만 사용하고 단어를 _로 구분해요.

DO: 타입 이름은 UpperCamelCase로 지어요

린터 규칙: camel_case_types

클래스, enum 타입, typedef, 타입 매개변수는 각 단어(첫 단어 포함)의 첫 글자를 대문자로 하고 구분자를 사용하지 않아야 해요.

// 좋음
class SliderMenu { ... }

class HttpRequest { ... }

typedef Predicate<T> = bool Function(T value);

이것은 메타데이터 애너테이션에 사용될 의도인 클래스도 포함해요.

// 좋음
class Foo {
  const Foo([Object? arg]);
}

@Foo(anArg)
class A { ... }

@Foo()
class B { ... }

애너테이션 클래스의 생성자가 매개변수를 받지 않는다면, 그에 대한 별도의 lowerCamelCase 상수를 만들고 싶을 수도 있어요.

// 좋음
const foo = Foo();

@foo
class C { ... }

DO: 확장(extension) 이름은 UpperCamelCase로 지어요

린터 규칙: camel_case_extensions

타입과 마찬가지로 확장도 각 단어(첫 단어 포함)의 첫 글자를 대문자로 하고 구분자를 사용하지 않아야 해요.

// 좋음
extension MyFancyList<T> on List<T> { ... }

extension SmartIterable<T> on Iterable<T> { ... }

DO: 패키지, 디렉터리, 소스 파일 이름은 lowercase_with_underscores로 지어요

린터 규칙: file_names, package_names

일부 파일 시스템은 대소문자를 구분하지 않기 때문에, 많은 프로젝트가 파일 이름을 모두 소문자로 요구해요. 구분 문자를 사용하면 그 형태에서도 이름을 읽기 쉽게 유지할 수 있어요. 밑줄을 구분자로 사용하면 이름이 여전히 유효한 Dart 식별자임을 보장해요. 이는 나중에 언어가 심볼릭 import를 지원할 때 유용할 수 있어요.

// 좋음
my_package
└─ lib
   └─ file_system.dart
   └─ slider_menu.dart
// 나쁨
mypackage
└─ lib
   └─ file-system.dart
   └─ SliderMenu.dart

DO: import 접두사 이름은 lowercase_with_underscores로 지어요

린터 규칙: library_prefixes

// 좋음
import 'dart:math' as math;

import 'package:angular_components/angular_components.dart' as angular_components;
import 'package:js/js.dart' as js;
// 나쁨
import 'dart:math' as Math;

import 'package:angular_components/angular_components.dart' as angularComponents;
import 'package:js/js.dart' as JS;

DO: 그 외 다른 식별자는 lowerCamelCase로 지어요

린터 규칙: non_constant_identifier_names

클래스 멤버, 최상위 정의, 변수, 매개변수, 이름 붙은 매개변수는 첫 단어를 제외한 각 단어의 첫 글자를 대문자로 하고 구분자를 사용하지 않아야 해요.

// 좋음
var count = 3;

HttpRequest httpRequest;

void align(bool clearItems) {
  // ...
}

PREFER: 상수 이름에는 lowerCamelCase를 사용해요

린터 규칙: constant_identifier_names

새 코드에서는 enum 값도 포함해 상수 변수에 lowerCamelCase를 사용해 주세요.

// 좋음
const pi = 3.14;
const defaultTimeout = 1000;
final urlScheme = RegExp('^([a-z]+):');

class Dice {
  static final numberGenerator = Random();
}
// 나쁨
const PI = 3.14;
const DefaultTimeout = 1000;
final URL_SCHEME = RegExp('^([a-z]+):');

class Dice {
  static final NUMBER_GENERATOR = Random();
}

다음 경우처럼 기존 코드와의 일관성을 위해 SCREAMING_CAPS를 사용할 수 있어요.

  • 이미 SCREAMING_CAPS를 사용하는 파일이나 라이브러리에 코드를 추가할 때
  • Java 코드와 평행한 Dart 코드를 생성할 때 – 예를 들어 protobuf에서 생성된 enum 타입

참고: 우리는 처음에 상수에 Java의 SCREAMING_CAPS 스타일을 사용했어요. 여러 이유로 바꿨어요.

  • SCREAMING_CAPS는 특히 CSS 색상 같은 enum 값에서 나빠 보여요.
  • 상수는 종종 final 비-const 변수로 바뀌는데, 그러면 이름 변경이 필요해져요.
  • enum 타입에 정의된 values 프로퍼티는 const이고 소문자예요.

DO: 두 글자보다 긴 약어와 준말은 단어처럼 첫 글자를 대문자로 써요

대문자 약어는 읽기 어렵고, 인접한 여러 약어는 모호한 이름으로 이어질 수 있어요. 예를 들어 HTTPSFTP라는 식별자가 주어졌을 때 독자는 그것이 HTTPS FTP를 뜻하는지 HTTP SFTP를 뜻하는지 알 수 없어요. 이런 문제를 피하려면 대부분의 약어와 준말을 일반 단어처럼 대문자화해 주세요. 이 식별자는 전자를 뜻한다면 HttpsFtp, 후자를 뜻한다면 HttpSftp가 될 거예요.

두 글자 약어와 준말은 예외예요. 영어에서 두 글자가 모두 대문자라면, 식별자에서도 둘 다 대문자로 유지해야 해요. 그렇지 않으면 단어처럼 대문자화해 주세요.

// 좋음
// 두 글자보다 길어서 항상 단어처럼:
Http             // "hypertext transfer protocol"
Nasa             // "national aeronautics and space administration"
Uri              // "uniform resource identifier"
Esq              // "esquire"
Ave              // "avenue"

// 두 글자, 영어로 대문자라 식별자에서도 대문자:
ID               // "identifier"
TV               // "television"
UI               // "user interface"

// 두 글자, 영어로 대문자가 아니라 식별자에서 단어처럼:
Mr               // "mister"
St               // "street"
Rd               // "road"
// 나쁨
HTTP             // "hypertext transfer protocol"
NASA             // "national aeronautics and space administration"
URI              // "uniform resource identifier"
esq              // "esquire"
ave              // "avenue"

Id               // "identifier"
Tv               // "television"
Ui               // "user interface"

MR               // "mister"
ST               // "street"
RD               // "road"

어떤 형태의 약어라도 lowerCamelCase 식별자 시작 부분에 오면, 약어는 모두 소문자여야 해요.

var httpConnection = connect();
var tvSet = Television();
var mrRogers = 'hello, neighbor';

PREFER: 사용하지 않는 콜백 매개변수에는 와일드카드를 사용해요

때로 콜백 함수의 타입 시그니처가 매개변수를 요구하지만, 콜백 구현은 그 매개변수를 사용하지 않아요. 이런 경우 매개변수 이름을 _로 짓는 것이 관용적이에요. 이는 비바인딩(non-binding) 와일드카드 변수를 선언해요.

// 좋음
futureOfVoid.then((_) {
  print('Operation complete.');
});

와일드카드 변수는 비바인딩이기 때문에, 여러 개의 사용하지 않는 매개변수를 모두 _로 이름 지을 수 있어요.

// 좋음
.onError((_, _) {
  print('Operation failed.');
});

이 지침은 익명이면서 지역인 함수에만 적용돼요. 이런 함수는 보통 그 사용하지 않는 매개변수가 무엇을 나타내는지 명확한 맥락에서 즉시 사용돼요. 반면 최상위 함수와 메서드 선언에는 그런 맥락이 없으므로, 매개변수가 사용되지 않더라도 각 매개변수가 무엇을 위한 것인지 명확하도록 이름을 지어야 해요.

버전 참고: 비바인딩 와일드카드 변수 선언은 언어 버전이 최소 3.7이어야 해요. 이전 언어 버전에서는 _____ 같은 추가 밑줄을 사용해 이름 충돌을 우회해요. 그것들을 사용하지 못하게 하고 나중에 와일드카드로의 마이그레이션을 단순화하려면 no_wildcard_variable_uses 린트를 활성화해 주세요. 이 관례에서 와일드카드 변수로 마이그레이션하는 데 도움을 받으려면 unnecessary_underscores 린트를 활성화해 주세요.

DON'T: 비공개가 아닌 식별자에 밑줄 접두사를 쓰지 마세요

Dart는 멤버와 최상위 선언을 비공개로 표시하기 위해 식별자의 앞 밑줄을 사용해요. 이는 사용자들이 앞 밑줄을 그런 종류의 선언 중 하나와 연관시키도록 훈련시켜요. 그들은 "_"를 보고 "비공개"라고 생각해요.

지역 변수, 매개변수, 지역 함수, 라이브러리 접두사에는 "비공개" 개념이 없어요. 이것들 중 하나의 이름이 밑줄로 시작하면 독자에게 혼란스러운 신호를 보내요. 그런 이름에서는 앞 밑줄을 사용하지 않아 이런 혼란을 피해 주세요.

DON'T: 접두 글자를 쓰지 마세요

헝가리안 표기법과 그 밖의 방식들은 컴파일러가 코드를 이해하는 데 별 도움이 되지 않던 BCPL 시대에 생겨났어요. Dart는 선언의 타입, 범위, 가변성, 그 밖의 특성들을 알려줄 수 있기 때문에, 그런 특성들을 식별자 이름에 인코딩할 이유가 없어요.

// 좋음
defaultTimeout
// 나쁨
kDefaultTimeout

DON'T: 라이브러리에 명시적으로 이름을 붙이지 마세요

library 지시문에 이름을 붙이는 것은 기술적으로 가능하지만 레거시 기능이며 권장되지 않아요. Dart는 각 라이브러리의 경로와 파일 이름을 기반으로 고유한 태그를 생성해요. 라이브러리에 이름을 붙이면 이 생성된 URI를 덮어써요. URI가 없으면 도구가 해당하는 기본 라이브러리 파일을 찾기 더 어려워질 수 있어요.

// 나쁨
library my_library;
// 좋음
/// A really great test library.
@TestOn('browser')
library;

정렬(Ordering)

파일의 서문을 깔끔하게 유지하려면 지시문이 나타나야 하는 정해진 순서가 있어요. 각 "섹션"은 빈 줄로 구분해야 해요.

하나의 린터 규칙이 모든 정렬 지침을 처리해요. directives_ordering이에요.

DO: dart: import를 다른 import보다 먼저 배치해요

린터 규칙: directives_ordering

// 좋음
import 'dart:async';
import 'dart:collection';

import 'package:bar/bar.dart';
import 'package:foo/foo.dart';

DO: package: import를 상대(relative) import보다 먼저 배치해요

린터 규칙: directives_ordering

// 좋음
import 'package:bar/bar.dart';
import 'package:foo/foo.dart';

import 'util.dart';

DO: 모든 import 뒤의 별도 섹션에서 export를 지정해요

린터 규칙: directives_ordering

// 좋음
import 'src/error.dart';
import 'src/foo_bar.dart';

export 'src/error.dart';
// 나쁨
import 'src/error.dart';
export 'src/error.dart';
import 'src/foo_bar.dart';

DO: 섹션을 알파벳순으로 정렬해요

린터 규칙: directives_ordering

// 좋음
import 'package:bar/bar.dart';
import 'package:foo/foo.dart';

import 'foo.dart';
import 'foo/foo.dart';
// 나쁨
import 'package:foo/foo.dart';
import 'package:bar/bar.dart';

import 'foo/foo.dart';
import 'foo.dart';

형식(Formatting)

많은 언어처럼 Dart는 공백을 무시해요. 하지만 사람은 그렇지 않아요. 일관된 공백 스타일을 가지면 사람 독자가 컴파일러와 같은 방식으로 코드를 보도록 보장하는 데 도움이 돼요.

DO: dart format을 사용해 코드를 형식화해요

형식화는 지루한 작업이며 특히 리팩터링 중에 시간이 많이 걸려요. 다행히 그것에 대해 걱정할 필요가 없어요. 우리는 dart format이라는 정교한 자동 코드 포맷터를 제공하며 그것이 그 일을 해줘요. Dart의 공식 공백 처리 규칙은 dart format이 만들어내는 결과에요. 포맷터 FAQ는 그것이 강제하는 스타일 선택에 대해 더 많은 통찰을 줄 수 있어요.

나머지 형식화 지침은 dart format이 해결할 수 없는 몇 가지 것들을 위한 거예요.

CONSIDER: 코드를 formatter에 친화적으로 바꾸는 것을 고려해요

포맷터는 던져진 코드로 최선을 다하지만 기적을 만들 수는 없어요. 코드에 특히 긴 식별자, 깊게 중첩된 표현식, 다양한 종류의 연산자가 섞여 있다면, 형식화된 출력도 여전히 읽기 어려울 수 있어요.

그럴 때 코드를 재구성하거나 단순화해 주세요. 지역 변수 이름을 줄이거나 표현식을 새로운 지역 변수로 끌어올리는 것을 고려해 보세요. 즉, 코드를 손으로 형식화하면서 더 읽기 좋게 만들려고 했을 때 할 것과 같은 수정을 하면 돼요. dart format을 때로는 반복적으로 협력해 아름다운 코드를 만들어내는 파트너십으로 생각해 주세요.

PREFER: 80자 이하의 줄을 선호해요

린터 규칙: lines_longer_than_80_chars

가독성 연구에 따르면 긴 줄은 다음 줄의 시작으로 눈이 더 멀리 이동해야 하기 때문에 읽기 더 어려워요. 신문과 잡지가 여러 개의 텍스트 컬럼을 사용하는 이유가 그것이에요.

정말 80자보다 긴 줄을 원한다면, 여러분의 코드가 아마 너무 장황하고 조금 더 간결해질 수 있다는 것이 우리의 경험이에요. 주된 범인은 보통 VeryLongCamelCaseClassNames예요. "그 타입 이름의 각 단어가 저에게 중요한 무엇을 알려주거나 이름 충돌을 막아주나요?"라고 스스로 물어보세요. 그렇지 않다면, 생략하는 것을 고려해 보세요.

dart format은 기본이 80자 이하지만 기본값을 구성할 수 있다는 점에 유의해 주세요. 긴 문자열 리터럴을 80 컬럼에 맞추기 위해 분리하지는 않으므로, 그것은 수동으로 해야 해요.

예외: 주석이나 문자열에서 URI나 파일 경로가 발생하면(보통 import나 export에서), 줄이 80자를 넘게 해도 그대로 둘 수 있어요. 이렇게 하면 소스 파일에서 경로를 검색하기 더 쉬워져요.

예외: 여러 줄 문자열은 줄바꿈이 문자열 안에서 의미가 있고 줄을 더 짧게 분리하면 프로그램을 바꿀 수 있기 때문에 80자보다 긴 줄을 포함할 수 있어요.

DO: 모든 흐름 제어 문장에 중괄호를 사용해요

린터 규칙: curly_braces_in_flow_control_structures

이렇게 하면 매달린 else(dangling else) 문제를 피할 수 있어요.

// 좋음
if (isWeekDay) {
  print('Bike to work!');
} else {
  print('Go dancing or read a book!');
}

예외: else 절이 없는 if 문이 한 줄에 완전히 들어맞는다면, 원하면 중괄호를 생략할 수 있어요.

// 좋음
if (arg == null) return defaultValue;

그러나 본문이 다음 줄로 넘어가면 중괄호를 사용해 주세요.

// 좋음
if (overflowChars != other.overflowChars) {
  return overflowChars < other.overflowChars;
}
// 나쁨
if (overflowChars != other.overflowChars)
  return overflowChars < other.overflowChars;

더 알아보기