출시·고도화 중
Angular 안내서 · 3/6
Angular 애플리케이션은 몇 가지 핵심 개념으로 이루어집니다. 템플릿을 가진 스탠드얼론 컴포넌트, 반응형 상태를 담는 시그널, 의존성 주입으로 공유하는 서비스, 그리고 라우터입니다. 이 장에서는 각각을 작고 동작하는 예제로 살펴봅니다.
컴포넌트는 @Component 데코레이터가 붙은 TypeScript 클래스입니다. selector, 템플릿, 그리고 템플릿에서 쓰는 다른 컴포넌트나 디렉티브를 imports에 적습니다. 컴포넌트는 기본적으로 스탠드얼론이어서 NgModule이 필요 없습니다. 입력과 출력은 input()과 output() 함수로 선언합니다.
import { Component, input, output } from '@angular/core';
export interface Hero {
id: number;
name: string;
}
@Component({
selector: 'app-hero-card',
template: `
<h3>{{ hero().name }}</h3>
<button type="button" (click)="selected.emit(hero())">Select</button>
`,
})
export class HeroCard {
readonly hero = input.required<Hero>();
readonly selected = output<Hero>();
}입력도 시그널이므로 템플릿에서는 hero()로 읽습니다. 부모 컴포넌트는 이 컴포넌트를 HTML 요소처럼 쓰며, 자신의 imports에 HeroCard를 추가해야 합니다.
템플릿은 HTML에 몇 가지 문법을 더한 것입니다.
| 문법 | 뜻 |
|---|---|
{{ value }} | 텍스트 보간 |
[property]="expr" | 요소나 컴포넌트의 속성에 값 연결 |
(event)="handler($event)" | DOM 이벤트나 컴포넌트 출력 듣기 |
[(value)]="state" | 양방향 바인딩(예: model() 입력) |
[class.active]="isActive" | CSS 클래스 켜고 끄기 |
내장 제어 흐름 블록은 예전의 *ngIf, *ngFor 디렉티브를 대신하며 따로 가져올 필요가 없습니다.
<ul>
@for (hero of heroes(); track hero.id) {
<li>
<app-hero-card [hero]="hero" (selected)="select($event)" />
</li>
} @empty {
<li>No heroes yet.</li>
}
</ul>
@if (current(); as hero) {
<p>Selected: {{ hero.name }}</p>
} @else {
<p>Pick a hero.</p>
}
@switch (mode()) {
@case ('edit') { <p>Editing</p> }
@default { <p>Read-only view</p> }
}@for에는 track 식이 꼭 필요합니다. 보통 고유 id를 지정하며, 목록이 바뀔 때 Angular가 DOM 노드를 재사용하는 기준이 됩니다. @empty는 목록이 비었을 때 표시됩니다. 이 밖에 템플릿 일부를 지연 로딩하는 @defer와 템플릿 지역 변수를 선언하는 @let도 있습니다.
시그널은 값이 바뀌면 Angular에 알려 주는 값입니다. computed()는 다른 시그널에서 읽기 전용 값을 파생하며, 원본이 바뀔 때만 다시 계산합니다. effect()는 읽은 시그널이 바뀔 때마다 로그 출력이나 저장소 동기화 같은 부수 효과를 실행합니다.
import { Component, computed, effect, signal } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<p>{{ count() }} x 2 = {{ double() }}</p>
<button type="button" (click)="increment()">+1</button>
`,
})
export class Counter {
protected readonly count = signal(0);
protected readonly double = computed(() => this.count() * 2);
constructor() {
effect(() => console.log('count is now', this.count()));
}
increment() {
this.count.update((value) => value + 1);
}
}값을 통째로 바꿀 때는 set()을, 현재 값에서 다음 값을 계산할 때는 update()를 씁니다. 파생 값으로 표현할 수 있는 것은 effect()보다 computed()로 만드는 것이 좋습니다.
여러 컴포넌트가 함께 쓰는 로직은 서비스에 둡니다. @Injectable({ providedIn: 'root' })를 붙이면 앱 전체에서 인스턴스 하나를 공유하고, inject()로 꺼내 씁니다.
import { Injectable, signal } from '@angular/core';
import { Hero } from './hero-card';
@Injectable({ providedIn: 'root' })
export class HeroService {
private readonly heroes = signal<Hero[]>([{ id: 1, name: 'Windstorm' }]);
readonly all = this.heroes.asReadonly();
add(name: string) {
this.heroes.update((list) => [...list, { id: Date.now(), name }]);
}
}컴포넌트에서는 클래스 필드로 private readonly heroService = inject(HeroService);라고 씁니다. 서비스는 처음 요청될 때 만들어집니다. 특정 컴포넌트만의 인스턴스가 필요하면 그 컴포넌트의 providers 배열에 서비스를 적습니다.
라우트는 URL 경로를 컴포넌트에 연결합니다. loadComponent를 쓰면 해당 페이지가 별도 번들로 나뉘어, 그 경로에 들어갈 때만 내려받습니다.
import { Routes } from '@angular/router';
import { HeroList } from './heroes/hero-list';
export const routes: Routes = [
{ path: '', component: HeroList, title: 'Heroes' },
{
path: 'heroes/:id',
loadComponent: () => import('./heroes/hero-detail').then((m) => m.HeroDetail),
},
{ path: '**', redirectTo: '' },
];필요한 곳에서 RouterLink와 RouterOutlet을 가져와 <a routerLink="/heroes/42">로 링크하고, <router-outlet />에 현재 페이지를 표시합니다. provideRouter에 withComponentInputBinding()을 켜 두면 id 같은 라우트 매개변수가 컴포넌트의 readonly id = input<string>()으로 바로 들어옵니다. canActivate 같은 가드는 inject()로 서비스를 쓸 수 있는 평범한 함수입니다.
imports를 가진 스탠드얼론 클래스입니다.input(), output()과 템플릿 바인딩으로 부모와 자식 컴포넌트를 연결합니다.@if, @for(track 필수), @switch로 템플릿에 무엇을 그릴지 정합니다.computed(), effect()로 변화에 반응합니다.providedIn: 'root' 서비스는 inject()로 공유하고, 라우터는 URL을 컴포넌트에 연결합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.