본문 바로가기

Web + APP/Angular

Angualr v16 - signal

반응형
SMALL

Angular v16가 release가 됐다.

 

사실 된 지 오래됨 ㅋ

 

이제서야 부랴부랴 좀 영상 좀 챙겨보는데, 재밌는 기능이 나와서 이거 좀 풀어보려고 한다.


Angular에서 Change Detectionzone JS에 의존하고 있었다.

 

Change Detection
앱 내의 상태가 변화할 때, 브라우저 안 UI를 Angular가 변화시키는 개념
zone JS
Angular에게 변화 생겼다고 말해주는 중간 띄기 사업자
몽키패치하여 비동기 변화를 감지한다.
몽키패치
원래 있던 기능에 자기가 원하는 코드 삽입하는 행위

 

근데, 솔삐 중간 중개업자한테 굳이 수수료 떼면서 동작을 해야하나 싶었는데, Angular 팀에서도 이걸 알았는지, 이제 signal 이라는 기능을 만들었다.

 

지들도 적합한 솔루션이 아니라고 했다.

 

Up to this point, we've relied solely on change detection power by zone JS, but over time, we found tha, at scale, it isn't the optinal solution.
Angular v16 소개 영상 - https://www.youtube.com/watch?v=bkOEMw0oTkY

 

야심차게 소개한 signal 좀 핥아보자.

 

핥짝


Writable signals

const count = signal(0);

console.log('숫자는' + count());

signal 값의 경우엔 getter function을 통해서 읽혀진다.

 

값 바꾸고 싶으면, 이렇게

counst.set(3);

이전 값 이용해서 지지고 볶고 하고 싶으면, 이렇게

count.update(value => value + 1);

간단하다.

 

객체 참조값 안 바꾸고, 안에 데이터만 바꾸고 싶을 땐, mutate 메소드 사용이 가능하다.

const todos = signal([{title: 'Learn signals', done: false}]);

// 배열 참조값을 바꾸지 않음
todos.mutate(value => {value[0].done = true;});

Computed Signals

const count: WritableSignal<number> = signal(0);
const doubleCount: Signal<number> = computed(() => count() * 2);

doubleCount의 경우엔 count signal에 의존이되는데, count의 값이 바뀌면 Angualar가 둘 다 값을 바꿔버린다.

 

computed signal의 경우 다른 signal를 통해 값을 얻는 signal을 명시한다.

 

여기서 중요한 점은 doubleCount의 리턴 값은 캐싱된다는 점이다. 추후에 또 값을 읽을 때, 계산을 또 안 한다.

 

count가 변하면, doubleCount의 값이 유효하지 않으니까, 다시 계산을 하는 구조

 

그렇기에 성능상에도 좋고, 누이좋고 매부좋다.

 

doubleCount.set(3);

 

근데, 이건 안됨

 

computed Signal의 경우엔 writable signal이 아니기 때문에, 컴파일 에러가 뜬다.

 

const showCount = signal(false);
const count = signal(0);
const conditionalCount = computed(() => {
	if (showCount()) {
    	return `숫자는 ${count()}.`;
    } else {
    	return `없어요`;
	}
});

conditionalCount가 count 값에 의해 캐싱될거라 생각될 수 있는데, showCount가 false라 count가 읽히지 않았다.

 

즉, count 값이 변하더라도 이는 재계산이 들어가지 않고, 캐싱되지 않는다.

 

showCount가 true가 됐을 때부터 count의 값을 추적하고 캐싱하기 시작한다.

 

이 후 다시 showCount가 false로 변했을 땐, 다시 의존을 끊어버린다.


Reading signals in OnPush components

Onpush 컴포넌트의 경우 Angular가 signal을 추적한다.

 

signal의 변화가 됐을 때, Angular가 자동으로 해당 컴포넌트에 marks를 하며, 다음 Change Detection이 동작할 때, 이를 업데이트한다.

 

OnPush 전략
컴포넌트에서 실제 레퍼런스가 변경됐는지 확인하여, 변경감지 프로세스를 수행하게 만드는 전략

Effects

이 부분이 가장 재밌었는데, ngrx의 effect와 유사해보였다.

 

signal의 값이 바뀌면, 이를 추적해서 추가적은 로직을 수행한다.

 

effect(() => {
	console.log(`숫자는 ${count()}`);
});

 

Effect는 항상 한 번은 실행되는데, signal 값을 추적해서 값이 변할 때마다 effect가 실행한다.

 

computed signal과 유사하게 effect는 동적으로 의존성을 가지고 signal의 가장 최근의 값을 계속해서 쫓는다.

 

Effect는 Change Detection 과정 속에서 항상 비동기로 실행한다.


Uses for effects

computed signal과 유사한 effect

 

언제 쓰는게 좋을까 ?

 

  • 디버깅 툴 또는 분석도구로서 data log를 보여주고 싶을 때
  • window.localStorage와 데이터 싱크를 유지하고 싶을 때
  • template 구문에 표현할 수 없는 custom DOM 동작을 추가할 때
  • charting library, 서드 파티 UI library, <canvas> 와 같은 렌더를 수행할 때

 

그럼 언제 안 쓸까 ?

 

state change propagation을 위해서 effect 쓰는걸 좋지 않다고 말한다.

 

ExpressionChangedAfterItHasBeenChecked 에러가 발생할 수 있다. (NG0100 Error)

 

무한 반복 및 필요 없는 Change Detection cycle이 추가적으로 수행될 수 있다.

 

현 프로젝트에서 발생하는 에러였는데,,, ngrx effect가 문제였을까 ?

Injection context

effect 함수는 injection context가 필요한데, 가장 쉬운 방법이 effect를 constructor 안에서 실행하는 것이다.

 

@Component({...})
export class EffectiveCounterCmp {
	readonly count = signal(0);
    
    constructor() {
    	effect(() => {
        	console.log(`숫자는 ${this.count()}`);
        });
    }
}

// 걍 클래스 변수로 선언해도 됨
@Component({...})
export class EffectiveCounterCmp {
	readonly count = signal(0);
    
    private loggingEffect = effect(() => {
    	console.log(`숫자는 ${this.count()}`);
    });
}

 

constructor 밖에서 하고 싶으면 injector를 사용하면 된다.

 

@Component({...})
export class EffectiveCounterCmp {
	readonly count = signal(0);
    
    constructor(private injector: Injector) {}
    
    initializeLoggin(): void {
    	effect(() => {
        	console.log(`숫자는 ${this.count()}`);
        }, {injector: this.injector});
    }
}

Destroying effects

effect도 결국 구독하는 시스템과 똑같은데, 그대로 두면 메모리를 계속 차지해서 펑 터지는 순간이 올텐데, 다행히 자동으로 destroyed가 된다고 한다.

 

컴포넌트가 닫히면 같이 없어지는데, 이는 service 및 directive도 마찬가지다.

 

Effects는 EffectRef를 리턴하는데, destroy 메소드를 호출해서 직접 destroy해도 된다.

 


Signal equality functions

signal을 만들 때, equality 함수를 옵션으로 넘길 수 있는데, 이는 새로운 값이 이전 값과 같은지 여부를 따는 옵션이다.

 

import _ from 'lodash';

const data = signal(['test'], {equal: _.isEqual});

data.set(['test']);

 

 

 

배열 인스턴스는 다르지만, 깊은 비교를 하기 때문에 이를 같다고 고려하여, signal은 update를 하지 않는다.

 

Equality 함수는 writable, computed signal 둘 다 제공한다.

 

근데, 로대시를 끌어다가 쓰는 예제를 공식문서에서 사용한다는거가 쬐애끔 가오 상하지 않나 싶은데, 음..

 

writable signal에서 쓰는 mutate 메소드의 경우엔 equality를 체크하지 않는데, mutate의 경우 참조값을 바꾸지 않고, 현재 값을 반환하기 때문이다.

 

 


Reading without tracking dependencies

의존을 하지 않으면서, computed와 effect를 쓰고 싶은 경우가 있을 수 있다.

 

예를 들어서 currentUser가 변경되면 counter의 값이 기록되어야한다고 해보면, 아래와 같을 것이다.

 

effect(() => {
	console.log(`${currentUser()}와 숫자 ${counter()}`);
});

 

해당 예제에서는 currentUser와 counter의 변화를 추적하는데, currentUser의 변화가 일어났을 때만, counter를 변화시키고 싶다고 가정할 수 있다.

 

그런 경우 untracked 메소드를 이용해서 막을 수 있다.

 

effect(() => {
	console.log(`${currentUser()}와 숫자 ${untracked(counter)}`);
});

 

untracked는 종속성을 취급하지 않는 외부 코드를 실행할 때, 사용될 수 있다.

 

effect(() => {
    untracked(() => {
    	const user = currentUser();
    	this.logginService.log(`${user}`);
    });
});

 

만약 logginService가 signal을 읽는다고해서 해당 effect의 종속성으로 계산되지 않는다.

 

근데, Angular 공식 문서에 있는 예제 좀 이상한거 같은데,,,

https://stackblitz.com/edit/stackblitz-starters-ncd8mx?description=An%20angular-cli%20project%20based%20on%20@angular/animations,%20@angular/common,%20@angular/compiler,%20@angular/core,%20@angular/forms,%20@angular/platform-browser,%20@angular/platform-browser-dynamic,%20@angular/router,%20core-js,%20rxjs,%20tslib%20and%20zone.js&file=src%2Fmain.ts&title=Angular%20Starter

위의 예제가 untracked를 이용한 예제인데, 문제는 const user 선언부의 위치이다.

공식 문서

공식 문서에는 const user가 untracked 밖에서 선언되었다.
그러니 ngAfterViewInit에서 선언한 Devis가 더해지는데, 이 때 console.log가 찍힌다 .. ?
종속 관계 없다며 .. ?

수정 전

그래서 의심스러워서 저 user 선언을 untracked 안으로 넣고 해보았다.
그러니까 John만 불림

수정 후

뭐여 ;

Effect cleanup functions

Effects의 경우엔 destroyed 되거나, 실행이 끝나기 전에 다시 호출되면 취소될 수 있다.

 

onCleanup 함수를 첫번째 파라미터로 넣을 수 있는데, 이 함수의 경우엔 callback 함수를 등록할 수 있다.

 

effect가 destroyed 되기 전 또는 다음 effect 호출이 일어났을 때, 해당 콜백을 실행하는 로직이다.

 

effect((onCleanup) => {
	const user = currentUser();
    
    const timer = setTimeout(() => {
    	console.log(`1초 전 ${user}`);
    }, 1000);
    
    onClenup(() => {
    	clearTimeout(timer);
    });
});

 

예제로 보는게 낫겠다.

onCleanup 없을 때
onCleanup 있을 때


이상 signal이었는데, 언제 어디서 쓸 지 현재 하고 있는 플젝을 뜯어보면서 살펴봐야겠다.

 

근데, 당장 돌입 할 수 있으려나 .. ?

반응형
LIST

'Web + APP > Angular' 카테고리의 다른 글

Angular Hydration  (0) 2023.06.10
providers와 viewProviders의 차이 / content child와 view child  (0) 2022.05.21
HostListener & HostBinding  (0) 2022.04.30
NGRX Store - An Architecture Guide (1)  (0) 2022.04.11
attr vs attr.name  (0) 2022.03.30