팁: 전문적인 내용을 확인하기 전에 핵심 가이드를 읽어보는 것도 좋습니다.
시그널이 무엇인가요?
시그널(signal) 은 값을 감싸는 래퍼(wrapper)인데, 시그널은 그 값이 변경될 때 관심있는 사용자에게 알림을 보냅니다. 시그널은 기본 자료형부터 복잡한 데이터 구조까지 다양한 값을 담을 수 있습니다.
시그널의 값을 읽으려면 시그널 생성 함수를 실행하면 되고, Angular도 이 방식을 사용해서 시그널의 값이 변경되는 것을 추적합니다.
시그널은 값을 쓸 수 있거나(writable) 읽기 전용(read-only) 입니다.
값을 쓸 수 있는 시그널(Writable signals)
값을 쓸 수 있는 시그널의 값을 변경하려면 시그널의 내부 메서드를 직접 실행하면 됩니다.
이런 시그널은 signal
함수로 생성할 수 있으며, 시그널을 생성하면서 초기값을 지정할 수도 있습니다:
const count = signal(0);// 시그널은 그 자체로 게터 함수입니다. 값을 읽으려면 실행하세요.console.log('The count is: ' + count());
시그널의 값을 변경하려면 .set()
메서드를 실행하면 됩니다:
count.set(3);
아니면 .update()
메서드를 사용해서 이전 값을 참조하는 방식으로 변경할 수 있습니다:
// 값을 1 증가시킵니다.count.update(value => value + 1);
값을 쓸 수 있는 시그널은 WritableSignal
타입입니다.
연산 시그널(Computed signals)
연산 시그널(Computed signal) 은 어떤 시그널의 영향을 받아값이 변경되는 시그널을 의미합니다.
연산 시그널은 computed
함수로 생성할 수 있습니다:
const count: WritableSignal<number> = signal(0);const doubleCount: Signal<number> = computed(() => count() * 2);
위 코드에서 doubleCount
시그널은 count
시그널에 영향을 받습니다.
count
시그널의 값이 변경되면 doubleCount
의 값도 변경되며, Angular는 이 변화를 감지합니다.
연산 시그널은 지연 연산되며 연산된 결과는 캐싱됩니다
doubleCount
는 처음 실행하기 전까지 실제 연산을 수행하지 않습니다.
그리고 계산된 값은 캐싱되며, 다음 doubleCount
를 실행하면 다시 연산하지 않고 캐싱된 값을 바로 반환합니다.
이후에 count
시그널이 변경되면 Angular는 이를 감지하고 doubleCount
에 캐싱된 값이 더이상 유효하지 않다는 것을 판단하기 때문에, 이후에 doubleCount
를 실행하면 실제 연산이 실행됩니다.
따라서 배열 필터링과 같이 계산이 많은 경우라면 연산 시그널을 사용하는 것이 성능면에서 유리합니다.
연산 시그널은 값을 지정할 수 없습니다
연산 시그널에는 직접 값을 설정할 수 없습니다.
doubleCount.set(3);
이렇게 코드를 작성하면 컴파일 에러가 발생합니다.
doubleCount
는 WritableSignal
타입이 아니기 때문입니다.
연산 시그널의 종속성은 동적으로 변경됩니다
Angular는 구독자가 있는 시그널만 시그널만 추적합니다.
showCount
시그널이 true
값일 때만 값을 참조하는 count
연산 시그널이 있다고 합니다:
const showCount = signal(false);const count = signal(0);const conditionalCount = computed(() => { if (showCount()) { return `The count is ${count()}.`; } else { return 'Nothing to see here!'; }});
이제 showCount
시그널의 값이 false
일 때 conditionalCount
시그널의 값을 읽으면 count
시그널은 실행되지도 않고 "Nothing to see here" 메시지가 반환됩니다.
이 말은, 나중ㅇ ㅔcount
시그널이 변경되더라도 conditionalCount
는 연산을 다시 하지 않는 다는 것을 의미합니다.
그리고 showCount
시그널의 값이 true
일 때 conditionCount
시그널의 값을 읽으면, 시그널의 증분 함수가 실행되면서 showCount
가 true
인 분기를 타게 되고, count
시그널의 값을 문자열에 담아 반환합니다.
그리고 count
시그널이 변경되면 conditionalCount
시그널에 캐싱된 값도 유효하지 않은 것으로 판단합니다.
시그널의 종속성은 동적으로 변경됩니다.
이후에 showCount
시그널의 값이 false
가 되면, conditionalCount
시그널은 더이상 count
값이 변경되는 것을 감지하지 않습니다.
컴포넌트 OnPush
함수에서 시그널 읽기
컴포넌트 템플릿에서 시그널을 참조하면 Angular는 이 시그널의 의존성을 추적합니다. 그래서 종속 관계인 시그널의 값이 변경되며 Angular가 컴포넌트를 마크(marks) 했다가 다음 변경 감지 싸이클이 실행될 때 화면을 갱신합니다. 자세한 내용은 컴포넌트 서브트리 건너뛰기 문서를 참고하세요.
효과 함수
시그널은 해당 시그널을 구독하는 쪽으로 알림을 줄 수 있기 때문에 유용합니다.
그리고 이 시그널은 값이 변경될 때마다 효과 함수가 실행됩니다.
효과 함수의 동작을 지정하려면 effect
함수를 사용하면 됩니다.
effect(() => { console.log(`The current count is: ${count()}`);});
효과 함수는 최소한 한 번은 실행됩니다. 그리고 효과 함수가 실행되면서 시그널을 추적하기 시작합니다. 효과 함수는 연산 시그널과 비슷하게, 의존성 관계도 동적으로 변경되며 가장 최근에 실행했을 때 값을 캐싱하며 시그널의 값이 변경되는지 감지합니다.
효과 함수는 언제나 변화 갑지 싸이클에서 비동기로 실행됩니다.
효과 함수 활용하기
효과 함수는 사용하는 경우가 거의 없지만, 이런 경우에는 유용합니다:
- 화면에 표시하는 데이터가 변경될 때마다 로그로 출력할 때
window.localStorage
과 데이터를 동기화해야 할 때- 템플릿 문법으로 불가능한 커스텀 DOM 동작을 추가할 때
<canvas>
로 렌더링을 커스터마이징 할 때, 차트 라이브러리를 사용하거나 서드 파티 UI 라이브러리를 사용할 때
효과 함수를 사용하지 말아야 하는 경우
상태값이 변경되는 것을 전파할 때 효과 함수를 사용하지 마세요.
이렇게 사용하면 순환 종속성으로 변화 감지 싸이클이 무한으로 실행되며 ExpressionChangedAfterItHasBeenChecked
에러가 발생합니다.
상태값을 전파해야 하는 경우에는 computed
시그널을 사용하세요.
의존성 주입 컨텍스트
기본적으로 effect()
함수는 inject
함수에 접근할 수 있는 의존성 주입 컨텍스트 안에서 실행할 수 있습니다.
컴포넌트, 디렉티브, 서비스에서 이 조건을 만족하려면 constructor
함수 안에서 effect
를 실행하는 것이 가장 간단합니다:
@Component({...})export class EffectiveCounterComponent { readonly count = signal(0); constructor() { // 효과 함수를 등록합니다. effect(() => { console.log(`The count is: ${this.count()}`); }); }}
아니면 클래스 프로퍼티로 등록해도 됩니다.
@Component({...})export class EffectiveCounterComponent { readonly count = signal(0); private loggingEffect = effect(() => { console.log(`The count is: ${this.count()}`); });}
생성자 함수 밖에서 효과 함수를 등록하려면 Injector
를 인자로 전달해야 합니다:
@Component({...})export class EffectiveCounterComponent { readonly count = signal(0); private injector = inject(Injector); initializeLogging(): void { effect(() => { console.log(`The count is: ${this.count()}`); }, {injector: this.injector}); }}
효과 함수가 종료되는 과정
효과 함수는 효과 함수가 생성된 컨텍스트가 종료될 때 함께 자동으로 종료됩니다. 이 말은, 효과 함수가 컴포넌트 컨텍스트에서 생성되었으면 컴포넌트가 종료될 때 함께 종료된다는 것을 의미합니다. 디렉티브나 서비스에서 생성하는 효과 함수도 마찬가지입니다.
효과 함수를 생성하면 EffectRef
를 반환하기 때문에 .destroy()
메서드를 실행하면 수동으로 종료할 수 있습니다.
그리고 manualCleanup
옵션을 사용하면 수동으로 종료하기 전까지 종료되지 않는 효과 함수를 만들 수도 있습니다.
사용하지 않는 효과 함수는 반드시 정리해야 하는 것을 잊지 마세요.
고급 주제
시그널 동일성 평가 함수
시그널을 생성할 때 동일성 평가 함수(equality function)를 옵션으로 지정할 수 있습니다. 이 함수는 새 값이 이전 값과 다른지 판단하는 역할을 합니다.
import _ from 'lodash';const data = signal(['test'], {equal: _.isEqual});// 인스턴스가 다르더라도 deep equal 함수가 동일성을 판단하기 때문에// 시그널은 값을 갱신하지 않습니다.data.set(['test']);
동일성 평가 함수는 값을 쓸 수 있는(writable) 시그널과 연산(computed) 시그널에 모두 사용할 수 있습니다.
도움말: 동일성 평가 함수의 기본값은 참조 비교(Object.is()
)입니다.
추적 종속성과 관계없이 값 읽기
드문 경우지만, computed
나 effect
와 같은 반응형 함수 안에서 종속성을 추가하지 않고 코드를 실행할 수 있습니다.
예를 들면, currentUser
시그널의 값이 변경될 때 counter
시그널의 값을 로그로 출력한다고 합시다.
그렇다면 이런 효과 함수를 작성할 수 있습니다:
effect(() => { console.log(`User set to ${currentUser()} and the counter is ${counter()}`);});
이렇게 구현하면 currentUser
시그널이나 counter
시그널 둘 중에 하나가 변경될 때마다 로그가 출력됩니다.
하지만 효과 함수가 currentUser
시그널이 변경될 때만 반응해야 하고, counter
시그널이 변경되는 것은 감지하지 않아야 하는 경우는 어떻게 해야 할까요?
이런 경우라면 untracked
함수 안에 게터 함수를 전달하면 됩니다:
effect(() => { console.log(`User set to ${currentUser()} and the counter is ${untracked(counter)}`);});
untracked
함수는 종속성 관계가 아닌 외부 코드를 실행해야 할 때도 유용합니다:
effect(() => { const user = currentUser(); untracked(() => { // `logginvService`는 `user` 시그널을 읽지만, 종속성 관계는 아닙니다. this.loggingService.log(`User set to ${user}`); });});
효과 함수 종료하기
효과 함수의 실행이 길어지는 경우 다른 동작이 시작되기 전에 효과 함수를 정리해야 하는 경우가 있습니다.
이런 경우는 효과 함수를 생성할 때 첫번째 인자로 onCleanup
함수를 전달하면 됩니다.
이 방식으로 다음 실행이 시작되기 전이나 효과 함수가 종료될 때 실행할 콜백 함수를 등록할 수 있습니다.
effect((onCleanup) => { const user = currentUser(); const timer = setTimeout(() => { console.log(`1 second ago, the user became ${user}`); }, 1000); onCleanup(() => { clearTimeout(timer); });});
RxJS와 시그널 함께 사용하기
시그널과 RxJS를 함께 사용하는 방법을 알아보려면 RxJS와 Angular 시그널 함께 사용하기 문서를 참고하세요.