본문 바로가기

개발 공부/Angular

Angular Signals (2)

Angular Signals를 활용한 상태 관리 심화

 

실전 상태 관리

Signals로 글로벌 상태 관리 구현

글로벌 상태 관리는 애플리케이션 전체에서 데이터를 공유할 때 사용됩니다.

Signal을 활용하면 별도의 상태 관리 라이브러리 없이도 이를 간단히 구현할 수 있습니다.

 

예제: 글로벌 상태를 관리하는 서비스

import { Injectable, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class AppStateService {
  // 글로벌 상태 정의
  user = signal<{ name: string; isLoggedIn: boolean }>({ name: '', isLoggedIn: false });

  // 상태 업데이트 메서드
  login(name: string) {
    this.user.set({ name, isLoggedIn: true });
  }

  logout() {
    this.user.set({ name: '', isLoggedIn: false });
  }
}

 

사용 예시: 컴포넌트에서 글로벌 상태 접근

import { Component } from '@angular/core';
import { AppStateService } from './app-state.service';

@Component({
  selector: 'app-header',
  template: `
    <div *ngIf="appState.user().isLoggedIn">
      Welcome, {{ appState.user().name }}!
    </div>
    <button (click)="appState.logout()">Logout</button>
  `,
})
export class HeaderComponent {
  constructor(public appState: AppStateService) {}
}

 


컴포넌트 간 데이터 공유

Signal을 사용하면 부모-자식 컴포넌트뿐 아니라 멀리 떨어진 컴포넌트 간에도 데이터를 쉽게 공유할 수 있습니다.

Signal로 상태 공유

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

@Component({
  selector: 'app-parent',
  template: `
    <app-child [count]="count"></app-child>
    <button (click)="increment()">Increment</button>
  `,
})
export class ParentComponent {
  count = signal(0);

  increment() {
    this.count.set(this.count() + 1);
  }
}

@Component({
  selector: 'app-child',
  template: `<p>Count: {{ count() }}</p>`,
})
export class ChildComponent {
  count = signal(0);
}

 

 

 


Signals vs. 다른 상태 관리 도구

BehaviorSubject와의 비교

Feature Signal BehaviorSubject
API의 간결성 간단하고 직관적 RxJS 연산자를 이해해야 함
의존성 추적 자동 수동
퍼포먼스 의존성 기반 최적화 수동 최적화 필요

 

NgRx와의 비교

Feature Signal NgRx
학습 곡선 낮음 높음
상태 관리 복잡성 적합 (중소형 앱) 적합 (대형 앱)
Boilerplate 코드 거의 없음 많음

 

사용 사례에 따른 적합성 분석

  • Signals: 간단한 상태 관리가 필요한 프로젝트에 적합.
  • BehaviorSubject: RxJS와 밀접하게 통합된 기능을 활용해야 할 때 유리.
  • NgRx: 대규모 상태 관리와 강력한 미들웨어가 필요한 대형 애플리케이션에 적합.

 

 


고급 사용법

Signal은 상태 변화를 자동으로 추적하지만, 필요하지 않은 곳에서 Signal을 사용하면 성능 저하가 발생할 수 있습니다.

  • Lazy Computation: computed를 필요한 시점에만 계산되도록 설정.
  • Effect 관리: 효과가 너무 자주 실행되지 않도록 의존성 구조를 설계.

Lazy Computation

const expensiveComputation = computed(() => {
  console.log('Expensive computation executed');
  return heavyCalculation(signalValue());
});

 

퍼포먼스 및 메모리 관리

  • Signal과 effect를 남용하면 메모리 누수가 발생할 수 있습니다.
  • 사용하지 않는 effect은 구독 취소를 통해 해제해야 합니다.

 

effect와 구독 취소의 필요성

effect는 Angular 내부에서 관리되지 않으므로 수동으로 정리(cleanup)해야 합니다.

Angular의 OnDestroy 라이프사이클을 활용하여 effect를 정리할 수 있습니다.

import { Component, OnDestroy } from '@angular/core';
import { effect, signal } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
    <p>Counter: {{ count() }}</p>
    <button (click)="increment()">Increment</button>
  `,
})
export class CounterComponent implements OnDestroy {
  count = signal(0);
  private effectCleanup: VoidFunction;

  constructor() {
    // Signal의 변화를 감지하고 실행되는 effect 정의
    this.effectCleanup = effect(() => {
      console.log(`Count value: ${this.count()}`);
    });
  }

  increment() {
    this.count.update(value => value + 1);
  }

  // 컴포넌트가 파괴될 때 effect를 정리
  ngOnDestroy() {
    if (this.effectCleanup) {
      this.effectCleanup();
    }
  }
}

 

Signal 자체의 경우 구독 취소가 불필요

Signal은 Angular의 의존성 시스템과 밀접하게 통합되어 있습니다.

Signal은 Angular의 Zone-less 구조를 활용하여 상태 변화를 감지하고, 컴포넌트의 생명주기를 자동으로 추적하므로 명시적인 구독 관리가 필요하지 않습니다.

즉, Signal은 effect와 달리 구독 취소를 걱정하지 않아도 됩니다. effect를 사용할 때만 구독 취소를 명시적으로 처리하면 충분합니다.