# Angular 18 Signals 완전 가이드: 새로운 리액티브 API와 Zoneless 변경 감지
> Angular 18의 Signals, Zoneless 변경 감지, signal 기반 API를 해설합니다. input(), model(), viewChild()의 실전 사용법을 코드 예제와 함께 소개합니다.
- Published: 2026-01-18
- Updated: 2026-04-10
- Author: SharpSkill
- Tags: angular 18, angular signals, zoneless, signal inputs, reactivity
- Reading time: 12 min
---
Angular 18은 Signals의 안정화를 통해 프레임워크 진화의 전환점을 맞이했습니다. 이 새로운 리액티브 프리미티브는 Angular 컴포넌트 구축 방식을 근본적으로 바꾸며, 기존 데코레이터의 모던한 대안을 제공하는 동시에 Zone.js 없는 변경 감지로 가는 길을 열어줍니다.
> **이 글에서 배울 내용**
>
> Angular 18의 signal 기반 API: input(), model(), viewChild(), 그리고 더 가볍고 성능 좋은 애플리케이션을 위한 Zoneless 설정을 다룹니다.
## Angular 18에서의 Signals 기본 개념
Signals는 Angular에서 리액티비티에 대한 새로운 접근법입니다. Zone.js 변경 감지에 의존하는 `@Input()` 같은 기존 데코레이터와 달리, Signals는 세밀하고 명시적인 리액티비티를 제공합니다. 각 Signal은 값을 캡슐화하고, 해당 값이 변경될 때 소비자에게 자동으로 알림을 보냅니다.
이 접근법의 장점은 다음과 같습니다: 타깃 업데이트를 통한 성능 향상, `computed()`와 `effect()` 함수와의 네이티브 통합, 그리고 Angular의 Zoneless 미래를 위한 준비입니다.
```typescript
// signals-basics.component.ts
// Demonstration of fundamental Signal concepts
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
Counter: {{ count() }}
Double: {{ doubleCount() }}
`
})
export class CounterComponent {
// Writable signal - value can be modified
count = signal(0);
// Computed signal - automatically derived from count
// Only recalculates when count changes
doubleCount = computed(() => this.count() * 2);
constructor() {
// Effect - executed on every count change
// Useful for side effects (logs, API calls, etc.)
effect(() => {
console.log(`New counter value: ${this.count()}`);
});
}
increment() {
// update() allows modification based on previous value
this.count.update(value => value + 1);
}
decrement() {
this.count.update(value => value - 1);
}
reset() {
// set() directly replaces the value
this.count.set(0);
}
}
```
Signals는 리액티브 컨테이너로 작동합니다. `signal()`은 쓰기 가능한 Signal을 생성하고, `computed()`는 파생된 계산 값을 만들며, `effect()`는 변경에 따른 액션 실행을 가능하게 합니다.
## input()을 사용한 Signal Inputs
`input()` 함수는 기존의 `@Input()` 데코레이터를 대체합니다. 읽기 전용 `InputSignal`을 반환하므로 데이터가 항상 부모에서 자식으로 단방향으로 흐르며, 실수로 변경될 일이 없습니다.
```typescript
// book-card.component.ts
// Component using signal inputs
import { Component, input, computed } from '@angular/core';
interface Book {
id: string;
title: string;
author: string;
price: number;
discountPercent?: number;
}
@Component({
selector: 'app-book-card',
standalone: true,
template: `
{{ book().title }}
By {{ book().author }}
@if (hasDiscount()) {
\${{ book().price }}\${{ discountedPrice() }}
} @else {
\${{ book().price }}
}
@if (featured()) {
Featured
}
`
})
export class BookCardComponent {
// Required input - template won't compile without this prop
book = input.required();
// Optional input with default value
featured = input(false);
// Computed based on input - automatically recalculated
hasDiscount = computed(() => {
const discount = this.book().discountPercent;
return discount !== undefined && discount > 0;
});
// Discounted price calculation
discountedPrice = computed(() => {
const { price, discountPercent } = this.book();
if (!discountPercent) return price;
return (price * (100 - discountPercent) / 100).toFixed(2);
});
}
```
부모 템플릿에서의 사용법은 기존과 유사하지만, 타입 안전성과 Signal 리액티비티가 추가됩니다.
```typescript
// book-list.component.ts
// Parent component using book-card
import { Component, signal } from '@angular/core';
import { BookCardComponent } from './book-card.component';
@Component({
selector: 'app-book-list',
standalone: true,
imports: [BookCardComponent],
template: `
@for (book of books(); track book.id) {
}
`
})
export class BookListComponent {
books = signal([
{ id: '1', title: 'Clean Code', author: 'Robert C. Martin', price: 35 },
{ id: '2', title: 'The Pragmatic Programmer', author: 'David Thomas', price: 42, discountPercent: 15 }
]);
featuredBookId = signal('1');
}
```
`@Input()`과의 가장 큰 차이점은 signal inputs가 읽기 전용이라는 것입니다. 자식 컴포넌트에서 `this.book.set()`을 호출하는 것이 불가능하며, 이를 통해 단방향 데이터 흐름이 강화됩니다.
## model()을 사용한 양방향 바인딩
양방향 동기화가 필요한 경우를 위해 Angular 18은 `model()`을 도입했습니다. 이 함수는 쓰기 가능한 Signal을 생성하며, 변경 사항을 부모 컴포넌트에 자동으로 전파합니다.
```typescript
// search-input.component.ts
// Component with bidirectional binding via model()
import { Component, model, output, computed } from '@angular/core';
@Component({
selector: 'app-search-input',
standalone: true,
template: `
`
})
export class CartComponent {
// Signal for mutable data
items = signal([]);
// Computed for derived values - avoids unnecessary recalculations
itemCount = computed(() => this.items().length);
total = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
constructor() {
// Effect for side effects (analytics, persistence)
effect(() => {
const currentItems = this.items();
// untracked avoids creating a dependency
untracked(() => {
localStorage.setItem('cart', JSON.stringify(currentItems));
});
});
}
addItem(product: Product) {
// update() for modifications based on previous state
this.items.update(current => {
const existing = current.find(i => i.id === product.id);
if (existing) {
return current.map(i =>
i.id === product.id
? { ...i, quantity: i.quantity + 1 }
: i
);
}
return [...current, { ...product, quantity: 1 }];
});
}
removeItem(id: string) {
this.items.update(current => current.filter(i => i.id !== id));
}
}
```
기억해야 할 핵심 사항은 다음과 같습니다.
- 템플릿에서 재계산하는 대신 파생 값에는 `computed()`를 사용할 것
- 새 값이 이전 값에 의존하는 경우 `set()`보다 `update()`를 우선 사용할 것
- effect 내에서 순환 의존성을 피하기 위해 `untracked()`를 사용할 것
- 렌더링 최적화를 위해 `@for` 루프에서 반드시 `track`을 지정할 것
## 결론
Angular 18은 Signals를 통해 Zone.js 없는 미래의 기반을 마련했습니다. 주요 요점은 다음과 같습니다.
- **input()** 은 `@Input()`을 대체하며, 더 엄격한 타입 지정과 읽기 전용 접근을 보장합니다
- **model()** 은 부모-자식 간 리액티브 양방향 바인딩을 구현합니다
- **viewChild()** 와 **contentChild()** 는 라이프사이클 훅의 필요성을 제거합니다
- **Zoneless** 는 번들 크기를 줄이고 성능을 향상시킵니다
- **computed()** 와 **effect()** 가 리액티브 에코시스템을 완성합니다
- 컴포넌트 단위의 점진적 마이그레이션이 가능합니다
Signals 도입은 Zoneless 모드가 표준이 될 미래의 Angular 버전을 위한 준비입니다. 이 전환은 장기적인 유지보수성과 성능을 위한 현명한 투자라 할 수 있습니다.
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/ko/blog/angular/angular-18-signals-new-features