signals는 애플리케이션 전체에서 state가 사용되는 방법과 위치를 세부적으로 추적하여 프레임워크가 렌더링 업데이트를 최적화할 수 있도록 하는 시스템이다
signals는 설정 된 값이 변경될 때, 알릴 수 있는 기능을 한다. 단순한 primitive 값 부터 복잡한 데이터 구조까지 모든 값이 포함될 수 있다. 항상 getter 함수로만 읽히며, 쓰기 가능하거나 읽기 전용일 수 있다.
const count = signal(0);
console.log(‘the Count is’ + count()); //getter
signals는 세 가지의 키워드를 가지고 있다.
Writable signal은 set() 또는 update()로 값이 변경될 수 있다.
count.set(3)
count.update(value => value + 1)
computed signa는 다른 signal로부터 값이 결정된다.
const count: WritableSignal<number> = signal(0);
const doubleCount: Signal<number> = computed(() => count() * 2);
위의 코드에서 doubleCount는 count에 의존한다. 즉, count가 변경될 때마다 count나 doubleCount에 의존하는 모든 것들을 변경한다.
computed signal는 읽히기 전까지 실행되지 않으며, 한번 계산되면 캐싱하고 있다가 읽을 때 반환한다 (읽을 때마다 함수를 호출하지 않는다)
함수 내에 존재하는 signals가 변경되면, computed signal은 캐싱한 값을 사용하지 않고 값이 읽어질 때 재계산한다
computed signal는 writable signal이 아니기 때문에, set()을 이용해서 값을 업데이트 할 수 없다.
computed signal 함수 내부 코드에 signal이 있지만 조건문으로 인해 읽혀지지 않은 경우, 해당 signal의 변경으로 computed signal이 재계산되지는 않는다.
const showCount = signal(false);
const count = signal(0);
const conditionalCount = computed(() => {
if (showCount()) {
return `The count is ${count()}.`;
} else {
return 'Nothing to see here!';
}
});
effect는 1개 이상의 signal 값이 변경되었을 때 실행된다.
effect는 최초에 한번 실행되고, 이후 변경될 때마다 실행된다.
computed signal과 유사하게 그들이 가장 마지막에 읽은 signal를 추적한다. 조건문으로 인해 분기되어 읽히지 못한 signal은 추적하지 않는다
effect가 전역변수 또는 constructor에 사용될 경우 injector가 필요 없지만, constructor 외부에서 사용할 경우 두번째 매개변수로 Injector를 넘겨야 한다.@Component({...})
export class EffectiveCounterCmp {
readonly count = signal(0);
constructor(private injector: Injector) {}
initializeLogging(): void {
effect(() => {
console.log(`The count is: ${this.count()})`);
}, {injector: this.injector});
}
}
effect는 포함하고 있는 context가 종료될 때 함께 종료되며, effect는 EffectRef를 반환하기 때문에 .destroy() 함수를 이용해서 수동 종료 할 수 있다. effect를 생성할 때, manualCleanup 옵션을 주면 수동으로 종료될 때까지 지속할 수 있다.signal을 만들 때 equal option에 동일한지 확인하는 함수를 넘기면, 새로운 값으로 변경될 때 이전 값과 다른지 비교해서 다를 때 trigger를 제공한다. Computable signal 및 writable signal 둘 다 사용될 수 있다.
const data = signal(['test'], {equal: _.isEqual});
data.set(['test']); // 기존값과 같기 때문에 trigger가 실행되지 않는다.
effect에서 추적되지 않는 signal을 사용하고 싶으면 signal을 untracked로 래핑하면 된다.effect(() => {
console.log(`User set to `${currentUser()}` and the counter is ${untracked(counter)}`);
});
effect의 첫번쨰 매개변수로 넘겨지는 함수는 onCleanup 함수인데, 이 함수는 effect가 종료될 때 실행된다.effect((onCleanup) => {
const user = currentUser();
const timer = setTimeout(() => {
console.log(`1 second ago, the user became ${user}`);
}, 1000);
onCleanup(() => {
clearTimeout(timer);
});
});
toSignal은 Observable 값을 추적하는 signal을 생성한다. async pipe와 같은 기능을 하며, 전체 애플리케이션에서 사용할 수 있다.
counterObservable = interval(1000);
// Get a `Signal` representing the `counterObservable`'s value.
counter = toSignal(this.counterObservable, {initialValue: 0});
toSignal은 Observable에 발행되는 값을 구독하기 때문에 사이드 이펙트를 유발할 수 있다. 그래서 toSignal로 생성되는 Subscription은 호출된 컴포넌트가 종료될 때 자동으로 구독을 해제한다.
initialValue가 생략되면 signal은 Observable에 값이 발행되기 전까지는 undefined를 반환한다.
BehaviorSubject 같이 subscription이 생성되었을 때 값을 반환하는 Observable인 경우, requireSync: true 옵션을 설정할 수 있다. 이땐 initialValue 옵션을 사용하지 않아도 undefined가 반환되지 않는다.
toSignal은 기본적으로 컴포넌트가 종료되면 자동 구독 해제된다. manualCleanup은 자동 구독 해제되지 않도록 한다.
toSignal을 사용할 때 error가 발생하면, signal을 읽을 때 에러가 던져진다. 에러는 상위 로직에서 처리되고, signal에서는 값을 받는 것이 권장된다. RxJS의 catchError를 이용해서 위 작업을 수행할 수 있다.
toSignal이 complete되면, toSignal은 complete 되기 전 가장 마지막 값을 가진다.
toSignal의 rejectErrors 옵션을 사용하면 오류는 RxJS로 던져지고, signal은 가장 마지막으로 값을 받았던 값을 반환한다.
toObservable은 signal를 추적하는 Observable을 생성한다. 생성된 Observable에 값이 변경되면, effect를 통해 추적된다.
query: Signal<string> = inject(QueryService).query;
query$ = toObservable(this.query);
results$ = this.query$.pipe(
switchMap(query => this.http.get('/search?q=' + query ))
);
위의 코드에서 query 값이 변경되면, query$ Observable은 새로운 HTTP request를 요청한다.
toObservable은 컴포넌트/서비스의 constructor나 전역 변수로만 사용될 수 있고, Injector 옵션을 이용하면 어디서나 사용될 수 있다.
toObservable은 Observable처럼 값을 동기적으로 표시하지 않고, signal이 안정화 되면 값을 보낸다.const obs$ = toObservable(mySignal);
obs$.subscribe((value) => console.log(value));
mySignal.set(1);
mySignal.set(2);
mySignal.set(3);
// console에는 3만 표시된다.