모범 사례

Angular 코딩 스타일 가이드

소개

이 가이드는 Angular 애플리케이션 코드에 적용할 여러 스타일 규칙을 설명합니다. 이 권장 사항은 Angular가 동작하기 위한 필수 조건이 아니라, Angular 생태계 전반의 일관성을 높이기 위한 코딩 관례입니다. 일관된 관례가 있으면 코드를 공유하고 프로젝트 사이를 이동하기 쉬워집니다.

이 가이드는 Angular와 관련 없는 TypeScript나 일반적인 코딩 관례는 다루지 않습니다. TypeScript에 관해서는 Google TypeScript 스타일 가이드를 확인하세요.

판단하기 어렵다면 일관성을 우선하세요

이 규칙이 특정 파일의 스타일과 충돌한다면 해당 파일 내의 일관성을 유지하는 데 우선순위를 두세요. 한 파일 안에 여러 스타일 규칙을 섞는 것은 이 가이드의 권장 사항에서 벗어나는 것보다 혼란을 더 크게 만듭니다.

이름 짓기

파일 이름의 단어는 하이픈으로 구분하세요

파일 이름의 단어는 하이픈(-)으로 구분하세요. 예를 들어 UserProfile이라는 컴포넌트의 파일 이름은 user-profile.ts입니다.

테스트 파일에는 같은 이름을 사용하고 끝에 .spec을 붙이세요

단위 테스트의 파일 이름은 .spec.ts로 끝나게 하세요. 예를 들어 UserProfile 컴포넌트의 단위 테스트 파일 이름은 user-profile.spec.ts입니다.

파일 이름은 내부 TypeScript 식별자와 맞추세요

파일 이름은 일반적으로 그 안에 있는 코드의 내용을 설명해야 합니다. 파일에 TypeScript 클래스가 있다면 파일 이름에 클래스 이름을 반영하세요. 예를 들어 UserProfile 컴포넌트가 있는 파일의 이름은 user-profile.ts입니다.

파일에 이름을 붙일 수 있는 주요 식별자가 여러 개 있다면 코드의 공통 주제를 설명하는 이름을 선택하세요. 파일의 코드가 하나의 공통 주제나 기능 영역에 속하지 않는다면 여러 파일로 나누는 것을 고려하세요. helpers.ts, utils.ts, common.ts처럼 지나치게 포괄적인 파일 이름은 피하세요.

컴포넌트의 TypeScript, 템플릿, 스타일 파일은 같은 이름을 사용하세요

컴포넌트는 보통 TypeScript 파일 하나, 템플릿 파일 하나, 스타일 파일 하나로 구성됩니다. 이 파일들은 이름을 같게 하고 확장자만 다르게 해야 합니다. 예를 들어 UserProfile 컴포넌트는 user-profile.ts, user-profile.html, user-profile.css 파일로 구성할 수 있습니다.

컴포넌트에 스타일 파일이 여러 개라면 각 파일에 고유한 스타일을 설명하는 단어를 이름에 덧붙이세요. 예를 들어 UserProfile에는 user-profile-settings.css와 user-profile-subscription.css 스타일 파일이 있을 수 있습니다.

프로젝트 구조

애플리케이션 코드는 모두 src 디렉터리에 두세요

Angular UI 코드(TypeScript, HTML, 스타일)는 모두 src라는 디렉터리 안에 두어야 합니다. 설정 파일이나 스크립트처럼 UI와 관련 없는 코드는 src 디렉터리 바깥에 두세요.

이렇게 하면 Angular 프로젝트마다 루트 디렉터리의 구성이 일관되고, 프로젝트의 UI 코드와 다른 코드를 명확하게 구분할 수 있습니다.

src 바로 아래의 main.ts 파일에서 애플리케이션을 부트스트랩하세요

Angular 애플리케이션을 시작하는, 즉 부트스트랩하는 코드는 항상 main.ts라는 파일에 두어야 합니다. 이 파일이 애플리케이션의 주요 진입점입니다.

밀접하게 관련된 파일은 같은 디렉터리에 모으세요

Angular 컴포넌트는 TypeScript 파일로 구성되며, 템플릿 파일과 하나 이상의 스타일 파일을 선택적으로 포함할 수 있습니다. 이 파일들은 같은 디렉터리에 모아야 합니다.

단위 테스트는 테스트 대상 코드와 같은 디렉터리에 두세요. 서로 관련 없는 테스트를 하나의 tests 디렉터리에 모으는 것은 피하세요.

프로젝트는 기능 영역별로 구성하세요

애플리케이션의 기능이나 코드의 공통 주제를 기준으로 프로젝트를 하위 디렉터리로 나누세요. 예를 들어 영화관 사이트 MovieReel의 프로젝트 구조는 다음과 같을 수 있습니다:

src/
├─ movie-reel/
│ ├─ show-times/
│ │ ├─ film-calendar/
│ │ ├─ film-details/
│ ├─ reserve-tickets/
│ │ ├─ payment-info/
│ │ ├─ purchase-confirmation/

디렉터리에 들어가는 코드의 종류를 기준으로 하위 디렉터리를 만들지 마세요. 예를 들어 components, directives, services 같은 디렉터리는 피하세요.

한 디렉터리에 파일을 너무 많이 넣어 읽거나 탐색하기 어려워지지 않도록 하세요. 파일 수가 늘어나면 하위 디렉터리로 더 나누는 것을 고려하세요.

파일 하나에는 개념 하나를 담으세요

소스 파일은 하나의 개념 에 집중하는 것이 좋습니다. Angular 클래스에서는 보통 파일마다 컴포넌트, 디렉티브, 서비스 중 하나를 두는 것을 뜻합니다. 다만 클래스가 비교적 작고 하나의 개념으로 긴밀히 연결된다면 한 파일에 여러 컴포넌트나 디렉티브를 두어도 괜찮습니다.

판단하기 어렵다면 파일이 더 작아지는 방식을 선택하세요.

의존성 주입

생성자 매개변수 주입보다 inject 함수를 사용하세요

생성자 매개변수로 주입하는 것보다 inject 함수를 사용하는 것이 좋습니다. inject 함수는 생성자 매개변수 주입과 같은 방식으로 동작하지만, 스타일 측면에서 다음과 같은 이점이 있습니다:

  • inject는 대체로 읽기 쉬우며, 특히 클래스가 많은 의존성을 주입할 때 유리합니다.
  • 주입한 의존성에 주석을 추가하는 문법이 더 간단합니다.
  • inject는 타입 추론이 더 좋습니다.
  • ES2022 이상을 대상으로 useDefineForClassFields를 사용할 때, 필드가 주입된 의존성을 참조하더라도 필드 선언과 초기화를 분리하지 않아도 됩니다.

자동화 도구로 기존 코드를 inject 방식으로 리팩토링할 수 있습니다.

컴포넌트와 디렉티브

컴포넌트 셀렉터 선택하기

컴포넌트 가이드의 셀렉터 선택하기를 참고하세요.

컴포넌트와 디렉티브 멤버의 이름 짓기

컴포넌트 가이드의 입력 프로퍼티 이름 짓기와 출력 프로퍼티 이름 짓기를 참고하세요.

디렉티브 셀렉터 선택하기

디렉티브는 컴포넌트와 같은 애플리케이션 고유의 접두사를 사용해야 합니다.

디렉티브에 어트리뷰트 셀렉터를 사용한다면 어트리뷰트 이름에 카멜 표기법을 사용하세요. 예를 들어 애플리케이션 이름이 "MovieReel"이고 요소에 툴팁을 추가하는 디렉티브를 만든다면 [mrTooltip] 셀렉터를 사용할 수 있습니다.

Angular 고유의 프로퍼티는 메서드 앞에 모으세요

컴포넌트와 디렉티브의 Angular 고유 프로퍼티는 보통 클래스 선언의 윗부분에 모아두어야 합니다. 주입한 의존성, 입력, 출력, 쿼리가 여기에 해당합니다. 이러한 프로퍼티와 다른 프로퍼티는 클래스 메서드보다 앞에 정의하세요.

이렇게 하면 클래스의 템플릿 API와 의존성을 쉽게 찾을 수 있습니다.

컴포넌트와 디렉티브는 화면 표현에 집중하도록 하세요

컴포넌트와 디렉티브 내부의 코드는 대체로 페이지에 표시되는 UI와 관련되어야 합니다. UI와 분리해도 독립적으로 의미가 있는 코드는 다른 파일로 옮기는 것이 좋습니다. 예를 들어 폼 유효성 검사 규칙이나 데이터 변환을 별도의 함수나 클래스로 분리할 수 있습니다.

템플릿에 지나치게 복잡한 로직을 넣지 마세요

Angular 템플릿은 JavaScript와 유사한 표현식을 사용할 수 있도록 설계되었습니다. 비교적 간단한 로직은 이러한 템플릿 표현식에 직접 작성하면 됩니다.

하지만 템플릿 코드가 너무 복잡해지면 로직을 TypeScript 코드로 옮기세요(보통 computed를 사용합니다).

어느 정도가 "복잡한지"를 정하는 절대적인 기준은 없습니다. 상황에 맞게 판단하세요.

컴포넌트 템플릿에서만 사용하는 클래스 멤버에는 protected를 사용하세요

컴포넌트 클래스의 공개 멤버는 의존성 주입과 쿼리로 접근할 수 있는 공개 API를 구성합니다. 컴포넌트 템플릿에서 읽는 용도의 멤버에는 protected 접근 제한자를 사용하는 것이 좋습니다.

@Component({
  ...,
  template: `<p>{{ fullName() }}</p>`,
})
export class UserProfile {
  firstName = input();
  lastName = input();

// `fullName`은 컴포넌트의 공개 API가 아니며 템플릿에서 사용합니다.
  protected fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
}

변경하면 안 되는 프로퍼티에는 readonly를 사용하세요

Angular가 초기화하는 컴포넌트와 디렉티브 프로퍼티는 readonly로 표시하세요. input, model, output, 쿼리로 초기화하는 프로퍼티가 여기에 해당합니다. readonly 접근 제한자는 Angular가 설정한 값을 덮어쓰지 못하도록 합니다.

@Component(/* ... */)
export class UserProfile {
  readonly userId = input();
  readonly userSaved = output();
  readonly userName = model();
}

데코레이터 기반의 @Input, @Output, 쿼리 API를 사용하는 컴포넌트와 디렉티브에서는 이 지침이 출력 프로퍼티와 쿼리에 적용되지만 입력 프로퍼티에는 적용되지 않습니다.

@Component(/* ... */)
export class UserProfile {
  @Output() readonly userSaved = new EventEmitter<void>();
  @ViewChildren(PaymentMethod) readonly paymentMethods?: QueryList<PaymentMethod>;
}

ngClass와 ngStyle보다 class와 style을 사용하세요

NgClass와 NgStyle 디렉티브보다 class와 style 바인딩을 사용하는 것이 좋습니다.

권장
<div [class.admin]="isAdmin" [class.dense]="density === 'high'">
  <div [style.color]="textColor" [style.background-color]="backgroundColor">
    <!-- 또는 -->
    <div [class]="{admin: isAdmin, dense: density === 'high'}">
      <div [style]="{'color': textColor, 'background-color': backgroundColor}"></div>
    </div>
  </div>
</div>
권장하지 않음
<div [ngClass]="{admin: isAdmin, dense: density === 'high'}">
  <div [ngStyle]="{'color': textColor, 'background-color': backgroundColor}"></div>
</div>

class와 style 바인딩은 모두 표준 HTML 어트리뷰트에 가까운 간단한 문법을 사용합니다. 특히 기본 HTML에 익숙한 개발자가 템플릿을 읽고 이해하기 쉽습니다.

또한 NgClass와 NgStyle 디렉티브는 내장된 class, style 바인딩 문법보다 추가 성능 비용이 발생합니다.

자세한 내용은 바인딩 가이드를 참고하세요.

이벤트 핸들러의 이름은 이벤트가 아니라 수행하는 동작 을 나타내도록 지으세요

이벤트 핸들러의 이름은 실행을 유발한 이벤트보다 수행하는 동작을 나타내도록 짓는 것이 좋습니다:

권장
<button (click)="saveUserData()">Save</button>
권장하지 않음
<button (click)="handleClick()">Save</button>

이처럼 의미 있는 이름을 사용하면 템플릿만 읽어도 이벤트가 어떤 동작을 하는지 쉽게 알 수 있습니다.

키보드 이벤트에서는 구체적인 핸들러 이름과 Angular의 키 이벤트 수식어를 함께 사용할 수 있습니다:

<textarea (keydown.control.enter)="commitNotes()" (keydown.control.space)="showSuggestions()">

이벤트 처리 로직이 매우 길거나 복잡하면 적절한 이름의 핸들러 하나로 선언하기 어려울 수 있습니다. 이 경우에는 'handleKeydown' 같은 이름을 사용하고 이벤트 세부 정보에 따라 더 구체적인 동작으로 위임해도 괜찮습니다:

@Component(/* ... */)
class RichText {
  handleKeydown(event: KeyboardEvent) {
    if (event.ctrlKey) {
      if (event.key === 'B') {
        this.activateBold();
      } else if (event.key === 'I') {
        this.activateItalic();
      }
      // ...
    }
  }
}

라이프사이클 메서드는 간단하게 유지하세요

ngOnInit 같은 라이프사이클 후킹 메서드에 길고 복잡한 로직을 넣지 마세요. 대신 로직을 설명하는 이름의 메서드를 만들고 라이프사이클 후킹 메서드에서 그 메서드를 호출 하는 것이 좋습니다. 라이프사이클 후킹 메서드의 이름은 언제 실행되는지를 나타내므로, 내부 코드가 어떤 일을 하는지 설명하는 의미 있는 이름이 되지는 못합니다.

권장
ngOnInit() {
  this.startLogging();
  this.runBackgroundTask();
}
권장하지 않음
ngOnInit() {
  this.logger.setMode('info');
  this.logger.monitorErrors();
  // ...그리고 이 메서드들에서 펼쳐 쓴 나머지 코드가 이어집니다.
}

라이프사이클 후킹 인터페이스를 사용하세요

Angular는 각 라이프사이클 메서드에 해당하는 TypeScript 인터페이스를 제공합니다. 클래스에 라이프사이클 후킹 메서드를 추가할 때는 해당 인터페이스를 가져와 implement하여 메서드 이름이 올바른지 확인하세요.

import {Component, OnInit} from '@angular/core';

@Component(/* ... */)
export class UserProfile implements OnInit {
  // `OnInit` 인터페이스는 이 메서드의 이름이 올바른지 확인합니다.
  ngOnInit() {
    /* ... */
  }
}