Zone.js Hijacked Your Browser for 8 Years. Here's What Replaced It.
Zone.js는 8년간 브라우저를 가로챘습니다. Angular는 무엇으로 바꿨을까요?
Angular는 Zone.js가 브라우저 비동기 API를 가로채 변경 감지를 시작하던 방식에서 Signals 기반의 Zoneless 구조로 옮겨가고 있습니다. 글은 두 방식의 작동 원리와 마이그레이션 때 일반 필드 변경이 화면에 반영되지 않는 문제, RxJS Observable을 Signal로 연결하는 방법을 설명합니다.
- 주제
에디터 노트
Zoneless 전환에서 가장 큰 변화는 검사 비용보다 알림 책임이 개발자 코드 쪽으로 이동한다는 점입니다. 글은 Signal을 템플릿이 읽으면 변경 표시가 전파된다고 설명하고, 댓글은 마지막 렌더링에서 읽지 않은 Signal이나 서비스의 일반 필드는 자동 갱신을 보장하지 않는다고 짚습니다. 둘은 같은 조건을 강조합니다. Zone.js가 추가 검사를 실행하던 환경에서 동작한 코드는 Zoneless에서 조용히 멈출 수 있습니다. 따라서 글의 Signal 우선 마이그레이션 제안은 댓글의 경험적 조언과 맞닿아 있습니다. 다만 제시된 글은 변경 감지 알림 다섯 가지와 코드 예제를 중심으로 설명하며, 대규모 코드베이스에서 의존성을 찾아 전환하는 절차나 검증 기준은 다루지 않습니다. 성능 개선 설명도 구체적인 비교 측정값 없이 작동 방식에 근거합니다. 독자는 아키텍처 설명과 실제 성능 결과를 구분해 읽을 필요가 있습니다.
AI 요약
Angular는 오랫동안 Zone.js로 브라우저의 비동기 동작을 감지해 변경 감지를 실행했습니다. Zone.js는 setTimeout, 이벤트 처리기, 네트워크 요청 같은 API를 패치하고 작업이 끝나면 Angular에 알렸습니다. 하지만 어떤 데이터가 바뀌었는지는 알지 못했습니다. Angular는 알림을 받을 때마다 ApplicationRef.tick()을 실행해 루트부터 컴포넌트 트리를 훑고 템플릿 바인딩을 확인했습니다.
전체 검사에서 Signals 기반 업데이트로
글은 대시보드에 컴포넌트가 2,000개 있고 백그라운드 위젯의 setInterval이 실행되는 경우를 예로 듭니다. 실제로 바뀐 컴포넌트가 일부여도 전체 트리 검사가 반복될 수 있습니다. OnPush는 입력 참조 변경, 해당 컴포넌트의 템플릿 이벤트, AsyncPipe의 값 방출, markForCheck() 호출 같은 조건에서만 하위 트리를 확인하도록 제한합니다. 대신 객체를 제자리에서 수정하면 참조가 바뀌지 않아 화면에 갱신이 반영되지 않는 상황이 생길 수 있다고 설명합니다.
현대 Angular의 Signals는 값과 그 값을 읽는 소비자 사이의 의존 관계를 기록합니다. computed()가 count()와 multiplier()를 읽으면 두 Signal이 계산값의 의존성으로 등록됩니다. Signal이 바뀌면 의존한 소비자에 먼저 변경 표시를 전파하고, 새 값은 실제로 읽힐 때 계산합니다. 글은 이 방식을 push-pull 알고리즘으로 설명하며, 활성 템플릿에서 읽지 않는 계산은 실행하지 않는다고 말합니다.
Zoneless 모드와 변경 감지 알림
Zone.js를 제거하면 Angular는 브라우저 API를 전역으로 패치하지 않습니다. 대신 ChangeDetectionScheduler가 명시적인 알림을 받을 때 변경 감지를 예약합니다. 글이 꼽은 알림은 템플릿에서 읽은 Signal의 값 변경, 템플릿 이벤트 처리, AsyncPipe의 새 값, ComponentRef.setInput()으로 전달한 입력 변경, ChangeDetectorRef.markForCheck() 호출입니다. 여러 알림은 queueMicrotask를 사용해 다음 마이크로태스크에서 한 번으로 묶고, 변경 표시된 뷰를 검사한다고 설명합니다.
마이그레이션 때 달라지는 코드
템플릿 이벤트 처리기 안에서 값을 바꾸는 경우와 달리 WebSocket, RxJS 스트림, 네이티브 콜백에서 일반 필드를 수정하면 Zoneless Angular가 변경을 알지 못할 수 있습니다. 글은 화면 상태를 Signal로 바꾸고 값 변경 때 set()을 호출하는 방법을 제안합니다. RxJS Observable을 직접 구독해 일반 필드에 저장하는 대신 toSignal()로 Signal에 연결하는 예도 듭니다.
effect() 안에서 읽은 Signal은 모두 의존성으로 등록되므로, 부수 효과를 다시 실행할 조건에 포함하지 않을 값은 untracked()로 읽으라고 설명합니다. 글은 Zone.js 제거로 전역 패치와 불필요한 트리 검사, Zone 내부 프레임이 줄고 Signals 기반의 세밀한 의존성 추적이 가능해진다고 정리합니다.
dev.to 반응
- @yunusemrevurgun — 예전 tick() 파이프라인을 잘 설명했습니다. 마이그레이션할 때 실용적인 점 하나를 덧붙이면, Zoneless는 실패를 없애는 게 아니라 실패 양상을 바꿉니다. Signals는 Zone.js가 흉내 내던 세밀한 알림을 제공하지만, 마지막 렌더링 때 템플릿에서 실제로 읽은 Signal에만 해당합니다. 컴포넌트가 서비스의 일반 변경 가능 필드를 계속 읽는다면 Zoneless에서는 화면이 조용히 갱신되지 않습니다. Zone.js에서는 필요 이상으로 다시 검사했을 뿐입니다. 제가 권하는 마이그레이션 순서는 클래스 필드를 먼저 Signal로 바꾸고, 그다음 Zoneless 변경 감지를 켠 뒤, 예전에는 알아서 동작하던 경로를 하나씩 눌러 확인하는 것입니다.
- @smtahosin — 정말 유용한 마이그레이션 팁입니다! Zone.js는 일반 필드의 의존성을 추가 검사로 감추곤 해서 Zoneless로 바꾸면 문제가 꽤 빨리 드러납니다. 먼저 필드를 Signal로 바꾸는 편이 훨씬 안전한 경로 같네요. 체크리스트를 공유해 주셔서 감사합니다!
원문: dev.to / 번역·요약: Trawling