제가 @defer를 며칠 만에 세분화된 코드 스플리팅에 구현한 정확한 방법 — 단계별

발행: (2025년 12월 11일 오후 06:00 GMT+9)
8 min read
원문: Dev.to

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 은 번들을 미리 로드하지만 렌더링은 하지 않습니다.

빌드 결과 확인 방법

  1. ng build 를 실행하고 defer-heavy-chart.js 와 같은 새로운 청크가 생성됐는지 확인합니다.
  2. 앱을 서빙하고 Chrome DevTools → Network 탭을 열어, 무거운 번들이 트리거가 발생했을 때만 로드되는지 확인합니다.
  3. 단위 테스트에서는 다음과 같이 defer 상태를 시뮬레이션할 수 있습니다:
TestBed.deferBlockBehavior = DeferBlockBehavior.Manual;

실제 적용 예시: 대시보드 매직

@defer (on viewport; prefetch on idle) {
  
}
@placeholder (minimum 500ms) {
  
}

데이터가 무거운 대시보드 위젯을 이렇게 감싸면 복잡한 페이지에서 메인 번들이 30 % 이상 감소하여 아래‑폴드 영역에서도 부드러운 경험을 제공합니다.

핵심 트리거 및 서브‑블록

트리거설명
idlerequestIdleCallback 으로 브라우저가 유휴 상태일 때 실행(기본).
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 주의점

  1. 탐색기 설치: npm i -D source-map-explorer.
  2. 소스맵 포함 빌드: ng build --source-map.
  3. 분석 실행:
npx source-map-explorer dist/**/main.js

인터랙티브 트리맵을 통해 무거운 standalone 컴포넌트가 분리되면서 메인 번들이 얼마나 줄어드는지 확인할 수 있습니다.

HMR 주의: 개발 서버는 모든 @defer 청크를 즉시 로드해 트리거를 우회합니다. 정확한 테스트를 위해 HMR을 비활성화하세요:

ng serve --no-hmr

시각적 예시

Tools for Measuring @defer Bundle Gains

ARIA Live 영역을 활용한 접근성

스크린 리더가 플레이스홀더에서 로드된 콘텐츠로 전환되는 순간을 놓칠 수 있습니다. defer 블록을 ARIA live 속성이 있는 컨테이너로 감싸면 동적 콘텐츠 변화가 자동으로 알림됩니다:

@defer (on hover) {  }
@placeholder {  }
@loading {  }
@error {  }

이렇게 하면 UI가 성능을 유지하면서도 모두에게 포용적인 경험을 제공할 수 있습니다.

Back to Blog

관련 글

더 보기 »

🚀 Timeout Tamer: 인내가 진보를 만났을 때

두려운 교착 상태 그리도피아 땅에서 마법사 루나는 그녀의 가장 위대한 주문—427 rows of enchanted data를 퍼블리싱—을 준비했다. 그녀는 마법 지팡이를 휘두르며 클릭했다.