Effective Dart

Effective Dart

일관되고 유지보수하기 쉬우며 효율적인 Dart 라이브러리를 만들기 위한 모범 사례를 소개해 드릴게요.

출처: 원문

본문

지난 몇 년 동안 우리는 수많은 Dart 코드를 작성하면서 무엇이 잘 작동하고 무엇이 그렇지 않은지에 대해 많이 배웠어요. 여러분도 일관되고, 견고하며, 빠른 코드를 작성할 수 있도록 이 경험을 공유해 드릴게요. 여기에는 두 가지 핵심 주제가 있어요.

  • 일관되게 작성해요. 형식이나 대소문자 같은 것에 대해 어떤 것이 더 나은지는 주관적이라 결론을 내기 어려워요. 다만 우리가 확실히 아는 것은 일관성이 객관적으로 도움이 된다는 점이에요. 두 코드 조각이 다르게 보인다면, 그건 어떤 의미 있는 부분에서 실제로 다르기 때문이어야 해요. 눈에 띄는 코드 한 조각이 있다면, 그건 유용한 이유가 있어서 눈에 띄는 것이어야 해요.
  • 간결하게 작성해요. Dart는 익숙함을 목표로 설계되어 C, Java, JavaScript 등 다른 언어들의 많은 문장과 표현을 물려받았어요. 그러나 우리가 Dart를 만든 이유는 그 언어들이 제공하는 것보다 개선할 여지가 많기 때문이에요. 문자열 보간(interpolation)부터 초기화 형식 인자(initializing formals)까지, 여러분의 의도를 더 간단하고 쉽게 표현할 수 있도록 많은 기능을 추가했어요. 어떤 것을 표현하는 방법이 여러 가지라면, 보통 가장 간결한 방법을 고르는 게 좋아요. 이것은 프로그램 전체를 한 줄에 몰아넣는 코드 골프를 하라는 뜻이 아니에요. 목표는 밀도 있는 코드가 아니라 경제적인 코드예요.

가이드 소개

지침을 소화하기 쉽게 몇 개의 별도 페이지로 나누어 두었어요.

  • 스타일 가이드 – 코드를 배치하고 구성하는 규칙, 적어도 dart format이 처리해 주지 못하는 부분을 정의해요. 스타일 가이드는 식별자를 어떻게 형식화하는지(camelCase, using_underscores 등)도 지정해요.
  • 문서화 가이드 – 주석 안에 무엇을 넣어야 하는지에 대해 알아야 할 모든 것을 알려줘요. 문서 주석(doc comment)과 평범한 일반 코드 주석 둘 다요.
  • 사용 가이드 – 언어 기능을 가장 잘 활용해 동작을 구현하는 방법을 가르쳐줘요. 문장이나 표현에 들어가는 내용이라면 여기서 다뤄요.
  • 디자인 가이드 – 가장 부드러운 가이드지만 범위가 가장 넓어요. 라이브러리를 위해 일관되고 사용하기 좋은 API를 설계하는 데 배운 것들을 다뤄요. 타입 시그니처나 선언에 들어가는 내용이라면 이 가이드에서 다뤄요.

모든 지침의 링크는 요약(summary)에서 볼 수 있어요.

가이드 읽는 방법

각 가이드는 몇 개의 섹션으로 나뉘어요. 섹션은 지침 목록을 담고 있어요. 각 지침은 다음 단어 중 하나로 시작해요.

  • DO(하세요) 지침은 항상 따라야 하는 관행을 설명해요. 이 지침에서 벗어날 타당한 이유는 거의 없어요.
  • DON'T(하지 마세요) 지침은 그 반대, 즉 거의 좋은 생각이 아닌 것들이에요. 우리는 역사적 짐이 적기 때문에 다른 언어들만큼 이런 지침이 많지 않기를 바래요.
  • PREFER(선호하세요) 지침은 따라야 하는 관행이에요. 다만 그렇게 하지 않는 것이 타당한 상황도 있을 수 있어요. 그럴 때는 지침을 무시했을 때의 모든 영향까지 이해하고 행동하도록 해요.
  • AVOID(피하세요) 지침은 "선호"의 반대, 즉 하지 말아야 하지만 드물게 타당한 이유가 있을 수 있는 것들이에요.
  • CONSIDER(고려하세요) 지침은 상황, 선례, 그리고 자신의 선호에 따라 따를 수도 있고 안 따를 수도 있는 관행이에요.

일부 지침은 규칙이 적용되지 않는 예외를 설명해요. 예외가 나열되어 있어도 완전하지 않을 수 있으니, 다른 경우에는 여전히 자신의 판단을 써야 할 수도 있어요.

이렇게 들으면 신발끈을 제대로 묶지 않으면 경찰이 문을 두드릴 것처럼 느껴질 수 있지만, 그렇게 심각한 건 아니에요. 여기 있는 지침 대부분은 상식이고 우리 모두 합리적인 사람들이에요. 목표는 언제나 읽기 좋고, 명확하며, 유지보수하기 쉬운 코드예요.

Dart 분석기(analyzer)는 이런 지침과 그 외 다른 지침을 따르는 좋고 일관된 코드를 작성하도록 돕는 린터(linter)를 제공해요. 지침을 따르는 데 도움이 되는 린터 규칙이 하나 이상 있으면, 그 지침은 해당 규칙에 연결돼요. 링크는 다음과 같은 형식을 사용해요.

린터 규칙: unnecessary_getters_setters

린터 사용법을 배우려면 linter 규칙 활성화 방법과 linter 규칙 목록을 참고해요.

용어집

지침을 간결하게 유지하기 위해, 서로 다른 Dart 구조를 가리키는 몇 가지 약칭 용어를 사용해요.

  • 라이브러리 멤버(library member) 는 최상위 필드, 게터, 세터, 또는 함수예요. 기본적으로 타입이 아닌 최상위에 있는 모든 것이에요.
  • 클래스 멤버(class member) 는 클래스 안에 선언된 생성자, 필드, 게터, 세터, 함수, 또는 연산자예요. 클래스 멤버는 인스턴스 멤버일 수도 있고 정적(static) 멤버일 수도 있으며, 추상적이거나 구체적일 수 있어요.
  • 멤버(member) 는 라이브러리 멤버 또는 클래스 멤버 중 하나예요.
  • 변수(variable) 는 일반적으로 사용될 때 최상위 변수, 매개변수, 지역 변수를 가리켜요. 정적 필드나 인스턴스 필드는 포함하지 않아요.
  • 타입(type) 은 이름 붙은 타입 선언, 즉 클래스, typedef, 또는 enum이에요.
  • 프로퍼티(property) 는 최상위 변수, 게터(클래스 안 또는 최상위, 인스턴스 또는 정적), 세터(동일), 또는 필드(인스턴스 또는 정적)예요. 대략 "필드처럼 생긴" 이름 붙은 구조를 가리켜요.

전체 규칙 요약

스타일(Style)

식별자(Identifiers)

  • DO: 타입 이름은 UpperCamelCase로 지어요.
  • DO: 확장(extension) 이름은 UpperCamelCase로 지어요.
  • DO: 패키지, 디렉터리, 소스 파일 이름은 lowercase_with_underscores로 지어요.
  • DO: import 접두사 이름은 lowercase_with_underscores로 지어요.
  • DO: 그 외 다른 식별자는 lowerCamelCase로 지어요.
  • PREFER: 상수 이름에는 lowerCamelCase를 사용해요.
  • DO: 두 글자보다 긴 약어와 준말은 단어처럼 첫 글자를 대문자로 써요.
  • PREFER: 사용하지 않는 콜백 매개변수에는 와일드카드를 사용해요.
  • DON'T: 비공개가 아닌 식별자에 밑줄 접두사를 쓰지 마세요.
  • DON'T: 접두 글자를 쓰지 마세요.
  • DON'T: 라이브러리에 명시적으로 이름을 붙이지 마세요.

정렬(Ordering)

  • DO: dart: import를 다른 import보다 먼저 배치해요.
  • DO: package: import를 상대(relative) import보다 먼저 배치해요.
  • DO: 모든 import 뒤의 별도 섹션에서 export를 지정해요.
  • DO: 섹션을 알파벳순으로 정렬해요.

형식(Formatting)

  • DO: dart format을 사용해 코드를 형식화해요.
  • CONSIDER: 코드를 formatter에 친화적으로 바꾸는 것을 고려해요.
  • PREFER: 80자 이하의 줄을 선호해요.
  • DO: 모든 흐름 제어 문장에 중괄호를 사용해요.

문서화(Documentation)

주석(Comments)

  • DO: 주석을 문장처럼 형식화해요.
  • DON'T: 문서화에 블록 주석을 사용하지 마세요.

문서 주석(Doc comments)

  • DO: 멤버와 타입을 문서화할 때 /// 문서 주석을 사용해요.
  • PREFER: 공개 API에 문서 주석을 작성해요.
  • CONSIDER: 라이브러리 수준 문서 주석 작성도 고려해요.
  • CONSIDER: 비공개 API에 문서 주석 작성도 고려해요.
  • DO: 문서 주석을 한 문장 요약으로 시작해요.
  • DO: 문서 주석의 첫 문장을 별도의 문단으로 분리해요.
  • AVOID: 주변 맥락과 중복을 피해요.
  • PREFER: 함수나 메서드의 주요 목적이 부수 효과(side effect)라면 3인칭 동사로 주석을 시작해요.
  • PREFER: 불리언이 아닌 변수나 프로퍼티 주석은 명사구로 시작해요.
  • PREFER: 불리언 변수나 프로퍼티 주석은 "Whether" 뒤에 명사구나 동명사구로 시작해요.
  • PREFER: 함수나 메서드의 주요 목적이 값 반환이라면 명사구나 비명령형 동사구를 사용해요.
  • DON'T: 프로퍼티의 게터와 세터 둘 다에 문서를 작성하지 마세요.
  • PREFER: 라이브러리나 타입 주석은 명사구로 시작해요.
  • CONSIDER: 문서 주석에 코드 샘플을 포함하는 것을 고려해요.
  • DO: 문서 주석에서 대괄호를 사용해 범위 안의 식별자를 가리켜요.
  • DO: 매개변수, 반환 값, 예외를 산문(prose)으로 설명해요.
  • DO: 메타데이터 애너테이션 앞에 문서 주석을 배치해요.

마크다운(Markdown)

  • AVOID: 마크다운을 과하게 사용하지 마세요.
  • AVOID: 형식화에 HTML을 사용하지 마세요.
  • PREFER: 코드 블록에는 백틱 fence를 사용해요.

작문(Writing)

  • PREFER: 간결함을 선호해요.
  • AVOID: 명백하지 않은 한 약어와 준말을 사용하지 마세요.
  • PREFER: 멤버의 인스턴스를 가리킬 때 "the" 대신 "this"를 사용해요.

사용(Usage)

라이브러리(Libraries)

  • DO: part of 지시문에 문자열을 사용해요.
  • DON'T: 다른 패키지의 src 디렉터리 안에 있는 라이브러리를 import하지 마세요.
  • DON'T: import 경로가 lib 안팎으로 침범하지 않게 해요.
  • PREFER: 상대 import 경로를 선호해요.

Null

  • DON'T: 변수를 명시적으로 null로 초기화하지 마세요.
  • DON'T: 명시적 기본값 null을 사용하지 마세요.
  • DON'T: 동등 비교 연산에 truefalse를 사용하지 마세요.
  • AVOID: 초기화 여부를 확인해야 한다면 late 변수를 사용하지 마세요.
  • CONSIDER: nullable 타입을 사용할 때 타입 승격이나 null 검사 패턴을 고려해요.

문자열(Strings)

  • DO: 인접한 문자열을 사용해 문자열 리터럴을 이어 붙여요.
  • PREFER: 문자열과 값을 조합할 때 보간(interpolation)을 사용해요.
  • AVOID: 필요하지 않을 때 보간에서 중괄호를 사용하지 마세요.

컬렉션(Collections)

  • DO: 가능하면 컬렉션 리터럴을 사용해요.
  • DON'T: 컬렉션이 비어 있는지 확인할 때 .length를 사용하지 마세요.
  • AVOID: 함수 리터럴과 함께 Iterable.forEach()를 사용하지 마세요.
  • DON'T: 결과의 타입을 바꾸려는 게 아니라면 List.from()을 사용하지 마세요.
  • DO: 컬렉션을 타입별로 필터링할 때 whereType()을 사용해요.
  • DON'T: 가까운 연산으로 처리될 수 있을 때 cast()를 사용하지 마세요.
  • AVOID: cast()를 사용하지 마세요.

함수(Functions)

  • DO: 함수에 이름을 바인딩할 때 함수 선언을 사용해요.
  • DON'T: tear-off로 충분할 때 람다를 만들지 마세요.

변수(Variables)

  • DO: 지역 변수의 varfinal에 일관된 규칙을 따르세요.
  • AVOID: 계산할 수 있는 것을 저장하지 마세요.

멤버(Members)

  • DON'T: 필드를 불필요하게 게터와 세터로 감싸지 마세요.
  • PREFER: 읽기 전용 프로퍼티를 만들 때 final 필드를 사용해요.
  • CONSIDER: 단순한 멤버에 =>를 사용하는 것을 고려해요.
  • DON'T: 이름 붙은 생성자로 리다이렉트하거나 그림자(shadowing)를 피할 때를 제외하고 this.를 사용하지 마세요.
  • DO: 가능하면 선언 시점에 필드를 초기화해요.

생성자(Constructors)

  • DO: 가능하면 초기화 형식 인자(initializing formals)를 사용해요.
  • DON'T: 생성자 초기화 리스트로 충분할 때 late를 사용하지 마세요.
  • DO: 빈 생성자 본문에는 {} 대신 ;을 사용해요.
  • PREFER: 간결한 생성자 구문을 사용해요.
  • DON'T: new를 사용하지 마세요.
  • DON'T: const를 중복해서 사용하지 마세요.

오류 처리(Error handling)

  • AVOID: on 절 없는 catch를 피하세요.
  • DON'T: on 절 없는 catch의 오류를 버리지 마세요.
  • DO: 프로그램 오류에만 Error를 구현하는 객체를 throw해요.
  • DON'T: Error나 그것을 구현하는 타입을 명시적으로 catch하지 마세요.
  • DO: 잡은 예외를 다시 던질 때 rethrow를 사용해요.

비동기(Asynchrony)

  • PREFER: raw future보다 async/await을 선호해요.
  • DON'T: 유용한 효과가 없을 때 async를 사용하지 마세요.
  • CONSIDER: 스트림을 변환할 때 고차 메서드를 사용하는 것을 고려해요.
  • AVOID: Completer를 직접 사용하지 마세요.
  • DO: 타입 인자가 Object가 될 수 있는 FutureOr<T>를 구분할 때 Future<T>를 검사해요.

디자인(Design)

이름(Names)

  • DO: 용어를 일관되게 사용해요.
  • AVOID: 약어를 사용하지 마세요.
  • PREFER: 가장 설명적인 명사를 마지막에 배치해요.
  • CONSIDER: 코드가 문장처럼 읽히도록 하는 것을 고려해요.
  • PREFER: 불리언이 아닌 프로퍼티나 변수에는 명사구를 사용해요.
  • PREFER: 불리언 프로퍼티나 변수에는 비명령형 동사구를 사용해요.
  • CONSIDER: 이름 붙은 불리언 매개변수에서 동사를 생략하는 것을 고려해요.
  • PREFER: 불리언 프로퍼티나 변수에는 "긍정적" 이름을 사용해요.
  • PREFER: 주요 목적이 부수 효과인 함수나 메서드에는 명령형 동사구를 사용해요.
  • PREFER: 주요 목적이 값 반환이라면 함수나 메서드에 명사구나 비명령형 동사구를 사용해요.
  • CONSIDER: 수행하는 작업에 주의를 끌고 싶다면 함수나 메서드에 명령형 동사구를 고려해요.
  • AVOID: 함수나 메서드 이름을 get으로 시작하지 마세요.
  • PREFER: 객체의 상태를 새 객체로 복사하는 메서드 이름은 to___()로 지어요.
  • PREFER: 원본 객체에 기반한 다른 표현을 반환하는 메서드 이름은 as___()로 지어요.
  • AVOID: 함수나 메서드 이름에 매개변수를 설명하지 마세요.
  • DO: 타입 매개변수 이름을 지을 때 기존의 연상 기호 규약을 따르세요.

라이브러리(Libraries)

  • PREFER: 선언을 비공개로 만드는 것을 선호해요.
  • CONSIDER: 같은 라이브러리에 여러 클래스를 선언하는 것을 고려해요.

클래스와 믹스인(Classes and mixins)

  • AVOID: 단순한 함수로 충분할 때 멤버가 하나뿐인 추상 클래스를 정의하지 마세요.
  • AVOID: 정적 멤버만 포함하는 클래스를 정의하지 마세요.
  • AVOID: 서브클래싱을 의도하지 않은 클래스를 확장하지 마세요.
  • DO: 클래스가 확장 가능한지 제어하기 위해 클래스 한정자를 사용해요.
  • AVOID: 인터페이스로 의도되지 않은 클래스를 구현하지 마세요.
  • DO: 클래스가 인터페이스가 될 수 있는지 제어하기 위해 클래스 한정자를 사용해요.
  • PREFER: mixin 클래스보다 순수 mixin이나 순수 클래스를 정의해요.

생성자(Constructors)

  • CONSIDER: 클래스가 지원한다면 생성자를 const로 만드는 것을 고려해요.

멤버(Members)

  • PREFER: 필드와 최상위 변수를 final로 만드는 것을 선호해요.
  • DO: 개념적으로 프로퍼티에 접근하는 연산에는 게터를 사용해요.
  • DO: 개념적으로 프로퍼티를 변경하는 연산에는 세터를 사용해요.
  • DON'T: 대응하는 게터 없이 세터를 정의하지 마세요.
  • AVOID: 오버로딩을 흉내내기 위해 런타임 타입 검사를 사용하지 마세요.
  • AVOID: 초기화가 없는 공개 late final 필드를 피하세요.
  • AVOID: nullable Future, Stream, 컬렉션 타입을 반환하지 마세요.
  • AVOID: 플루언트 인터페이스를 위해 메서드에서 this를 반환하지 마세요.

타입(Types)

  • DO: 초기화가 없는 변수에는 타입 애너테이션을 붙여요.
  • DO: 타입이 명확하지 않으면 필드와 최상위 변수에 타입 애너테이션을 붙여요.
  • DON'T: 초기화된 지역 변수에 타입 애너테이션을 중복해서 붙이지 마세요.
  • DO: 함수 선언의 반환 타입에 애너테이션을 붙여요.
  • DO: 함수 선언의 매개변수 타입에 애너테이션을 붙여요.
  • DON'T: 함수 표현식의 추론된 매개변수 타입에 애너테이션을 붙이지 마세요.
  • DON'T: 초기화 형식 인자에 타입 애너테이션을 붙이지 마세요.
  • DO: 추론되지 않는 제네릭 호출에는 타입 인자를 작성해요.
  • DON'T: 추론되는 제네릭 호출에는 타입 인자를 작성하지 마세요.
  • AVOID: 불완전한 제네릭 타입을 작성하지 마세요.
  • DO: 추론이 실패하도록 두는 대신 dynamic으로 애너테이션을 붙여요.
  • PREFER: 함수 타입 애너테이션에 시그니처를 사용해요.
  • DON'T: 세터에 반환 타입을 지정하지 마세요.
  • DON'T: 레거시 typedef 구문을 사용하지 마세요.
  • PREFER: typedef보다 인라인 함수 타입을 선호해요.
  • PREFER: 매개변수에 함수 타입 구문을 사용해요.
  • AVOID: 정적 검사를 끄고 싶지 않다면 dynamic을 사용하지 마세요.
  • DO: 값을 생성하지 않는 비동기 멤버의 반환 타입으로 Future<void>를 사용해요.
  • AVOID: 반환 타입으로 FutureOr<T>를 사용하지 마세요.

매개변수(Parameters)

  • AVOID: 위치 기반 불리언 매개변수를 피하세요.
  • AVOID: 사용자가 앞선 매개변수를 생략하고 싶을 수 있다면 선택적 위치 매개변수를 피하세요.
  • AVOID: 특수한 "인자 없음" 값을 받는 필수 매개변수를 피하세요.
  • DO: 범위를 받을 때 시작 포함(inclusive)과 끝 제외(exclusive) 매개변수를 사용해요.

동등(Equality)

  • DO: ==를 오버라이드하면 hashCode도 오버라이드해요.
  • DO: == 연산자가 수학적 동등 규칙을 따르게 해요.
  • AVOID: 가변 클래스에 사용자 지정 동등을 정의하지 마세요.
  • DON'T: ==의 매개변수를 nullable로 만들지 마세요.

더 알아보기