Angular Design Review

라코마코·2022년 9월 30일
0

Angular

목록 보기
6/6

Jquery AngularJS Backbone ... React Angular Vue 프레임워크 전쟁

프론트엔드 트렌드 변화는 매우 빠릅니다.

빠르게 변화하는 시장에서 Angular처럼 10년 넘게 사랑받은 프레임워크는 드뭅니다.

ng-conf에서 이렇게 오래도록 사랑받아온 Angular를 Design review 하는 세션이 있는데 영상은 없고 ppt 스크립트를 보면서 내용을 정리해봤습니다.

https://docs.google.com/presentation/d/1_eYqgbrj-d8XHCv15xxUDIk8U_UROY1Z17zPXeN42rw/edit#slide=id.g15a53494533_0_630

Automatic Global Change Detection

앵귤러팀은 앵귤러를 설계할때 값을 변경하면 곧바로 화면에 반영되길 원했다고 합니다.

따라서 View State를 Mutable하게 설계했습니다.

<section>
  {{user.profile.biography}}
<section>

  
in angular component...

change(){
  user.profile.biography = newBio; // it just works!!
}

혹시 틀린그림 찾기 해보신적이 있으신가요?

Angular는 Change Detection 으로 값이 변했는지 찾는데 문제는 언제 어느 시점에 값이 변경되었는지 알기 어렵습니다.

Angular팀은 이런 문제점을 해결하기 위해 Zone.JS를 도입하였습니다.

Zone.JS는 브라우저 이벤트들을 Monkey Patching 하여 View State가 변경될만한 이벤트가 발동하면 Change Detection을 수행하도록 할 수 있습니다.

(View State가 변경될만한 이벤트들)

- EventListeners
- setTimeouts
- Promises
- XHRs
- IntersectionObservers
- etc etc etc ...

위 이벤트들이 발동되면 Angulars는 Change Dection (CD라고 줄여서 부르겠습니다.)를 수행하여 뷰를 업데이트합니다.

하지만 Zone.js는 많은 비용이 듭니다.

1. 16kb gzip 크기
2. 50kb 브라우저가 파싱한 JS 파일 크기
3. Monkey Patching을 먼저 적용해야 하기 때문에 "반드시!!!!!!!!!!!!" Zone.JS가 실행된 후 어플리케이션이  실행되어야함.

또 Zone.js가 돌았다고 해서 View State가 변경되었음을 보장하는것도 아닙니다.

third party library를 Angular 내에서 실행시켰을때 의도하지 않은 이벤트에도 CD를 발동시킬 위험이 발생합니다.

따라서 앱이 커지게 된다면 개발자가 최적화를 직접 수행해야 합니다. (OnPush 전략을 말이죠)

Angular 개발자들은 값이 변경되면 화면도 변경되는 "it just works!" 라는 DX를 주고 싶었지만

최적화 (OnPush)를 해야하는 이슈가 발생해 개발자들은 언제 어느 시점에 어떻게 CD가 도는지 알아야 하며 CD가 돌지 않으면 문제를 해결해야 하기 때문에 DX를 주기 보단 개발자의 overHead가 커졌다고 합니다.

(Angular Load Map)

이 스크립트를 읽자 왜 angular LoadMap에 Zone.JS를 opt-out으로 제거하는 피쳐가 있는지 이해 되었습니다.

Zone.JS가 제거되면 Vue나 React처럼 수동 CD를 돌리는 API를 제공해줄까요?

(ZoneLess Angular 포스트1)

RxJS

Angular는 Rxjs를 적극적으로 사용하고 있습니다.

하지만 RxJS는 호불호가 확실하게 갈리는 라이브러리중 하나입니다.!


( Angular Developer Survey에서도 의견이 갈린다고 합니다. )

사실 Angular는 Reactive Programming을 RxJS에 국한에서 사용할 생각은 없었다고 합니다.

잉..? Angular는 내부적으로 RxJS 를 엄청 쓰고 있는거 아니었어?

Angular에서 RxJS를 적극적으로 사용하고 있는 부분은 외부에 노출된 API에서 이벤트를 streaming 하는 용도로 사용하고 있다고 합니다.

httpClient
Router
QueryList
FormControl

위 리스트 처럼 유저에게 공개된 API에서 사용되고 있습니다.

그럼에도 불구하고 많은 개발자들은 상태를 구축할때 RxJS를 사용합니다. 이로 인해서 몇가지 문제점이 발생하게 됩니다.

문제 1. Angular에서 Observable을 얻는것은 어색하다.

Angular개발을 해보면 한번쯤 이런 생각을 해봤을겁니다.

왜 @Input은 Observable이 아닐까????

Input을 Observable처럼 사용할려면 좀 어색한 코드를 작성해야합니다.


@Input() myInput!: string;
myInput$: BehaviorSubject<string> = new BehavirSubject(this.myInput);

ngOnChanges(changes){
  if('myInput' in changes){
    this.myInput$.next(this.myInput);
  }
}

Input은 Observable이 아니기 때문에 내부에 BehaviorSubject를 선언해야합니다.

또 BehaviorSubject를 연결해야하는 코드도 작성해야하죠.

많이 어색합니다.

Angular Proposal에서도 Input을 Observable로 만들어 달라는 요청이 있습니다. ( 무려 따봉이 256개 달려있죠 )

https://github.com/angular/angular/issues/5689

하지만 Angular 개발자들은 Angular가 RxJS에 종속성을 가질까봐 이 기능을 도입하는것에 거부하는듯한 움직임을 보였습니다.

Input을 Observable로 바꿔버린다면 Reactivity를 RxJS로만 사용해야 하기 때문에 기존에 선택했던 설계에 반하여 거부하였던것입니다.

(Input Observable을 왜 이렇게 거부하는지 도저히 이해가 안됐는데 ppt를 보고 드디어 납득 되었습니다..)

문제 2. Angular는 reactive graph를 최적화할 수 없다.

// in UserProfile Component

<profile-pic [user]="user$ | async"></profile-pic>

user$ = inject(UserService).currentUser$;

Observable을 UserService에서 얻어오고 있고 값이 변경되면 업데이트 하는 구조입니다.

이런 구조이죠. 파란색은 다시 업데이트 해야하는 영역이고 빨간색은 업데이트가 필요없는 영역입니다.

UserProfile 쪽만 다시 리렌더링 하면 되지만 Angular는 Top (AccountSettings) 부터 밑바닥까지 모두 돌며 CD를 해야합니다.

왜냐하면 user$ Observable을 어떤 컴포넌트들이 구독하고 있는지 모르기 때문에 모든 컴포넌트에 대해서 CD를 돌아야 합니다.

모든 컴포넌트를 OnPush로 걸어도 상황은 똑같습니다.

OnPush로 걸어도 CD는 Root부터 Leaf까지 거쳐야 하기 때문에 AccountSettings에 대해서 CD가 돌아갑니다.

아직은 최적화 하는 방법이 없고 Angular팀은 이 문제를 해결하고자 작업을 진행중이라고 합니다.

Selector based component APIs

Angular팀은 Angular의 templates가 평범한 HTML처럼 보이도록 하길 설계했습니다.

(실제로 유튜브 html 코드를 까보면 같은 코드가 존재하죠.)

그리고 컴포넌트나 디렉티브는 CSS Selector를 통해서 적용되도록 하였습니다.

@Component({
  selector: 'mat-checkbox',
  tmplateUrls:'mat-checkbox.html'
})


// in mat-checkbox.html
<div> 저는 mat checkbox 입니다. </div>
---

// usage case
<mat-checkbox></mat-checkbox> 

----

user page

<!-- mat-checkbox가 host element 입니다. -->
<mat-checkbox>
  <div>저는 mat checkbox입니다.</div>
</mat-checbkox>

이렇게 프레임워크를 디자인 하였기 때문에 모든 컴포넌트는 Host Element를 가지게 됩니다. 위 코드에선 mat-checkbox가 Host Element입니다.

@Component({
  selector: 'mat-slider',
  host: {
    'role': 'slider',
    'class': 'mat-slider',
    '[attr.aria-valuemin]': 'min',
    '[attr.aria-valuemax]': 'max',
    '[attr.aria-valuenow]': 'value',
  }
})

// usage case

<mat-slider aria-label="Volume" class="cool"></mat-slider>

// user page 

<mat-slider class="mat-slider" aria-valuemin="0" aria-valuemax="5" aria-valuenow="23" aria-label="Volume" class="cool"></mat-slider>

Component에서 Host Element에 들어갈 값들을 설정하면 자동으로 값이 바인딩 되며 또 컴포넌트 바깥쪽에서도 Host Element에 attribute나 class를 붙일 수 있기 때문에 손 쉽게 컴포넌트 외부, 바깥의 코드를 융합하는것이 가능해집니다.

또한 Angular는 natural element의 attribute 속성을 활용하여 코드를 붙이는것도 가능합니다.

@Component({
  selector: 'mat-slider, [mat-slider]', // [mat-slider] 속성으로 select
  host: {
    'role': 'slider',
    'class': 'mat-slider',
    '[attr.aria-valuemin]': 'min',
    '[attr.aria-valuemax]': 'max',
    '[attr.aria-valuenow]': 'value',
  }
})
<ul mat-slider>
</ul>

이렇게 되면 ul은 natural html element이면서 동시에 component가 됩니다.

웹 표준을 지키면서 유연한 API 설계가 가능한 구조가 됩니다.

하지만 Selector 이름이 중복되어 동시에 적용될 수 있다는 단점도 trade-off가 생기게 됩니다.

또한 디렉티브는 같은 NgModule 내에서만 적용되어야 하기 때문에 framework complexity를 증가시키는 trade-off가 있었다고 합니다.

Unidirectional Data flow

앵귤러는 템플릿에 이런 형식으로 view state를 반영합니다.

<p>{{userBiography}}</p>

view state가 변했는지는 기존 값을 비교하여 dirty checking 합니다.

if (newValue !== oldValue) {
  updateDom(newValue);
  oldValue = newValue;
}

Angular(2+)는 AngularJS에서 새롭게 시작하는 프로젝트이기 때문에 AngularJS에 영향을 받았는데

AngularJS에서는 $digest 디렉티브 문제가 있었다고 합니다.

사실 저도 AngularJS는 건드려본적이 없기 때문에 어떤 문제인지는 리서치를 통해서 훑어 보았는데요.

$digest는 현재까지 생성된 $scope valiable들의 watcher를 모두 실행하여 최신 값으로 업데이트 해주는 기능이라고 합니다.

기본적으로 10번 정도 실행되기 때문에 앱의 퍼포먼스를 떨어트린다고 합니다.

Angular에서는 이 문제점을 해결하기 위해서 Change Detection 실행시 DOM order에 맞춰서 Change Detection을 실행하도록 디자인 하였다고 합니다.

Angular(2+) 에서는 $watch $digest 기능을 제거하고 CD가 돌았을때 dom order에 맞춰 순차적으로 돌아가고 tempalte expression을 실행시키도록 돌아가게 됩니다.

이런 디자인을 채택하여서 $digest처럼 10번 도는 현상을 방지할 수 있었다고 합니다.

만약 template expression이 CD 도중 값이 변경되면 NG0100 에러를 내뱉도록 안정장치를 걸어둔 셈이죠.

https://angular.io/errors/NG0100 (너무 유명한 에러여서 공식 블로그에도 해결 방법이 있습니다.)

0개의 댓글