제가 @defer를 며칠 만에 세분화된 코드 스플리팅에 구현한 정확한 방법 — 단계별
Source: Dev.to
@defer 기본 개념 및 설정
Angular의 @defer 블록은 성능을 크게 개선해 주는 기능으로, 차트나 대시보드와 같은 무거운 템플릿 섹션을 별도의 JavaScript 번들로 분리하여 뷰포트 진입, 사용자 상호작용, 브라우저 유휴 시간 등 이벤트가 발생했을 때만 로드하도록 합니다. 이를 통해 초기 페이로드가 크게 줄어들어 Largest Contentful Paint (LCP)와 전체 앱 시작 속도가 빨라집니다. 특히 모든 사용자가 즉시 모든 기능을 필요로 하지 않는 대규모 싱글 페이지 앱에서 효과적입니다.
성공을 위한 요구 사항
- Angular v17 이상 (Angular 18부터 안정적인 defer 가능한 뷰 제공).
@defer블록 안에 있는 컴포넌트, 디렉티브, 파이프는 standalone이어야 하며 같은 파일 내 다른 곳에서 참조되지 않아야 합니다(예:ViewChild쿼리나 조기 로드를 일으키는 외부 임포트 금지).- 전이적 의존성은 standalone이든 NgModule 기반이든 혼합 가능하지만, 의존성 로드가 앞서 일어나므로 플레이스홀더는 가볍게 유지하세요.
기본 문법 예시
@defer (on viewport) {
}
@placeholder {
}
- 트리거:
idle(기본),interaction,hover,timer(2s), 혹은 커스텀when showLargeChart. - 로딩 표시:
@loading (minimum 1s) { } - 오류 처리:
@error { Retry? } - 프리패치:
prefetch on idle은 번들을 미리 로드하지만 렌더링은 하지 않습니다.
빌드 결과 확인 방법
ng build를 실행하고defer-heavy-chart.js와 같은 새로운 청크가 생성됐는지 확인합니다.- 앱을 서빙하고 Chrome DevTools → Network 탭을 열어, 무거운 번들이 트리거가 발생했을 때만 로드되는지 확인합니다.
- 단위 테스트에서는 다음과 같이 defer 상태를 시뮬레이션할 수 있습니다:
TestBed.deferBlockBehavior = DeferBlockBehavior.Manual;
실제 적용 예시: 대시보드 매직
@defer (on viewport; prefetch on idle) {
}
@placeholder (minimum 500ms) {
}
데이터가 무거운 대시보드 위젯을 이렇게 감싸면 복잡한 페이지에서 메인 번들이 30 % 이상 감소하여 아래‑폴드 영역에서도 부드러운 경험을 제공합니다.
핵심 트리거 및 서브‑블록
| 트리거 | 설명 |
|---|---|
idle | requestIdleCallback 으로 브라우저가 유휴 상태일 때 실행(기본). |
viewport | 요소가 뷰포트에 들어올 때 로드(Intersection Observer 사용). |
interaction | 클릭, 키다운 등 사용자 상호작용 시 실행. |
hover | 마우스오버 또는 포커스인 시 실행. |
immediate | 비‑defer 콘텐츠가 렌더링된 직후 로드. |
timer(2s) | 지정된 지연 시간 후 로드. |
여러 트리거는 세미콜론으로 결합해 OR 논리로 사용할 수 있습니다. 프리패치는 별도로 지정합니다. 예:
@defer (on viewport; prefetch on idle) { ... }
원활한 UX를 위한 서브‑블록
@placeholder– defer된 번들이 로드되기 전 미리 보여줄 가벼운 콘텐츠. 깜빡임 방지를 위해minimum 500ms등을 사용할 수 있습니다.@loading– 트리거가 발생하고 번들이 해결되기 전 표시.after 100ms; minimum 1s와 같은 파라미터로 타이밍을 제어합니다.@error– 로드 실패 시 대체 UI를 표시. 화면 읽기 프로그램을 위해aria-live="polite"컨테이너에 감싸세요.
@defer (on hover) {
}
@placeholder { }
@loading { }
@error { }
when 으로 커스텀 트리거 만들기
@defer (when isVisible()) {
}
when 은 신호나 표현식을 한 번 평가하고, 이후에는 되돌아가지 않습니다. 프리패치와도 조합 가능:
@defer (when userClicked(); prefetch when dataReady) { ... }
고급 패턴 및 최적화
중첩 @defer 와 연쇄 호출 방지
중첩 defer 블록은 계층형 lazy loading을 가능하게 하지만, 잘못 쌓이면 여러 청크가 동시에 요청될 수 있습니다. 트리거를 단계적으로 배치해 연쇄 요청을 피하세요:
@defer (on viewport) {
@defer (on interaction) {
} @placeholder {
Click to expand
}
} @placeholder {
Scroll to see more
}
@if (isExpanded) {
}
- 외부 블록은 섹션이 뷰포트에 들어올 때만 로드합니다.
- 내부 블록은 명시적인 상호작용을 기다려 네트워크 요청 폭주를 방지합니다.
SSR/SSG 와 하이드레이션 매직
- 서버에서는
@placeholder내용(또는 아무것도)만 렌더링되고, 트리거는 무시됩니다(뷰포트나 유휴 상태가 없기 때문). - 클라이언트 하이드레이션 시 설정된 트리거가 작동해 UI가 인터랙티브해집니다.
hydrate on트리거와 함께 사용하면 SEO 친화적인 서버 렌더링을 유지하면서 클라이언트 번들을 부풀리지 않을 수 있습니다. 프리패치(prefetch on idle)와 조합하면 하이드레이션 전에 코드를 미리 로드할 수 있습니다.
번들 분석 및 HMR 주의점
- 탐색기 설치:
npm i -D source-map-explorer. - 소스맵 포함 빌드:
ng build --source-map. - 분석 실행:
npx source-map-explorer dist/**/main.js
인터랙티브 트리맵을 통해 무거운 standalone 컴포넌트가 분리되면서 메인 번들이 얼마나 줄어드는지 확인할 수 있습니다.
HMR 주의: 개발 서버는 모든 @defer 청크를 즉시 로드해 트리거를 우회합니다. 정확한 테스트를 위해 HMR을 비활성화하세요:
ng serve --no-hmr
시각적 예시

ARIA Live 영역을 활용한 접근성
스크린 리더가 플레이스홀더에서 로드된 콘텐츠로 전환되는 순간을 놓칠 수 있습니다. defer 블록을 ARIA live 속성이 있는 컨테이너로 감싸면 동적 콘텐츠 변화가 자동으로 알림됩니다:
@defer (on hover) { }
@placeholder { }
@loading { }
@error { }
이렇게 하면 UI가 성능을 유지하면서도 모두에게 포용적인 경험을 제공할 수 있습니다.