개요


이 페이지는 Angular 프로젝트에서 가독성을 높이기 위해 권고하는 코딩 스타일 가이드에 대한 학습 자료를 정리하였습니다.

Angular 팀에서 제안하는 코딩 스타일 가이드


공식 사이트에서 크게 기억에 남는 부분만 발췌한 것으로, 더 자세한 내용은 아래 출처 페이지를 참고하시기 바랍니다.

 

단일 책임 원칙

모든 컴포넌트와 서비스, 심볼은 단일 책임 원칙(single responsibility principle, SRP)을 준수하며 작성하세요.

그러면 애플리케이션이 좀 더 깔끔해지고 유지보수하기도 편하며, 테스트하기도 편해집니다.

 

한 파일에는 400줄 이하의 코드만 작성하는 것을 권장합니다.

하나의 컴포넌트에서 모두 정의하지 말고, .component / .model / .service 등으로 나누어서 개발하세요.

파일 하나에 컴포넌트를 하나만 정의하면 좀 더 읽기 편한 코드를 작성할 수 있고, 유지보수하기도 편하며, 팀 단위로 개발할 때 코드 충돌이 발생하는 것도 피할 수 있습니다.

 

함수는 75줄 이하로 간단하게 작성하는 것을 권장합니다.

함수는 하나의 목적으로 하나의 기능만 구현되어 있을 때 가장 테스트하기 편합니다.

함수의 기능이 간단할수록 재사용하기 편합니다.

함수의 기능이 작을수록 코드를 읽기 쉽습니다.

함수의 기능이 작을수록 유지보수하기 편합니다.

 

명명규칙

일반 명명 규칙

파일의 이름은 그 파일에 정의된 심볼의 기능과 타입이 드러나도록 작성하세요. 기능.타입.ts와 같은 형식으로 작성하는 것을 권장합니다.

명명 규칙을 명확하게 정하면 파일의 이름만 봐도 내용을 쉽게 파악할 수 있습니다. 그래서 파일의 이름은 일관된 규칙으로 정해져야 하며, 프로젝트나 팀에서 정한 일관성이라도 좋습니다.

예를 들어 app/heroes/hero-list.component.ts라는 파일을 보면 이 파일이 히어로의 리스트를 처리하는 컴포넌트라고 바로 알 수 있습니다.

파일의 이름을 여러 단어로 설명해야 한다면 대시(-)로 구분하세요.
파일의 기능을 설명하는 부분과 타입은 마침표(.)로 구분하세요.

파일의 타입은 .service, .component, .pipe, .module, .directive로 작성하세요.

필요하다면 타입을 추가해도 문제없지만, 너무 많이 추가하는 것은 좋지 않습니다.

타입의 이름을 보면 이 파일이 어떤 역할을 하는지 직관적으로 알 수 있어야 합니다.

타입에 사용하는 단어는 축약하지 않는 것이 좋습니다. .srv, .svc, .serv와 같은 단어는 혼란을 줄 수 있습니다.

-> 타입은 축약 No

 

심볼과 파일 이름

클래스 이름은 대문자 캐멀 케이스를 사용하세요.
심볼의 이름과 파일의 이름이 연관되도록 하세요.
심볼 이름 뒤에는 타입을 표현하는 접미사(Component, Directive, Module, Pipe, Service)를 붙이세요.

심볼과 파일의 이름을 일관되게 지으면 어떤 타입인지 구분하기 편하고, 참조하기도 편합니다.

<예시>

hero-list.component.ts

@Component({ … }) 
// 컴포넌트의 클래스명은 파일명 + 타입명(컴포넌트) 형식으로 작성한다.(대문자로 시작)
export class HeroListComponent { }

서비스 이름

서비스의 이름은 그 서비스가 제공하는 기능을 표현하도록 정의하세요.

하지만 개발자에 따라서 "-er" 접미사를 사용하는 것이 더 익숙할 수도 있습니다. 그래서 LoggerService라는 이름보다 Logger가 더 익숙할 수 있습니다. 프로젝트에 어울린다면 어떤 방식을 사용해도 문제없습니다. 일관성을 유지하기만 하면 됩니다.

이름을 일관되게 정의하면 해당 심볼이 서비스라는 것이 명확해집니다.
Logger와 같은 이름을 사용할 때는 클래스에 Service 접미사를 붙이지 않는 것이 좋습니다.

<예시>

hero-data.service.ts

@Injectable() 
// 서비스의 클래스명은 파일명 + 타입명(Service) 형식으로 작성한다.(대문자로 시작)
export class HeroDataService { }

logger.service.ts

@Injectable() 
// -er로 끝나는 서비스는 Service 명칭을 생략하기로 정한다.
export class Logger { }

부트스트랩

부트스트랩이나 플랫폼과 관련된 로직은 main.ts 파일에 작성하세요.

부트스트랩 로직에서 발생할 수 있는 에러를 처리하는 로직도 함께 작성하세요.

애플리케이션 로직을 main.ts 파일에 작성하는 것은 피하세요. 이 로직은 컴포넌트나 서비스에 들어가는 것이 좋습니다.

main.ts 파일에는 애플리케이션을 시작할 때 필요한 로직만 들어가는 것이 좋습니다.

<예시>

main.ts

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';

// main.ts 파일에서는 부트 스트랩 로직 이외에는 별도로 작성하지 않는다.
platformBrowserDynamic().bootstrapModule(AppModule)
  .then(success => console.log(`Bootstrap success`))
  .catch(err => console.error(err));

컴포넌트 셀렉터

컴포넌트의 셀렉터 이름은 대시-케이스(dash-case) 나 케밥-케이스(kebab-case) 로 정의하세요.

커스텀 컴포넌트의 접두사

컴포넌트 셀렉터에는 커스텀 접두사를 사용하세요. 

예를 들어 프로젝트 이름이 Tour of Heroes 라면 toh를 접두사로 사용할 수 있으며, 관리자용 기능이 구현되어 있는 곳에서는 admin을 접두사로 사용할 수 있습니다.

컴포넌트의 엘리먼트 셀렉터는 다른 앱의 컴포넌트 셀렉터나 네이티브 HTML과 충돌하지 않도록 지정해야 합니다.

DOM에 사용된 컴포넌트는 다른 네이티브 HTML 엘리먼트와 쉽게 구분되어야 합니다.

-> 클래스 심볼명에는 toh와 같은 프로젝트 접두사를 붙이지 않는다.

 

<예시>

app/heroes/shared/hero-button/hero-button.component.ts

@Component({
  // 컴포넌트 selector는 대시 스타일로 작성하며, 커스텀 접두사는 구별을 위해 프로젝트명의 약자를 사용하면 좋다.
  selector: 'toh-hero-button',
  templateUrl: './hero-button.component.html'
})
// 클래스명과 파일명에는 굳이 커스텀 접두사를 넣을 필요가 없다.
export class HeroButtonComponent {}

디렉티브 셀렉터

디렉티브의 셀렉터는 소문자로 시작하는 캐멀 케이스를 사용하세요.

디렉티브의 셀렉터는 HTML문서에서 어트리뷰트로 사용됩니다.

디렉티브가 네이티브 HTML과 충돌하면 안됩니다.

디렉티브는 이름만 보고 쉽게 구분할 수 있어야 합니다.

 

<예시>

app/shared/validate.directive.ts

@Directive({
  // 커스텀 Directive(HTML의 커스텀 Attribute 정의)는 camelCase로 작성하며, 속성 selector 이므로 []로 감싼다.
  selector: '[tohValidate]'
})
export class ValidateDirective {}

파이프 이름

커스텀 파이프를 구현한 클래스에는 Pipe 접미사를 붙이고, 이 파일에는 .pipe 타입을 명시하세요. 

파이프 클래스의 이름은 클래스 이름이 보통 그렇듯 대문자 캐멀 케이스(UpperCamelCase)로 작성하며, 메타데이터의 name 프로퍼티는 소문자 캐멀 케이스(lowerCamelCase) 로 작성해야 합니다.

 

<예시>

init-caps.pipe.ts

@Pipe({
	// Pipe의 name 속성은 camelCase로 작성하며, 무거운 로직은 넣지 않는다.
	name: 'initCaps' 
}) 
export class InitCapsPipe implements PipeTransform { }

Angular NgModule의 이름

NgModule 심볼 이름에는 Module 접미사를 붙이세요.

모듈을 정의한 파일 이름에는 .module.ts 접두사를 붙이세요.

모듈의 기능을 표현할 수 있는 폴더 이름을 짓고, 이 파일 안에 모듈을 작성하세요.

정의하는 모듈이 라우팅 모듈 이라면 RoutingModule 접미사를 붙이세요.
라우팅 모듈 을 정의한 파일의 이름에는 -routing.module.ts 접미사를 붙이세요.

 

<예시>

heroes.module.ts

@NgModule({ … }) 
export class HeroesModule { }

heroes-routing.module.ts

@NgModule({ … }) 
// 라우팅 모듈은 ~RoutingModule 형식으로 선언한다.
export class HeroesRoutingModule { }

애플리케이션 구조와 NgModule

애플리케이션 코드는 모두 src 폴더에 둡니다. 

이 중에서 특정 기능은 따로 모아 별개의 폴더에 둘 수 있으며, 이 때 NgModule도 함께 생성합니다.

파일 하나에는 구성요소 하나만 선언하는 것이 좋습니다. 

컴포넌트, 서비스, 파이프 등 어떤 구성요소라도 한 파일에 함께 있지 말고 개별 파일로 있는 것이 좋습니다.

서드 파티 스크립트 파일은 src 폴더가 아니라 다른 폴더에 위치합니다.

이 코드는 개발자가 직접 수정하지 않기 때문에 src 폴더에 함께 있을 필요가 없습니다. 

 

LIFT 규칙을 따르는 것이 좋습니다.

  • Locate: 코드는 개발자가 찾기 쉬운 곳에 두세요.
  • Identify: 파일을 봤을 때 이 파일이 무엇인지 한 눈에 알아볼 수 있도록 하세요.
  • Flattest: 최대한 단순한 폴더 구조를 유지하세요.
  • T: 반복된 부분은 최대한 묶으세요. (Try to be DRY, DRY: Don't Repeat Yourself)

이런 상황을 항상 생각해 보세요: 이 기능을 수정하려면 어디에 있는 파일을 열어야 원하는 코드를 찾을 수 있을까?

Locate

코드는 직관적으로 떠오르는 위치에 두세요. 그게 간단하고 빠릅니다.

개발효율을 높이려면 원하는 파일을 빠르게 찾아야 하는데, 이 때 파일의 이름 을 기억하고 있는지는 중요하지 않습니다. 관련된 파일은 서로 비슷한 위치에 두어야 시간을 절약할 수 있습니다. 폴더 이름과 구조만 봐도 어떤 의미인지 알 수 있다면 당신과 당신의 후임이 만나는 세상은 조금 더 행복해질 것입니다.

Identify

파일 이름은 그 파일의 내용물을 보고 바로 생각나는 것으로 지으세요.
파일 이름은 파일의 내용과 관련된 것으로 짓고 그 파일은 그 용도로만 사용하세요.

파일 하나에 컴포넌트나 서비스를 여러개 선언하지 마세요.

코드가 어디있는지 오래 찾아다닐수록 개발 효율은 떨어집니다. 

그래서 애매한 의미로 짧게 줄인 파일 이름보다는 조금 길더라도 파일 내용을 잘 설명하는 이름이 더 좋습니다.

 

Flat

폴더 구조는 최대한 단순하게 유지하세요.

파일의 개수가 7개 이상 된다면 하위 폴더를 만들어서 분리하는 것을 권장합니다.

폴더 안에 파일이 10개 이상 있다면 하위 폴더를 만드는 것도 고려해볼만 합니다.

폴더를 만들어야 할 이유가 확실하게 생기지 않는다면 폴더 구조는 최대한 단순하게 유지하는 것이 좋습니다.

 

T-DRY (되도록이면 DRY 정책을 지키세요.)

DRY (Don't Repeat Yourself) 정책을 되도록이면 지키세요.
하지만 DRY 정책이 과하면 코드의 가독성이 떨어집니다.
DRY 정책을 유지하는 것은 좋지만 다른 LIFT 정책을 포기할 정도로 중요하지는 않습니다. 

그래서 DRY 정책을 반드시 지키라는 것이 아니라 되도록이면 지키라고 하는 것입니다. 

 

예를 들어 hero-view.component.html라는 이름의 템플릿 파일이 있을 때 이 파일의 확장자 부분 .html은 쓸모없어 보일 수 있습니다

하지만 확장자 지정을 생략하면 이 파일이 어떤 용도로 사용되는 파일인지 한 번에 알아볼 수 없게 됩니다.

 

폴더 구조 가이드라인

컴포넌트를 구성하는 파일이 여러 개라면(.ts, .html, .css, .spec) 이 파일들은 따로 폴더를 만들어서 관리하는 것을 고려해볼만 합니다.

애플리케이션의 폴더 구조는 개발 초기부터 간단하게 구성해야 애플리케이션을 확장할 때도 편합니다.

컴포넌트는 보통 4개 파일(*.html, *.css, *.ts, *.spec.ts)로 구성되기 때문에 컴포넌트 폴더를 따로 구분하지 않고 모아둔다면 이 폴더는 빠르게 복잡해집니다.

\

기능별로 폴더를 만드는 구성

해당 기능을 표현할 수 있는 폴더 이름을 지정하세요.

 

개발자가 찾으려고 하는 파일이 어디에 있는지 한 눈에 알 수 있어야 합니다. 

그러려면 폴더 구조가 단순할수록, 중복되거나 군더더기 없는 폴더 이름이 사용될수록 더 좋습니다.

 

기능 영역마다 NgModule을 생성하세요.
NgModule은 지연로딩을 적용할 수 있는 단위입니다.

 

 최상위 모듈

애플리케이션 최상위 모듈은 애플리케이션 최상위 폴더에 생성하세요. 보통은 /src/app 폴더입니다.

 

최상위 모듈이 어디에 있는지, 어떤 파일인지 쉽게 파악할 수 있는 곳에 두는 곳에 좋습니다.

 

기능 모듈

NgModule은 애플리케이션이 제공하는 특정 기능 단위로 생성하세요. 

 

히어로와 관련된 기능은 Heroes 모듈로 만드는 식입니다.

기능 모듈은 다른 모듈과 확실하게 구분되어야 합니다.

기능 모듈 폴더에는 해당 기능을 제공하기 위해 사용되는 컴포넌트들이 포함됩니다.

기능 모듈은 즉시 로드할 수 있으며 필요하면 지연로딩할 수도 있습니다.

기능 모듈은 다른 모듈과 격리할 수 있기 때문에 테스트하기도 편합니다.

 

공유 모듈

공유 모듈은 shared 폴더 안에 SharedModule이라는 이름으로 정의하세요. 

 

공유 모듈에 정의하는 컴포넌트, 디렉티브, 파이프는 다른 기능 모듈에 재사용될 수 있는 형태로 구현

 

SharedModule이라는 이름은 애플리케이션 전역에 사용된다는 의미이기 때문에 이 이름을 권장합니다.

 

공유 모듈에 서비스를 정의하는 것은 권장하지 않습니다

서비스의 인스턴스는 애플리케이션 전역에 싱글턴으로 하나만 존재하거나, 특정 기능 모듈에 존재합니다. 

 

아래 예제 코드 트리에는 SharedModule 안에 FilterTextService가 들어있지만, 서비스가 특정 상태를 기반으로 동작하는 것이 아니라면 예외적으로 허용될 수 있습니다.

 

모든 모듈이 사용하는 리소스는 SharedModule에서 불러오세요

CommonModule이나 FormsModule이 대상이 될 수 있습니다.

SharedModule 안에 있는 모든 심볼은 다른 기능 모듈이 사용할 수 있도록 모두 외부로 공개(export)하세요.

 

앱 전역에 싱글턴으로 존재하는 서비스의 프로바이더는 SharedModule 안에 등록하지 마세요. 의도적인 것이라면 괜찮겠지만, 주의해야 합니다.

 

왜?

지연로딩되는 기능 모듈이 공유 모듈을 로드하면 서비스 인스턴스를 새로 생성하기 때문에 원하는대로 동작하지 않을 수 있습니다.
싱글턴 서비스를 개별 모듈마다 따로 구성하는 상황은 바람직하지 않습니다SharedModule에 서비스 프로바이더를 등록하면 이런 상황이 될 수 있습니다.

 

<예시>

app/shared/shared.module.ts 파일에 SharedModule을 정의하는 식입니다

app/shared/shared.module.ts (공유 모듈)

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

import { FilterTextComponent } from './filter-text/filter-text.component';
import { FilterTextService } from './filter-text/filter-text.service';
import { InitCapsPipe } from './init-caps.pipe';

@NgModule({
  // 컴포넌트 구현 시 기본적으로 사용되는 Angular Module은 기본적으로 imports 하는 것이 좋습니다.
  imports: [CommonModule, FormsModule],
  // 공통 컴포넌트, 파이프, 디렉티브를 선언합니다.
  declarations: [
    FilterTextComponent,
    InitCapsPipe
  ],
  // SharedModule에서는 일반적으로 모듈에서 Service 인스턴스를 신규 생성해야되지 않는 한, 선언하지 않습니다.
  providers: [FilterTextService],
  // 다른 기능 모듈에서 사용할 수 있도록, export해주어야 합니다.
  exports: [
    // 다른 기능 모듈에서 사용할 수 있도록, 공통 Angular 모듈을 export 합니다.
    CommonModule,
    FormsModule,
    // 다른 기능 모듈에서 사용할 수 있도록, 공통 컴포넌트, 파이프, 디렉티브를 export 합니다.
    FilterTextComponent,
    InitCapsPipe
  ]
})
// SharedModule은 애플리케이션 전역에 사용되는 컴포넌트, 파이프, 디렉티브를 정의한 모듈입니다.
export class SharedModule { }

app/heroes/heroes.module.ts (기능 모듈)

 

 

지연로딩 모듈의 폴더

애플리케이션 모듈은 지연로딩하거나 흐름상 필요할 때 로드할 수 있습니다. 

그러면 애플리케이션에는 이 모듈이 제외되면서 초기 실행 속도가 빨라집니다.

지연로딩하는 모듈은 지연 로딩용 폴더에 따로 작성하세요. 

일반적으로 이 폴더에는 라우팅 컴포넌트, 라우팅 컴포넌트의 자식 컴포넌트, 관련 리소스가 위치합니다.

 

지연로딩 모듈을 직접 로드하지 마세요.

이웃 폴더나 부모 폴더에서 지연로딩 모듈을 직접 로드하는 것은 피하세요.**

지연모듈을 직접 로드하면 이 모듈이 사용된다는 것을 의미하며, 지연로딩되지 않고 애플리케이션이 시작될 때 즉시 로드됩니다. 지연로딩의 의미는 없어집니다.

파이프에 필터, 정렬 로직을 넣지 마세요.

커스텀 파이프에 필터, 정렬 로직을 넣는 것은 피하세요.

필터, 정렬 로직은 컴포넌트나 서비스 로직으로 미리 처리한 후에 템플릿에 바인딩하는 것이 좋습니다.

필터도 그렇지만 특히 정렬 로직은 많은 연산이 필요합니다. Angular 안에서는 파이프가 1초에 여러번도 호출될 수 있기 때문에, 이 때마다 정렬, 필터 연산이 실행되면 UX 측면에서 좋지 않습니다.

 

컴포넌트

엘리먼트로 사용하기

컴포넌트 셀렉터는 어트리뷰트([어트리뷰트명]) 나 클래스 셀렉터(.클래스명)보다 엘리먼트 셀렉터로 사용하는 것을 권장합니다.

컴포넌트에는 HTML 문법으로 작성된 템플릿이 있으며, Angular에서 제공하는 템플릿 문법이 이 템플릿에 사용되기도 합니다. 컴포넌트의 역할은 컴포넌트의 내용을 화면을 표시하는 것입니다. 따라서 네이티브 HTML 엘리먼트나 웹 컴포넌트와 동일한 계층을 사용해서 엘리먼트 셀렉터로 지정하는 것이 좋습니다.

컴포넌트 셀렉터를 엘리먼트로 지정하면, 템플릿을 봤을 때 어떤 것이 컴포넌트인지 쉽게 확인할 수 있습니다.

 

템플릿과 스타일은 개별 파일로 분리하세요.

템플릿과 스타일을 지정하는 코드가 3줄 이상 된다면 개별 파일로 분리하세요.

템플릿 파일의 이름은 [컴포넌트 이름].component.html으로 지정하세요.

스타일 파일의 이름은 [컴포넌트 이름].component.css로 지정하세요.

컴포넌트 클래스에서는 ./로 시작하는 상대 주소 로 참조하세요.

 

템플릿과 스타일을 컴포넌트 클래스 파일 안에 길게 작성하면 가독성이 떨어지고 유지보수하기도 어려워 집니다.

 

<예시>

app/heroes/heroes.component.ts

@Component({
  selector: 'toh-heroes',
  templateUrl: './heroes.component.html',
  styleUrls:  ['./heroes.component.css']
})
export class HeroesComponent {
  // 컴포넌트 상에서 private 지시어가 없으면, public을 의미하며 템플릿에서 사용합니다.
  heroes: Observable<Hero[]>;
  // 개별 속성을 선언하기 보다는, 모델 형식으로 선언하고, 서비스에서 그대로 받아오는 것이 좋습니다.
  selectedHero!: Hero;

  constructor(private heroService: HeroService) {
    this.heroes = this.heroService.getHeroes();
  }
}

app/heroes/heroes.component.html

-> .ts 파일 상에서 private이 아닌 public으로 선언된 값들은 모두 템플릿 파일 상에서 사용된다.

-> html은 객체.속성명 등으로 렌더링하면 깔끔한 듯?

<div>
  <h2>My Heroes</h2>
  // 여러 군데에 class를 선언하기보다는, list element의 최상위에만 정의하는 것이 깔끔합니다.
  <ul class="heroes">
  // Observable 객체에 async pipe를 사용하면, subscribe 효과로 Observable의 값을 불러올 수 있습니다.
    <li *ngFor="let hero of heroes | async">
      <button type="button" (click)="selectedHero=hero">
      	// 객체.속성명 형식으로 선언하면 깔끔하게 보입니다.
        <span class="badge">{{hero.id}}</span>
        <span class="name">{{hero.name}}</span>
      </button>
    </li>
  </ul>
  <div *ngIf="selectedHero">
    <h2>{{selectedHero.name | uppercase}} is my hero</h2>
  </div>
</div>

app/heroes/heroes.component.css

-> css는 아래와 같이 계층 구조로 작성하면 좋은 듯?

// 계층 구조로 작성하면, 훨씬 더 깔끔합니다.
.heroes {
  margin: 0 0 2em 0;
  list-style-type: none;
  padding: 0;
  width: 15em;
}

.heroes li {
  display: flex;
}

.heroes button {
  flex: 1;
  cursor: pointer;
  position: relative;
  left: 0;
  background-color: #EEE;
  margin: .5em;
  padding: 0;
  border-radius: 4px;
  display: flex;
  align-items: stretch;
  height: 1.8em;
}

.heroes button:hover {
  color: #2c3a41;
  background-color: #e6e6e6;
  left: .1em;
}

.heroes button:active {
  background-color: #525252;
  color: #fafafa;
}

.heroes button.selected {
  background-color: black;
  color: white;
}

.heroes button.selected:hover {
  background-color: #505050;
  color: white;
}

.heroes button.selected:active {
  background-color: black;
  color: white;
}

.heroes .badge {
  display: inline-block;
  font-size: small;
  color: white;
  padding: 0.8em 0.7em 0 0.7em;
  background-color: #405061;
  line-height: 1em;
  margin-right: .8em;
  border-radius: 4px 0 0 4px;
}

.heroes .name {
  align-self: center;
}

입출력 프로퍼티 지정하기

컴포넌트의 입출력 프로퍼티는 @Directive나 @Component 메타데이터의 inputs, outputs 속성으로 지정하지 말고,

@Input(), @Output() 데코레이터로 지정하세요

 

@Input(), @Output() 데코레이터는 프로퍼티 이름과 같은 줄에 놓는 것을 권장합니다.

다른 줄로 분리할 때 가독성이 좋아지는 것이 명확할 때만 데코레이터를 프로퍼티 선언과 다른 줄로 분리하세요.

 

입출력 데코레이터를 사용하면 클래스 프로퍼티 중 어떤 것이 입력 프로퍼티이고 어떤 것이 출력 프로퍼티인지 쉽게 구분할 수 있습니다.

 

메타데이터 선언은 짧을수록 가독성이 좋아집니다.

 

입출력 프로퍼티에 별칭(alias)을 사용하지 마세요.

꼭 사용해야 하는 경우가 아니라면 입력 프로퍼티와 출력 프로퍼티에 별칭을 지정하지 마세요.


컴포넌트 외부에서 사용하는 프로퍼티 이름과 내부에서 사용하는 프로퍼티 이름이 다르면, 당연히 헷갈릴 수 밖에 없습니다.

별칭은 디렉티브 이름이 입력 프로퍼티와 같을 때, 디렉티브 이름으로는 프로퍼티를 확인하기 어려울 때만 사용하는 것이 좋습니다.

 

<예시>

@Component({
  selector: 'toh-hero-button',
  template: `<button type="button">{{label}}</button>`
})
export class HeroButtonComponent {
  // 권장 (한 줄로 입력하고, 가칭을 입력하지 않는 것이 가독성에 좋습니다.)
  @Output() heroChange = new EventEmitter<any>();
  @Input() label = '';
  // 비권장
  @Output('heroChangeEvent') heroChange = new EventEmitter<any>();
  @Input('labelAttribute') label: string;
}

클래스 멤버의 순서

메소드보다 프로퍼티를 먼저 선언하세요.

private 멤버보다 public 멤버를 먼저, 이들끼리는 알파벳 순서로 선언하세요.

클래스 멤버를 일관된 순서로 선언하면 코드의 가독성이 좋아지며, 이 컴포넌트가 어떤 역할을 하는지 정보를 제공할 수도 있습니다.

 

<예시>

export class ToastComponent implements OnInit {
  // public 속성
  message = '';
  title = '';

  // private 필드
  private defaults = {
    title: '',
    message: 'May the Force be with you'
  };
  private toastElement: any;

  // public 함수
  activate(message = this.defaults.message, title = this.defaults.title) {
    this.title = title;
    this.message = message;
    this.show();
  }

  ngOnInit() {
    this.toastElement = document.getElementById('toh-toast');
  }

  // private 함수 -> 로직이 복잡할 경우, 서비스로 이동
  private hide() {
    this.toastElement.style.opacity = 0;
    window.setTimeout(() => this.toastElement.style.zIndex = 0, 400);
  }

  private show() {
    console.log(this.message);
    this.toastElement.style.opacity = 1;
    this.toastElement.style.zIndex = 9999;
    window.setTimeout(() => this.hide(), 2500);
  }
}

 

복잡한 컴포넌트 로직은 서비스로 옮기세요.

컴포넌트에 작성하는 로직은 뷰와 관련된 로직으로만 제한하세요. 

뷰와 관계없는 로직은 모두 서비스로 옮기는 것이 좋습니다.

다른 컴포넌트에서 재사용할 수 있는 로직도 서비스로 옮기세요. 컴포넌트에는 그 역할에 맞는 기능만 간결하게 작성해야 합니다.

다른 컴포넌트에도 재사용할 수 있는 로직을 서비스 안에 함수로 작성하면 필요한 곳에 자유롭게 활용할 수 있습니다.

컴포넌트의 로직을 서비스로 옮기면 컴포넌트에 주입하는 의존성을 줄일 수 있으며, 컴포넌트에 꼭 필요한 로직만 작성하기 쉬워집니다.

컴포넌트 코드는 짧게, 간결하게, 꼭 필요한 로직만 작성하세요.

 

<예시>

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

import { Hero, HeroService } from '../shared';

@Component({
  selector: 'toh-hero-list',
  template: `...`
})
// BEFORE 
// (모든 로직[if ~ then ~ else]을 Component 상에서 처리 한다)
export class HeroListComponent implements OnInit {
  heroes: Hero[];
  constructor(private http: HttpClient) {}
  getHeroes() {
    this.heroes = [];
    this.http.get(heroesUrl).pipe(
      catchError(this.catchBadResponse),
      finalize(() => this.hideSpinner())
    ).subscribe((heroes: Hero[]) => this.heroes = heroes);
  }
  ngOnInit() {
    this.getHeroes();
  }

  private catchBadResponse(err: any, source: Observable<any>) {
    // log and handle the exception
    return new Observable();
  }

  private hideSpinner() {
    // hide the spinner
  }
}

// AFTER 
// (상세 로직을 Service 안에서 처리 한다.)
export class HeroListComponent implements OnInit {
  // Observable이 아닐 경우, subscribe 로직을 사용해주어야 합니다.
  heroes: Hero[] = [];
  constructor(private heroService: HeroService) {}
  getHeroes() {
    this.heroes = [];
    this.heroService.getHeroes()
      .subscribe(heroes => this.heroes = heroes);
  }
  ngOnInit() {
    this.getHeroes();
  }
}

output 프로퍼티에 접두사를 붙이지 마세요.

출력 프로퍼티로 이벤트를 보낼 때 이 이벤트 이름에 on을 붙이지 마세요.

on 접두사는 해당 이벤트를 받는 이벤트 핸들러 메소드 이름에 붙이세요. (handle + <Element명> + <이벤트명>으로 대체함)

버튼 클릭과 같은 내장 이벤트를 처리할 때도 이 방식을 사용합니다.

Angular에서 제공하는 문법 중 on-*을 붙이는 문법은 이벤트 바인딩 대체 문법 중 하나입니다. 그래서 이벤트 이름에 on을 붙이면, 이 이벤트를 바인딩할 때 on-onEvent와 같은 표현을 사용해야 합니다.

export class HeroComponent {
  // 권장(수동태를 사용합니다.)
  @Output() savedTheDay = new EventEmitter<boolean>();
  // 금지(on 접두사는 붙이지 않습니다.)
  @Output() onSavedTheDay = new EventEmitter<boolean>();
}

뷰에 사용하는 로직은 컴포넌트 클래스에 작성하세요.

뷰에 사용하는 로직은 템플릿이 아니라 컴포넌트 클래스에 작성하세요.

컴포넌트를 처리하는 로직은 템플릿과 클래스에 나눠서 작성하는 것보다 컴포넌트 클래스 한 곳에 작성하는 것이 좋습니다.

뷰와 관련된 로직을 템플릿에 두지 않고 클래스에 두면 컴포넌트를 테스트하기 편하며, 유지보수하기도 편하고 재사용하기에도 유리합니다.

 

@Component({
  selector: 'toh-hero-list',
  template: `
    <section>
      Our list of heroes:
      <toh-hero *ngFor="let hero of heroes" [hero]="hero">
      </toh-hero>
      Total powers: {{totalPowers}}<br>
      Average power: {{avgPower}}
    </section>
  `
})
export class HeroListComponent {
  heroes: Hero[];
  totalPowers = 0;

  // 함수가 아닌, Computed Props와 같이 사용하면 가독성이 더 좋아진다.
  get avgPower() {
    return this.totalPowers / this.heroes.length;
  }
}

입력값 초기화하기

TypeScript 컴파일러 옵션 중 --strictPropertyInitialization를 사용한다면 클래스 프로퍼티의 기본값을 생성자에서 반드시 지정해야 합니다. 

이 옵션이 활성화되면 옵션으로 지정되지 않은 클래스 프로퍼티 중 값이 할당되지 않는 것이 있다면 에러가 발생합니다.

기본적으로 Angular는 @Input 프로퍼티를 옵션으로 간주합니다. 

하지만 가능하다면 --strictPropertyInitialization 플래그의 취지에 맞게 기본값을 지정하는 것이 좋습니다.

 

컴포넌트를 사용하는 유저가 모든 어트리뷰트를 넘겨서 @Input 필드를 필수 항목으로 사용하고 싶을 수 있습니다.

이런 경우에는 기본값을 꼭 지정하세요.

<예시>

@Component({
  selector: 'toh-hero',
  template: `...`
})
export class HeroComponent {
  // 권장 (기본값 설정)
  @Input() id = 'default_id';
  // 비권장 (에러를 무시하기 위해 !를 붙이는 것은 권장하지 않습니다.)
  @Input() id!: string;
  // 권장 (선택 항목이고 기본값 설정이 어려울 경우 ?로 대체 후, 관련 props 사용 시 null 체크 후 사용)
  @Input() id?: string;
  process() {
    if (this.id) {
      // ...
    }
  }
}

디렉티브

디렉티브는 엘리먼트를 확장하는 용도로 사용하세요.

어트리뷰트 디렉티브는 템플릿(HTML Element)에 필요한 로직(if ~ else, 연산, 이벤트 처리 등)을 구현할 때 사용하세요.

어트리뷰트([커스텀 Element 속성명]) 디렉티브에는 템플릿이 없습니다.

엘리먼트 하나에는 여러 개의 어트리뷰트 디렉티브가 적용될 수도 있습니다.

<예시>

app/shared/highlight.directive.ts (파일명과 클래스명에는 커스텀 접두사[프로젝트명 약어]를 넣지 않는다.)

@Directive({
  selector: '[tohHighlight]'
})
export class HighlightDirective {
  @HostListener('mouseover') onMouseEnter() {
    // do highlight work
    // 커스텀 tohHighlight 속성을 사용하면, 원하는 HTML Element에 event 발생 시, 동일한 로직 수행 가능
  }
}

app/app.component.html

<div tohHighlight>Bombasta</div>

HostListener/HostBinding 데코레이터 vs. host 메타데이터

@Directive나 @Component 데코레이터의 host 프로퍼티를 활용할 수 있는 로직은 @HostListener와 @HostBinding으로 사용하는 것을 권장합니다. (기본적으로 어노테이션의 속성인 host도 이벤트를 바인딩할 수 있는 기능을 제공하지만, 후자가 훨씬더 직관적이고 편리하다.)

코드의 일관성을 유지하세요.

@HostBinding과 연결된 프로퍼티나 @HostListener에 연결된 메소드는 이 데코레이터가 지정된 디렉티브 클래스 안 어디에나 선언하기만 하면 됩니다. 

하지만 host 메타데이터 프로퍼티를 사용하면, 이렇게 프로퍼티나 메소드를 데코레이터와 클래스 모드 양쪽에 선언해야 합니다.

<예시>

import { Directive, HostBinding, HostListener } from '@angular/core';

@Directive({
  selector: '[tohValidator]'
  // 비권장 (아래와 같이 @HostBinding, @HostListener가 더 깔끔하다.)
  ,host: {
    '[attr.role]': 'role',
    '(mouseenter)': 'onMouseEnter()'
  }
})
export class ValidatorDirective {
  // 권장
  @HostBinding('attr.role') role = 'button';
  @HostListener('mouseenter') onMouseEnter() {
    // do work
  }
}

서비스

서비스는 싱글턴으로 사용하세요.

서비스는 같은 인젝터를 사용해서 싱글턴으로 사용하세요. 서비스는 데이터와 함수를 공유하는 방식으로 사용해야 합니다.

-> 싱글턴으로 사용하지 않으면, 모듈이 생성될 때 인스턴스가 똑같이 생성된다. (그렇게 동작해야할 경우에는 기능 모듈에서 프로바이더 설정을 명시해준다.)

서비스는 여러 컴포넌트에 사용되는 기능을 한 곳에 모아두기 위해 만드는 것입니다.

서비스는 인-메모리 데이터를 공유하는 방식으로 사용하는 것이 가장 좋습니다.

 

단일 책임

서비스에는 그 서비스를 구현하는 목적에 해당하는 기능만 구현하세요.

기존에 있는 서비스의 범위에 벗어나는 기능이 필요할 때 새로운 서비스를 만드세요.

서비스에 여러 용도의 기능을 구현하면 테스트하기 힘들어 집니다.

서비스에 여러 용도의 기능을 구현하면, 컴포넌트나 다른 서비스에 이 서비스를 의존성으로 주입할 때 모든 기능을 한 번에 가지고 다녀야 합니다.

 

서비스 프로바이더

서비스는 @Injectable 데코레이터를 사용해서 애플리케이션 최상위 인젝터에 등록하세요.

Angular 인젝터는 계층에 따라 구성됩니다.

애플리케이션 최상위 인젝터에 서비스 프로바이더를 등록하면, 이 서비스의 인스턴스는 모든 클래스에 공유되며, 서비스의 스테이트나 메소드를 함께 활용할 수 있습니다. 서비스는 이런 방식으로 사용하는 것이 가장 좋습니다.

@Injectable 데코레이터를 사용해서 서비스를 등록하면, Angular CLI's와 같은 툴로 빌드할 때 앱에서 실제로 사용하지 않는 서비스를 트리 셰이킹으로 모두 제거할 수 있습니다.

같은 서비스를 의존성으로 주입받는 두 컴포넌트가 서로 다른 인스턴스를 사용하는 것은 서비스를 구현의도에 맞게 사용하는 방법이 아닙니다. 이 방식은 두 컴포넌트가 사용하는 서비스의 인스턴스를 명확하게 분리할 필요가 있을 때만 사용하는 방식입니다.

 

<예시>

app/heroes/shared/hero.service.ts

@Injectable()
export class HeroService {
  constructor(private http: HttpClient) { }

  getHeroes() {
    return this.http.get<Hero[]>('api/heroes');
  }
}

app/shared/common.service.ts

// 아래와 같인 속성값을 줄 경우, 싱글톤 형식으로 인스턴스가 생성됩니다.
@Injectable({
  providedIn: 'root',
})
export class CommonService {

}

@Injectable() 클래스 데코레이터를 사용하세요.

서비스를 토큰으로 참조할 때 @Inject 파라미터 데코레이터 대신 @Injectable() 클래스 데코레이터를 사용하세요.

-> @Inject 어노테이션을 사용하면, Inject하는 모든 객체들의 수만큼 선언해주어야 하므로, 복잡해진다.

서비스에도 의존성으로 주입하는 객체가 있을 수 있습니다. 이 때 Angular 의존성 주입 메커니즘에 따라 올바른 의존성 객체를 주입하려면, 서비스 생성자에 의존성 객체의 타입을 명시해야 합니다.

서비스를 토큰으로 주입하는 경우를 생각해보면, 생성자의 인자마다 @Inject()를 지정하는 것보다 @Injectable()로 서비스를 등록하고 토큰으로 바로 지정하는 것이 훨씬 간단합니다.

// 권장
@Injectable()
export class HeroArena {
  constructor(
    private heroService: HeroService,
    private http: HttpClient) {}
}

// 비권장
export class HeroArena {
  constructor(
      @Inject(HeroService) private heroService: HeroService,
      @Inject(HttpClient) private http: HttpClient) {}
}

데이터 서비스

서버와 통신할 때는 서비스를 사용하세요.

데이터를 가져오거나 가공하는 로직은 서비스에 작성하세요.

XHR 통신으로 데이터를 가져오거나 로컬 스토리지, 메모리에 데이터를 저장하는 로직은 서비스에 작성하세요.

컴포넌트는 화면을 담당하며, 화면에 표시된 정보를 모으는 것까지만 컴포넌트의 역할입니다. 

어딘가에서 데이터를 가져오는 로직은 컴포넌트가 담당하는 것이 아니며, 이 역할을 담당하는 무언가를 활용하기만 할 뿐입니다. 

데이터를 처리하는 로직은 모두 서비스로 옮기고, 컴포넌트는 화면을 담당하는 역할에 집중하도록 하세요.

데이터를 가져오는 로직을 컴포넌트에서 제거하면 목업 서비스를 활용할 수 있기 때문에 테스트하기 더 편합니다.

헤더를 지정하거나 HTTP 메소드를 선택하는 로직, 캐싱, 에러 처리, 실패했을 때 재시도하는 로직 등 데이터를 처리하는 로직은 컴포넌트와 직접적인 연관이 없습니다.

 로직들은 데이터 서비스 안쪽에 구현하는 것이 좋습니다. 그러면 데이터를 사용하는 쪽과 관계없이 로직을 수정하거나 확장할 수 있으며, 컴포넌트에 목업 서비스를 주입해서 테스트하기도 편해집니다.

 

라이프싸이클 후킹 함수

라이프싸이클 후킹 인터페이스 구현

라이프싸이클 후킹 인터페이스를 구현하세요.

라이프싸이클 인터페이스는 Angular 컴포넌트의 이벤트 시점을 활용할 수 있는 메소드를 미리 정의해 둔 것입니다. 이 메소드를 그대로 활용하면 오타를 내거나 문법을 잘못 사용하는 실수를 방지할 수 있습니다.

@Component({
  selector: 'toh-hero-button',
  template: `<button type="button">OK</button>`
})
// 권장 (인터페이스가 없으면, ngOnInit()에 대한 오타가 날 수도 있음)
export class HeroButtonComponent implements OnInit {
  ngOnInit() {
    console.log('The component is initialized');
  }
}

 

참고

파일 템플릿과 자동완성 플러그인(snippet)

코딩 스타일을 일관되게 유지하려면 파일 템플릿이나 자동완성 기능을 활용하세요. 

템플릿, 코드 자동 완성 툴이나 IDE는 다음과 같은 것이 있습니다.

  • VS Code : Angular TypeScript Snippets for VS Code

 

출처


  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기