Angular.io - GUIED - ARCHITECTURE

이 글은 Angular.io의 Guied Architecture를 번역한 글입니다. 오역 및 의역이 있을 수 있습니다.


Angular는 HTML과 javascript혹은 javascript로 컴파일되는 typescript로 클라이언트 어플리케이션을 만들기 위한 프레임워크입니다.

역주 - Angularjs라고 불리던 프레임워크가 버전업 되면서 Angular로 통칭하게 되었습니다.

Angular는 몇몇의 코어 라이브러리들과 부수적인 라이브러리들로 구성됩니다.

당신은 Angular의 마크업이 포함된 HTML템플릿을 작성하고 그것들을 조작하기 위해 컴포넌트 클래스를 추가합니다. 또한 어플리케이션로직은 서비스에서 처리합니다. 그리고 컴포넌트와 서비스는 모듈속에 넣습니다.

그후, 부트스트래핑된 루트모듈을 실행합니다. Angular는 제공된 지침에 따라 당신의 어플리케이션의 컨텐츠를 브라우저에 보여주고 유저의 인터랙션에 응답합니다.

물론 이것은 빙산의 일각입니다. 이 페이지에서 자세히 설명할 겁니다. 지금은 큰 그림에 집중해봅시다.

overview2

이 다이어그램은 Angular 어플리케이션을 크게 여덟가지로 구분합니다.

  • Modules
  • Components
  • Templates
  • Metadata
  • Data binding
  • Directives
  • Services
  • Dependency injection

이것들에 대해 배워봅시다.

이 페이지의 코드들은 이곳을 참고하세요 live example / downloadable example

Modules (모듈)

module

Angular는 모듈 친화적이며 Angular modules 혹은 ngModules이라는 모듈시스템을 자체적으로 가지고 있습니다. Angular modules은 매우 중요합니다. 이 페이지에서도 소개하지만 여기서 더 자세히 설명합니다.

모든 Angular 앱은 최소한 the root modules 이라는 Angular modules을 하나씩은 가지고 있습니다. 보통 AppModule이라 부릅니다.

작은 어플리케이션이라면 root modules하나만 존재하는 경우도 있지만, 일반적으로 많은 모듈들이 어플리케이션의 도메인, 워크플로우등의 기능들을 위해서 유기적으로 동작합니다.

Angular module은 어떤 모듈이든 간에 @NgModule 데코레이터를 사용합니다.

데코레이터는 javascript 클래스를 변형시키는 함수입니다. Angular는 메타데이터를 클래스에 적용시키기 위한 많은 데코레이터들을 가지고 있고 이는 그 클래스의 의미가 뭔지, 어떻게 동작하는지를 나타냅니다.

데코레이터에 대해 더 알아보기

NgModule은 모듈의 특성을 기술하는 하나의 메타데이터 객체를 취하는 함수입니다. 가장 중요한 특성은 다음과 같습니다.

  • declarations : 해당 모듈에 포함된 view 클래스. Angular는 components, directives, pipes라는 세가지 종류의 view class가 있습니다.
  • exports : declarations중에서 다른 모듈에서 사용할 모듈을 기술.
  • imports : 다른곳에서 export된 모듈 중 이 컴포넌트 템플릿에서 필요한 모듈을 기술.
  • providers : 글로벌 컬렉션을 위한 service의 생성자. 이는 앱 안의 어느 곳에서든 사용할 수 있습니다.
  • bootstrap : root component라고 하는, 모든 다른 앱 뷰에서 호스트 하는 메인 어플리케이션 뷰.

다음은 간단한 root modules의 예 입니다.

// src/app/app.module.ts
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
@NgModule({
  imports: [BrowserModule],
  providers: [Logger],
  declarations: [AppComponent],
  exports: [AppComponent],
  bootstrap: [AppComponent],
})
export class AppModule {}

여기서 AppComponent의 export는 그냥 export를 하는 방법을 보여주기 위함입니다. 사실 여기서는 필요하지 않습니다. 다른 모듈들에서 root module을 import할 필요가 없기 때문에 root module이 export되는 것은 무의미합니다.

root module을 bootstrapping해서 어플리케이션을 실행하세요. main.ts에서 AppModule을 bootstrapping하는 다음과 같은 형태가 될겁니다.

// src/main.ts
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { AppModule } from "./app/app.module";

platformBrowserDynamic().bootstrapModule(AppModule);

Angular modules vs. JavaScript modules

@NgModule을 사용한 Angular module은 Angular의 기본적인 특성입니다.

javascript또한 javascript 객체를 관리하기 위한 그 자체 모듈 시스템을 가지고 있습니다. 각각의 javascript 파일은 모듈이고 정의된 모든 객체는 모듈에 속합니다. 모듈은 export 키워드를 통해서 공개됩니다. 또한 import 키워드로 공개된 객체에 접근합니다.

import { NgModule } from "@angular/core";
import { AppComponent } from "./app.component";
export class AppModule {}

javascrirpt 모듈에 대해서 알아보기

이 두가지는 상호보완적인 모듈시스템입니다. 두가지 모두 적절히 사용하세요.

Angular libraries

library-module

Angular 모듈은 javascript 모듈컬렉션으로 제공합니다. 라이브러리 모듈이라고 봐도 무방합니다. 각 모듈의 이름은 @angular 접두사로 시작합니다. npm을 통해서 설치할 수 있고 javascript의 import문으로 불러올 수 있습니다.

예를들어 @angular/coreComponent 데코레이터를 불러오기 위해서는 다음과 같이 할 수 있습니다.

import { Component } from "@angular/core";

import문을 통해서 Angular라이브러리를 불러 올 수 있습니다.

import { BrowserModule } from "@angular/platform-browser";

그리고 root module에서 BrowserModule이 필요하다고 하면, 이를 *@NgModule에 추가합니다.

이는 Angular와 javascript의 모듈시스템을 같이 이용한 예입니다.

두 시스템 모두 import와 export라는 키워드를 공유하기 때문에 헷갈릴 수 있습니다. 하지만 많이 해보면 알게될겁니다.

Angular modules에 대해 알아보기

Components (컴포넌트)

hero-component

컴포넌트는 view라고 하는 화면의 부분을 제어합니다. 예를 들어 다음의 view는 구성요소에 의해 제어됩니다.

  • app root의 네비게이션 링크
  • 히어로들의 리스트
  • 히어로 에디터

역주 : angular.io의 Guide에서 히어로 리스트를 작성하고 수정하는 앱의 예제 코드를 통해서 설명하고 있기 때문에 이런 예를 든 것 같습니다.

당신은 클래스안에서 컴포넌트의 어플리케이션 로직(view에서 어떤일을 하는지)을 정의합니다. 클래스는 API 프로퍼티와 메소드를 통해 view와 상호작용합니다.

예를들어 다음의 HeroListComponent는 서비스로부터 얻어온 히어로들의 배열을 반환하는 heros프로퍼티를 가지고 있습니다. 또한 HeroListComponent는 유저가 히어로 리스트를 클릭했을 때 selectedHero를 설정하는 selectHero() 메소드를 가지고 있습니다.

// src/app/hero-list.component.ts (class)
export class HeroListComponent implements OnInit {
  heroes: Hero[];
  selectedHero: Hero;

  constructor(private service: HeroService) { }

  ngOnInit() {
    this.heroes = this.service.getHeroes();
  }

  selectHero(hero: Hero) { this.selectedHero = hero; }
}

Angular는 사용자의 움직임에 따라 컴포넌트들을 생성, 수정, 제거합니다. 당신의 앱은 위에서 **ngOnInit()**이 선언 된 것처럼 라이프사이클 훅을 통해서 특정 액션을 취할 수 있습니다.

Templates

template

당신은 템플릿으로 컴포넌트 뷰를 정의합니다. 템플릿은 컴포넌트를 어떻게 렌더링할지 Angular에게 알려주는 HTML폼 입니다.

템플릿은 몇가지를 제외하곤 기존의 HTML과 거의 흡사합니다. 다음은 HeroListComponent를 위한 템플릿입니다.

<!-- src/app/hero-list.component.html -->
<h2>Hero List</h2>
<p><i>Pick a hero from the list</i></p>
<ul>
  <li *ngFor="let hero of heroes" (click)="selectHero(hero)">{{hero.name}}</li>
</ul>

<hero-detail *ngIf="selectedHero" [hero]="selectedHero"></hero-detail>

<h2><p> 같은 엘리먼트들은 기존의 HTML과 같지만 {% raw %} {{hero.name}} {% endraw %}, *ngFor, (click), [hero], <hero-detail>

Angular의 템플릿 문법 을 사용합니다.

마지막줄의 <hero-detail>HeroDetailComponent에서 만든 커스텀엘리먼트입니다.

HeroDetailComponentHeroListComponent와는 약간 다른 컴포넌트 입니다. HeroDetailComponent(코드상엔 안보이지만)는 유저가 선택한 히어로를 HeroListComponent에 표시해줍니다. HeroDetailComponentHeroListComponent의 자식 컴포넌트입니다.

<hero-detail>이 기존의 HTML사이에 얼마나 잘 녹아들어가 있는지 보세요. 커스텀 컴포넌트는 기존의 컴포넌트들과 같은 레이아웃으로 위화감 없이 공존합니다.

component-tree

Metadata (메타데이터)

metadata

메타데이터는 Angular에게 클래스를 처리하는 방법을 알려줍니다.

위에서 HeroListComponent의 코드를 보면 그냥 클래스라는것을 알 수 있습니다. Angular가 개입하고 있는 부분이 전혀 없습니다. 사실, HeroListComponent는 Angular에게 이 클래스가 컴포넌트임을 알려주기 전까지는 그냥 클래스에 불과합니다.

Angular에게 HeroListComponent가 컴포넌트임을 알려주려면 메타데이터를 추가해줘야 합니다.

typescript에서 데코레이터를 통해서 메타데이터를 추가할 수 있습니다. 아래는 HeroListComponent의 메타데이터입니다.

// src/app/hero-list.component.ts (metadata)
@Component({
  moduleId: module.id,
  selector: "hero-list",
  templateUrl: "./hero-list.component.html",
  providers: [HeroService],
})
export class HeroListComponent implements OnInit {
  /* . . . */
}

@Component 데코레이터는 바로 아래있는 클래스를 컴포넌트로 인지합니다.

@Component 데코레이터에는 Angular가 뷰를 만들고 표시하기 위해 필요한 정보를 담고있는 객체가 필요합니다.

아래는 @Component를 구성하는 몇가지 요소들입니다.

  • moduleId : 마치 templateUrl처럼 모듈을 기준의 기본 주소(module.id)를 설정합니다.
  • selector : Angular가 부모 HTML에서 <hero-list>의 인스턴스를 만들고 삽입할 수 있도록 하는 CSS선택자입니다. 예를 들어 HTML에서 <hero-list></hero-list>를 삽입하면 Angular는 거기에 HeroListComponent를 만들어줍니다.
  • templateUrl : 모듈을 기준으로한 HTML 템플릿의 상대주소입니다.
  • providers : 컴포넌트에 필요한 서비스들의 의존성을 주입하기 위한 배열 객체입니다. 예제에서 처럼 컴포넌트가 히어로 목록을 표시하기 위해 HeroService가 필요함을 Angular에게 알려주는 방법중 하나입니다.

template-metadata-component

@Component의 메타데이터는 당신이 컴포넌트를 구성하기 위해 지정한 빌딩블럭을 어디서 가져올수 있는지 Angular에게 알려줍니다.

템플릿, 메타데이터, 그리고 컴포넌트가 합쳐져서 뷰를 구성하게됩니다.

다른 메타데이터 데코레이터도 비슷한 방식으로 적용시켜보세요. 자주 사용되는 데코레이터로는 @Injectable, @Input, @Output가 있습니다.

아키텍처상에서 중요한것은 Angular가 무엇을 할지 알려주기위해 메타데이터를 추가해줘야 한다는 것입니다.

Data binding (데이터 바인딩)

프레임워크가 없다면 당신은 유저의 액션이나 값의 업데이트를 반영하기 위해 데이터값을 HTML에 넣어줘야 할것입니다.

푸쉬/풀 로직을 작성하는것은 지루하고 에러가 발생하기도 쉽습니다. jQuery를 사용해왔던 프로그래머라면 더욱 힘들었을겁니다.

databinding

Angular는 템플릿과 컴포넌트를 일치시키는 데이터 바인딩을 지원합니다. HTML템플릿에 바인딩 마크업을 추가하는것으로 Angular에게 어떻게 둘을 연결할 지를 알려줄수 있습니다.

그림에서 처럼 데이터바인딩에는 네가지형태가 있습니다. 각각의 형태는 DOM으로 보내는것, DOM으로 부터 받는것, 혹은 양쪽의 방향성을 가지고 있습니다.

HeroListComponent예제에서는 세가지 형태가 들어있습니다.

<!-- src/app/hero-list.component.html (binding) -->
<li>{{hero.name}}</li>
<hero-detail [hero]="selectedHero"></hero-detail>
<li (click)="selectHero(hero)"></li>
  • {% raw %}{{hero.name}}{% endraw %} interpolation<li>엘리먼트 속에 hero.name 프로퍼티값을 보여줍니다.
  • [hero] property bindingselectedHero의 값을 부모 컴포넌트인 HeroListComponent에서 자식 컴포넌트인 HeroDetailComponenthero 프로퍼티로 전달합니다.
  • (click) event binding은 히어로 이름을 클릭했을 때 컴포넌트의 selectHero 메소드를 호출합니다.

양방향 데이터바인딩은 데이터바인딩의 네가지 방법중 네번째 형태입니다. ngModel디렉티브를 사용해서 하나의 표기법으로 프로퍼티를 결합하고 이벤트를 바인딩합니다. 아래 예제는 HeroDetailComponent 템플릿의 예제입니다.

<!-- src/app/hero-detail.component.html (ngModel) -->
<input [(ngModel)]="hero.name" />

양방향 데이터바인딩에서는 프로퍼티 바인딩과 마찬가지로 데이터 프로퍼티값이 컴포넌트에서 인풋박스로 흘러갑니다. 또한 유저의 값 변경은 반대로 컴포넌트로 흘러가서 최근의 값으로 프로퍼티를 리셋합니다.

Angular의 모든 데이터 바인딩 프로세스는 자식 컴포넌트를 통해서 루트 컴포넌트로 javascript 이벤트 사이클 한번에 하나씩 처리됩니다.

component-databinding

데이터 바인딩은 템플릿과 컴포넌트의 커뮤니케이션에 중요한 역할을 합니다.

또한 데이터 바인딩은 부모와 자식 컴포넌트 사이의 커뮤니케이션에서도 중요한 역할을 합니다.

Directive (디렉티브)

component-databinding

Angular 템플릿은 유동적입니다. Angular는 디렉티브에 따라서 DOM을 변형시킵니다.

디렉티브는 @Directive 데코레이터의 클래스입니다. 컴포넌트는 디렉티브와 템플릿의 조합입니다. @Component데코레이터는 사실 @Directive 데코레이터를 템플릿 형태로 확장한것입니다.

컴포넌트가 디렉티브의 확장이긴 하지만 컴포넌트는 Angular 어플리케이션에서 중요한 구성요소이기 때문에 디렉티브와 구분됩니다.

디렉티브는 속성(attribute) 디렉티브와 구조(structural) 디렉티브로 나뉩니다.

디렉티브는 attribute처럼 엘리먼트의 태그안에서 쓰이거나 태그의 이름으로 쓰이기도 합니다. 또한 할당하거나 바인딩되는 대상으로 쓰이는 경우도 빈번합니다.

구조 디렉티브는 DOM의 엘리먼트를 추가하고, 지우고, 대체시켜서 레이아웃을 바꿉니다.

예제에서는 두가지 내장 디렉티브를 사용합니다.

<!--src/app/hero-list.component.html (structural)-->
<li *ngFor="let hero of heroes"></li>
<hero-detail *ngIf="selectedHero"></hero-detail>
  • *ngFor은 Angular에게 heros 리스트에서 hero를 하나씩 <li>태그로 넣으라고 알려줍니다.
  • *ngIfseletedHero가 있을때만 HeroDetail컴포넌트를 포함시킵니다.

속성 디렉티브는 존재하는 엘리먼트의 외형 혹은 행동을 변화시킵니다. 템플릿에서는 일반적인 HTML attribute의 이름처럼 보입니다.

양방향 데이터 바인딩으로 구현된 ngModel은 속성 디렉티브의 예제입니다. ngModel은 프로퍼티를 변화시키고 변경이벤트에 대응해서 존재하는 엘리먼트(보통 <input>태그)의 행동을 변화시킵니다.

<!--src/app/hero-detail.component.html (ngModel)-->
<input [(ngModel)]="hero.name" />

Angular에는 레이아웃 구조를 바꾸거나(ngSwitch와 같은) DOM 엘리먼트, 컴포넌트를 수정하는(ngStyle,ngClass와 같은) 몇가지 디렉티브들을 가지고 있습니다.

물론 커스텀 디렉티브를 만들수도 있습니다. HeroListComponent와 같은 컴포넌트가 커스텀 엘리먼트의 예제입니다.

Services (서비스)

service

서비스는 당신의 앱에서 필요한 함수나 기능등 넓은 카테고리를 포괄합니다.

거의 모든것이 서비스라고 할 수 있습니다. 일반적으로는 잘 정의 된 클래스를 가르킵니다. 특정한 일을 잘 수행해야만 합니다.

예를들어 다음과 같습니다.

  • logging service
  • data service
  • message bus
  • tax calculator
  • application configuration

Angular는 서비스에대해 구체적으로 정의하지 않습니다. 클래스를 기본으로하는 서비스도 없을뿐만 아니라 서비스를 등록할 곳도 없습니다.

하지만 서비스는 Angular 어플리케이션의 근간입니다. 컴포넌트의 큰 축이기도합니다.

다음예제는 브라우저콘솔에 로그를 출력하는 서비스 클래스의 예제입니다.

// src/app/logger.service.ts (class)
export class Logger {
  log(msg: any) {
    console.log(msg);
  }
  error(msg: any) {
    console.error(msg);
  }
  warn(msg: any) {
    console.warn(msg);
  }
}

아래의 HeroService는 heros를 업데이트하는데 Promise를 사용합니다. HeroServiceLogger, 서버와의 커뮤니케이션을 위한 BackendService에 의존적입니다.

// src/app/hero.service.ts (class)
export class HeroService {
  private heroes: Hero[] = [];

  constructor(
    private backend: BackendService,
    private logger: Logger) { }

  getHeroes() {
    this.backend.getAll(Hero).then( (heroes: Hero[]) => {
      this.logger.log(`Fetched ${heroes.length} heroes.`);
      this.heroes.push(...heroes); // fill cache
    });
    return this.heroes;
  }
}

서비스는 어느곳에나 있습니다.

컴포넌트는 서비스에 의존해야합니다. 컴포넌트는 서버에서 데이터를 가져오거나, 유저의 입력을 검증하거나, 콘솔에 직접 출력하는 등의 일을 하지 않습니다. 이런 작업들은 서비스에 위임합니다.

컴포넌트의 역할은 유저에게 경험을 제공하는것 뿐입니다. 뷰(템플릿으로 렌더링 된)와 어플리케이션 로직(모델의 개념을 포함하기도 하는)을 중계하는 역할을 합니다. 훌륭한 컴포넌트는 데이터바인딩을 위해 프로퍼티와 메소드를 드러냅니다. 기타 다른 것들은 서비스에 위임합니다.

Angular는 그들의 원칙을 강요하지않습니다. 3000줄짜리 쓸데없는 컴포넌트를 작성해도 괜찮습니다.

Angular는 의존성주입을 통해서 어플리케이션 로직을 서비스에 반영하고 컴포넌트에서 서비스를 쉽게 사용할 수 있게 함으로서 이런 원칙을 지키는데 도움을 줍니다.

Dependency injection (의존성 주입)

dependency-injection

의존성 주입은 클래스의 새로운 인스턴스에 필요한 의존성을 공급해주는 방법입니다. 대부분은 서비스를 주입해줍니다. Angular는 컴포넌트에 필요한 서비스들의 의존성을 주입해 줍니다.

Angular는 생성자 파라미터를 통해서 컴포넌트에 어떤 서비스가 필요한지를 알 수 있습니다. 예를들어 당신의 HeroListComponentHeroService가 필요할 때는 다음처럼 합니다.

// src/app/hero-list.component.ts (constructor)
constructor(private service: HeroService) { }

Angular에서 컴포넌트를 만들 때, 컴포넌트에 필요한 서비스를 위해서 우선 injector를 호출합니다.

injector는 이전에 만들어져있던 서비스의 컨테이너를 관리합니다. 요청된 서비스가 컨테이너에 포함되어있지 않은경우, injector는 해당 서비스를 만들어서 컨테이너에 추가한 후 Angular에 반환합니다. 그리고 모든 서비스에 대한 처리가 끝난 후에, 서비스들을 인수로 하는 컴포넌트의 생성자를 호출할 수 있습니다. 이것이 의존성주입입니다.

HeroService의 주입은 아래와 같은 프로세스를 거칩니다.

injector-injects

만약 injector가 HeroService를 가지고 있지 않다면 어떻게 될까요?

즉, 미리 HeroService의 provider를 injector에 등록해놓아야 합니다. provider는 일반적으로 서비스 클래스 자체를 생성, 반환합니다.

당신은 providers를 모듈이나 컴포넌트에 등록할 수 있습니다.

보통, 같은 인스턴스의 서비스를 어디에서나 사용하기 위해서 루트모듈에 등록해 놓습니다.

// src/app/app.module.ts (module providers)
providers: [
  BackendService,
  HeroService,
  Logger
],

또는 컴포넌트레벨에서 @Component 메타데이터의 프로퍼티로 providers를 등록해 줄 수 있습니다.

// src/app/hero-list.component.ts (component providers)
@Component({
  selector:    'hero-list',
  templateUrl: './hero-list.component.html',
  providers:  [ HeroService ]
})

컴포넌트레벨에서 서비스를 등록한다는 것은 컴포넌트의 새로운 인스턴스가 생성될 때 마다 새로운 서비스의 인스턴스가 생성된다는 것을 의미합니다.

의존성 주입에서의 요점은

  • 의존성 주입은 Angular 프레임워크와 연결되어있고 어디서나 사용합니다.
  • injector의 주요 메커니즘은
    • injector는 생성된 서비스 인스턴스의 컨테이너를 관리합니다.
    • injector는 provider로 부터 새로운 인스턴스를 생성할 수 있습니다.
  • 서비스를 생성하는 방법은 provider를 사용하는겁니다.
  • injector에 provider를 등록합니다.

Wrap up

Angular의 기초가 되는 여덟가지를 배웠습니다.

  • Modules
  • Components
  • Templates
  • Metadata
  • Data binding
  • Directives
  • Services
  • Dependency injection

이것들은 모든 Angular 어플리케이션을 위한 기반이 될겁니다. 하지만 이것이 당신이 알아야 할 전부는 아닙니다.

아래는 Angular 어플리케이션을 위한 또 다른 중요한 것들의 목록입니다. 여기의 대부분은 도큐먼트에서 다룹니다.

Animation: Angular Animation 라이브러리로 CSS나 애니메이션에 대한 깊은 지식없이 컴포넌트 애니메이션을 구현합니다.

Change detection: Angular에서 컴포넌트의 프로퍼티가 변경되었을 때, 어떻게 화면을 업데이트하고 비동기를 인터셉트해서 바뀐것을 감지하는지에 대해 다룹니다.

Events: event 도큐먼트에서는 컴포넌트가 어떻게 이벤트를 발생시키고 감지하는지에 대해 다룹니다.

Forms: HTML을 기반으로한 유효성검사와 더티체킹 으로 복잡한 데이터폼을 다루는 시나리오를 도와줍니다.

HTTP: 데이터를 받아오거나 저장하기 위한 서버와의 커뮤니케이션과 HTTP 클라이언트의 서버사이드 액션입니다.

Lifecycle hooks: 라이프사이클 인터페이스를 통해서 컴포넌트가 생성되고 지워지는 순간을 활용합니다.

Pipes: 템플릿에서 값이 보여지는 방식을 통해 사용자경험을 향상시킵니다. currency pipe 표현식은 다음과 같습니다.

price | currency:'USD':true

이는 42.33 을 $42.33 과 같이 보여줍니다.

Router: 클라이언트사이드에서 페이지를 탐색하고 브라우저에서 떠나지않게합니다.

Testing: Angular와 인터랙션할 때 Angular Testing Platform을 이용해서 당신의 어플리케이션에서 유닛테스트를 진행합니다.