포스트

[Angular 마스터하기] Day 16 - Signal 고급, Effect와 Computed 마스터

[Angular 마스터하기] Day 16 - Signal 고급, Effect와 Computed 마스터

이제와서 시작하는 Angular 마스터하기 - Day 16 “Signal의 고급 기능으로 더 강력한 앱을 만들어봅시다! ⚡”

오늘 배울 내용

  • Effect 심화 활용
  • asReadonly()로 상태 보호
  • untracked() 패턴
  • 실전: 다크모드, LocalStorage 연동

1. Effect - 부수 효과 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Component, signal, effect } from '@angular/core';

@Component({
  selector: 'app-logger',
  template: `
    <button (click)="count.set(count() + 1)">클릭: {{ count() }}</button>
  `
})

export class LoggerComponent {
  count = signal(0);

  constructor() {
    // Effect: count가 변경될 때마다 실행
    effect(() => {
      console.log(`카운트: ${this.count()}`);

      // LocalStorage에 저장
      localStorage.setItem('count', this.count().toString());
    });
  }
}

Effect Cleanup

1
2
3
4
5
6
7
8
9
10
11
12
effect((onCleanup) => {
  const value = this.searchTerm();

  // API 호출
  const controller = new AbortController();
  fetch(`/api/search?q=${value}`, { signal: controller.signal });

  // Cleanup: 다음 실행 전 정리
  onCleanup(() => {
    controller.abort();
  });
});

2. asReadonly() - 상태 보호

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Injectable({ providedIn: 'root' })
export class ThemeService {
  private _isDark = signal(false);

  // Public: 읽기 전용
  isDark = this._isDark.asReadonly();

  toggle() {
    this._isDark.update(v => !v);
    this.applyTheme();
  }

  private applyTheme() {
    document.body.classList.toggle('dark', this._isDark());
  }
}

// 사용

@Component({
  template: `
    <button (click)="themeService.toggle()">
      {{ themeService.isDark() ? '🌙' : '☀️' }}
    </button>
  `
})

export class AppComponent {
  themeService = inject(ThemeService);
}

3. untracked() - 의존성 제외

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { untracked } from '@angular/core';

@Component({
  template: `
    <input [(ngModel)]="searchTerm" placeholder="검색">
    <p>결과: {{ results().length }}개</p>
  `
})

export class SearchComponent {
  searchTerm = signal('');
  results = signal<any[]>([]);
  searchCount = signal(0);

  constructor() {
    effect(() => {
      const term = this.searchTerm();

      // untracked: searchCount 변경해도 effect 재실행 안 됨
      untracked(() => {
        this.searchCount.update(c => c + 1);
        console.log(`검색 횟수: ${this.searchCount()}`);
      });

      // API 호출
      this.search(term);
    });
  }

  search(term: string) {
    // 검색 로직
  }
}

4. 실전 예제: 다크모드 + LocalStorage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Injectable({ providedIn: 'root' })
export class ThemeService {
  private _isDark = signal(false);
  isDark = this._isDark.asReadonly();

  constructor() {
    // 저장된 테마 불러오기
    this.loadTheme();

    // 테마 변경 시 자동 저장
    effect(() => {
      const dark = this._isDark();
      localStorage.setItem('theme', dark ? 'dark' : 'light');
      document.body.classList.toggle('dark', dark);
    });
  }

  toggle() {
    this._isDark.update(v => !v);
  }

  private loadTheme() {
    const saved = localStorage.getItem('theme');
    this._isDark.set(saved === 'dark');
  }
}

📝 정리

Effect 사용 시기

사용처 예시
LocalStorage 자동 저장
로깅 상태 변화 추적
DOM 조작 다크모드 적용
외부 API 동기화

체크리스트

  • Effect를 활용할 수 있나요?
  • asReadonly()로 상태를 보호할 수 있나요?
  • untracked()를 사용할 수 있나요?

📚 다음 학습


“Signal 고급 기능으로 더 강력한 앱을 만드세요!” ⚡

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.