코인원이 쓰는 Angular 에서 Change Detection 을 최적화하는 방법을 예제로 짚어가는 글이다. 최적화 축을 실행 횟수와 한 번 실행될 때의 CPU 사용 둘로 나누고, OnPush 전략부터 Zone 밖 이벤트 바인딩, detectChanges 수동 호출, immer 를 통한 참조 유지까지 단계별로 보여준다.
핵심 포인트- Change Detection 은 DOM 을 업데이트하는 게 아니라 업데이트할지 체크만 하는 과정이다. 다만 그 체크가 매우 빈번해 최적화 대상이 된다.
- 기본 전략에서는 한 컴포넌트에서 이벤트가 나면 아무 상관없는 컴포넌트들까지 전부 Change Detection 이 돈다. 형제 컴포넌트도 예외가 아니다.
- 첫 조치는 모든 컴포넌트의 changeDetection 을 OnPush 로 바꾸는 것이다. 그러면 이벤트 발생 컴포넌트와 그 상위만 돈다.
- Angular 의 click 바인딩은 Zone 안에서 로직을 돌려 Change Detection 을 유발한다. RxJS 의 fromEvent 로 Zone 밖에서 바인딩하면 Angular 가 이벤트 존재를 몰라 불필요한 실행이 사라진다.
- 값이 변한 컴포넌트만 돌리는 유일한 방법은 ChangeDetectorRef 의 detectChanges 를 직접 호출하는 것이다.
상세 정리- 예제는 parent 와 child-one, child-two 세 컴포넌트로 구성하고 각자 Change Detection 이 돌 때 콘솔에 로그를 남기게 했다.
- 기본 전략에서는 앱이 처음 뜰 때 컴포넌트마다 두 번씩 돈다. OnPush 로 바꾸면 한 번씩만 돈다.
- Zone 밖 바인딩의 부작용도 짚는다. 이벤트로 Change Detection 이 안 돌기 때문에 컴포넌트 프로퍼티 값은 갱신돼 콘솔에 찍히지만 템플릿에 바인딩된 값은 그대로다.
- 이를 위해 async pipe 를 쓴다. 템플릿에서 Observable 을 구독해 값이 올 때마다 Change Detection 을 실행시킨다.
- 하지만 async pipe 는 현재 컴포넌트와 모든 상위 컴포넌트에 markForCheck 를 걸어 상위까지 함께 돈다. 상위가 없거나 상위가 돌아도 문제없을 때 편의상 쓰라는 결론을 낸다.
- detectChanges 를 쓰면 값 변경 후 자기 컴포넌트만 갱신돼 부모의 Change Detection 이 돌지 않는다.
- props 전달도 실행 조건이다. Angular 는 부모에서 받은 값이 이전 참조와 다른지 엄격 동등 비교로 판단한다.
- 숫자 같은 primitive 는 값 전달이라 같은 0 을 계속 넘기면 자식은 돌지 않고, 값이 매번 달라지면 자식도 돈다. 여기까지는 의도대로다.
- 문제는 Object 나 Array 다. 참조 전달이라 모양이 같아도 새로 만든 객체는 다른 것으로 판정돼 내부 값이 그대로여도 자식의 Change Detection 이 매번 돈다.
- 해결책 두 가지를 든다. Immutable.js 나 immer 처럼 Structural Sharing 을 하는 라이브러리를 쓰거나, 다름의 기준을 엄격 동등 비교가 아닌 수동 체크로 바꾸는 것이다. 두 번째는 네이티브 DOM 을 다루는 서드파티 라이브러리에 유용하지만 복잡해 다루지 않는다.
- immer 의 produce 는 값이 실제로 바뀌지 않으면 인자로 받은 객체를 그대로 반환한다. 참조가 같아 자식은 돌지 않고, 값이 바뀌면 새 객체를 만들어 정상적으로 돈다.
- 두 번째 축은 Change Detection 한 번의 무게다. 템플릿에 바인딩된 메서드가 무거우면 실행될 때마다 불려 UI 스레드를 막는다.
- 이를 보여주려고 인자만큼 for 문을 도는 heavyCalc 를 템플릿에 바인딩하고, 애니메이션이 걸린 progress 태그로 렌더링이 멈추는 것을 눈에 보이게 만들었다.
- 서두에서 프레임워크가 알아서 해준다는 생각 때문에 성능 최적화가 방치되기 쉽다는 점을 지적하며, 가장 빠른 프론트엔드 프레임워크는 프레임워크를 쓰지 않는 것이라는 비교도 인용한다.
왜 읽나Angular 앱이 느려졌는데 원인을 못 짚고 있는 프론트엔드 개발자에게 Change Detection 이 언제 도는지와 단계별 차단 수단을 예제로 보여주는 글.