심화 가이드
템플릿

뷰 지연 로딩: @defer

@defer 블록은 지연 로딩 뷰를 선언하는 블록입니다. 애플리케이션 초기 렌더링에 꼭 필요하지 않은 코드를 나중에 불러오는 방식으로 애플리케이션 첫 실행에 필요한 파일의 크기를 줄일 수 있습니다. 뷰를 지연 로딩 하면 Core Web Vitals(CWV)나 Largest Contentful Paint(LCP), Time to First Byte(TTFB)가 향상되는 경우가 많습니다.

뷰를 지연 로딩하려면 템플릿을 @defer 블록으로 감싸면 됩니다:

      
@defer {  <large-component />}

@defer 블록 안에 있는 컴포넌트나 디렉티브, 파이프는 별도 JavaScript 파일로 분리되며, 나머지 템플릿이 렌더링 된 후 이 구성요소가 필요한 경우에 로드됩니다.

지연 로딩 뷰를 지정하면서 다양한 트리거, 사전 로딩 옵션, 플레이스 홀더, 로딩, 에러 상태 관리 등의 기능을 활용할 수 있습니다.

어떤 항목을 지연 로딩 할 수 있나요?

컴포넌트, 디렉티브, 파이프, 컴포넌트 CSS 스타일은 애플리케이션 최초 실행에서 제외하여 지연 로딩 할 수 있습니다.

@defer 블록 안에 있는 구성요소를 정말 지연 로딩하려면, 다음 조건을 만족해야 합니다:

  1. 독립(standalone) 구성요소여야 합니다. 그렇지 않으면 @defer 블록 안에 있더라도 로딩이 지연되지 않고 즉시 로드 됩니다.
  2. 같은 파일의 @defer 블록 밖에서 사용되지 않아야 합니다. @defer 블록 밖에서도 사용되거나 ViewChild 쿼리 등으로 참조되면 해당 항목은 즉시 로드됩니다.

@defer 블록 안에 사용된 컴포넌트, 디렉티브, 파이프의 내부 의존성 구성요소는 독립 구성요소가 아니어도 됩니다; 이 항목들은 NgModule에 등록해서 모듈 단위로 지연 로딩 할 수 있습니다.

Angular 컴파일러는 @defer 블록에 사용된 개별 컴포넌트, 디렉티브, 파이프마다 동적 불러오기(dynamic import)를 적용합니다. 그래서 이 블록의 내용은 모든 로딩이 끝난 후에 렌더링됩니다. 로딩 순서는 보장하지 않습니다.

지연 로딩 단계 관리하기

@defer 블록은 지연 로딩 과정을 세분화하는 하위 블록으로 구성할 수 있습니다.

@defer

지연 로딩 뷰를 정의하는 기본 블록입니다. 지연 로딩되는 뷰는 화면의 첫 렌더링에 포함되지 않고, 트리거(trigger)가 동작하거나 when 조건이 맞을 때만 로딩되어 렌더링됩니다.

기본적으로 @defer 블록은 브라우저가 [대기(idle)] (/guide/defer#idle) 상태가 되었을 때 트리거됩니다.

      
@defer {  <large-component />}

렌더링 위치 지정하기: @placeholder

기본적으로 @defer 블록은 트리거가 동작하기 전까지 아무것도 렌더링되지 않습니다.

이 때 @defer 블록이 표시되기 전에 화면에 표시할 내용이 있다면, @placeholder 블록을 지정하면 됩니다.

      
@defer {  <large-component />} @placeholder {  <p>Placeholder content</p>}

@placeholder 블록이 필수인 것은 아니지만, 어떤 트리거는 @placeholder템플릿 참조 변수가 필요합니다. 자세한 내용은 트리거 섹션을 참고하세요.

@defer 블록은 로딩된 후에 @placeholder 블록을 대체하며 렌더링됩니다. 그리고 @placeholder 블록에는 일반 HTML, 컴포넌트, 디렉티브, 파이프를 자유롭게 사용할 수 있습니다. @placeholder 블록에 사용되는 컴포넌트, 디렉티브, 파이프는 지연 로딩되지 않고 즉시 로딩 된다는 것을 기억하세요.

@placeholder 블록을 사용할 때 minimum 옵션을 사용할 수 있습니다. 이 옵션은 @placeholder 블록이 처음 렌더링 된 후, 표시되는 최소 시간을 지정하는 옵션입니다.

      
@defer {  <large-component />} @placeholder (minimum 500ms) {  <p>Placeholder content</p>}

minimum 변수에는 밀리초(ms) 단위나 초(s) 단위 시간을 지정합니다. 이 옵션은 지연 로딩되는 뷰가 너무 빨리 로딩되는 경우에 화면이 깜빡이는 것을 방지하는 용도로 사용합니다.

로딩 표시하기: @loading

지연 로딩되는 뷰가 로딩중일때 표시할 내용이 있다면 @loading 블록을 사용합니다. @loading 블록은 트리거가 동작하고 나면 @placeholder 블록 대신 화면에 표시됩니다.

      
@defer {  <large-component />} @loading {  <img alt="loading..." src="loading.gif" />} @placeholder {  <p>Placeholder content</p>}

@loading 블록에 사용되는 컴포넌트, 디렉티브, 파이프도 @Placeholder 블록과 비슷하게 즉시 로딩됩니다.

@loading 블록은 지연 로딩하는 뷰가 너무 빨리 로딩되어 화면이 깜빡이는 것을 방지하기 위해 옵션을 2개 받을 수 있습니다:

  • minimum - @placeholder 블록이 표시될 최소 시간
  • after - @loading 블록이 로딩되고 표시되기 전까지 대기할 시간
      
@defer {  <large-component />} @loading (after 100ms; minimum 1s) {  <img alt="loading..." src="loading.gif" />}

두 옵션 모두 밀리초(ms)나 초(s) 단위를 지정합니다. 이 타이머는 로딩 트리거가 동작한 직후부터 시작됩니다.

지연 로딩 에러 표시하기: @error

뷰 지연 로딩에서 발생하는 오류를 표시하려면 @error 블록을 사용합니다. @error 블록 안에 사용되는 구성요소는 @placeholder, @loading 블록과 비슷하게 즉시 로딩됩니다.

      
@defer {  <large-component />} @error {  <p>Failed to load large component.</p>}

트리거로 지연 로딩 제어하기

뷰 지연 로딩을 제어하려면 트리거(triggers) 를 사용하면 됩니다.

@defer 블록은 트리거가 동작하고 난 후 지연 로딩되면서 @placeholder 블록을 대체하면서 렌더링됩니다.

이 때 세미 콜론(;)을 사용해서 이벤트 트리거를 여러개 지정할 수 있으며, 이렇게 지정된 트리거는 OR 조건으로 동작합니다.

트리거는 크게 onwhen 으로 구분할 수 있습니다.

on

on@defer 블록 트리거가 동작하는 조건을 지정합니다.

이런 트리거를 사용할 수 있습니다:

트리거 설명
idle 브라우저가 대기 상태일 때 동작합니다.
viewport 특정 항목이 뷰포트에 진입할 때 동작합니다.
interaction 사용자가 특정 엘리먼트와 상호작용 할 때 동작합니다.
hover 마우스가 특정 영역 위에 올라갈 때 동작합니다.
immediate 지연 로딩이 아닌 뷰 렌더링이 끝난 직후 동작합니다.
timer 특정 시간 뒤에 동작합니다.

idle

idle 트리거는 브라우저가 대기 상태로 진입할 때 동작해서 지연 로딩 뷰를 로드합니다. 이 트리거가 기본값입니다.

      
<!-- @defer (on idle) -->@defer {  <large-cmp />} @placeholder {  <div>Large component placeholder</div>}

viewport

viewport 트리거는 Intersection Observer API를 활용해서 특정 항목이 뷰포트에 진입할 때 동작합니다. 이 때 특정 항목은 @placeholder 항목이거나 명시적으로 지정한 엘리먼트가 됩니다.

기본적으로 @defer 블록은 @placeholder 가 뷰포트에 진입하는 것을 감지합니다. 이 경우 @placeholder 는 반드시 엘리먼트 하나여야 합니다.

      
@defer (on viewport) {  <large-cmp />} @placeholder {  <div>Large component placeholder</div>}

아니면 대상 엘리먼트에 템플릿 참조 변수를 지정한 후에 뷰포트 트리거로 전달하면 됩니다.

      
<div #greeting>Hello!</div>@defer (on viewport(greeting)) {  <greetings-cmp />}

interaction

interaction 트리거는 사용자가 특정 엘리먼트와 click 이벤트나 keydown 이벤트로 상호작용할 때 동작합니다.

기본적으로 @placeholder는 상호작용하는 엘리먼트로 간주됩니다. 이 경우 @placeholder는 엘리먼트 하나여야 합니다.

      
@defer (on interaction) {  <large-cmp />} @placeholder {  <div>Large component placeholder</div>}

아니면 대상 엘리먼트에 템플릿 참조 변수를 지정한 후에 상호작용 트리거로 전달하면 됩니다.

      
<div #greeting>Hello!</div>@defer (on interaction(greeting)) {  <greetings-cmp />}

hover

hover 트리거는 마우스가 어떤 영역으로 이동해서 mouseover 이벤트나 focusin 이벤트가 발생했을 때 동작합니다.

기본적으로 @placeholder는 상호작용하는 엘리먼트로 간주됩니다. 이 경우 @placeholder는 엘리먼트 하나여야 합니다.

      
@defer (on hover) {  <large-cmp />} @placeholder {  <div>Large component placeholder</div>}

아니면 대상 엘리먼트에 템플릿 참조 변수를 지정한 후에 트리거로 전달하면 됩니다.

      
<div #greeting>Hello!</div>@defer (on hover(greeting)) {  <greetings-cmp />}

immediate

immediate 트리거는 지연 로딩 뷰를 즉시 로드합니다. 다르게 표현하면, 지연 로딩이 아닌 부분의 렌더링이 끝난 직후, 지연 로딩 하도록 지정된 나머지 뷰를 로드합니다.

      
@defer (on immediate) {  <large-cmp />} @placeholder {  <div>Large component placeholder</div>}

timer

timer 트리거는 특정 시간이 지난 후에 동작합니다.

      
@defer (on timer(500ms)) {  <large-cmp />} @placeholder {  <div>Large component placeholder</div>}

이 때 지연시간은 밀리초(ms) 단위나 초(s) 단위를 사용합니다.

when

when 트리거는 조건 표현식을 인자로 받으며 이 조건이 참으로 평가될 때 뷰를 로딩합니다.

      
@defer (when condition) {  <large-cmp />} @placeholder {  <div>Large component placeholder</div>}

조건식은 한 번만 평가됩니다. 조건이 참이었다가 거짓으로 평가되더라도 @placeholder 블록이 다시 표시되거나 하지는 않습니다.

사전 로딩하기: prefetch

내용을 로딩하는 조건문을 지정하면서 사전 로딩 트리거(prefetch trigger) 를 옵션으로 지정할 수 있습니다. 이 트리거를 사용하면 지연 로딩하는 내용을 표시하기 전에 @defer 블록과 연결된 JavaScript를 로드할 수 있습니다.

사전 로딩을 활용하면 사용자가 지연 로딩 뷰를 실제로 보기 전이나 특정 블록과 상호작용하기 전에 리소스를 빠르게 로딩해 둘 수 있습니다.

사전 로딩 트리거는 블록 트리거와 비슷하지만, prefetch 키워드가 접두사로 붙습니다. 블록 트리거와 사전 로딩 트리거를 함꼐 사용하는 경우에는 세미 콜론(;)으로 구분합니다.

아래 예제처럼 구현하면, 브라우저가 대기 상태로 진입하는 시점에 사전 로딩이 시작되며, @defer 블록의 내용은 사용자가 @placeholder 블록과 상호작용 할 때 렌더링됩니다.

      
@defer (on interaction; prefetch on idle) {  <large-cmp />} @placeholder {  <div>Large component placeholder</div>}

@defer 블록 테스트하기

Angular는 다양한 트리거와 함께 @defer 블록을 테스트할 수 있는 TestBed API를 제공합니다. 기본적으로 @defer 블록은 실제 애플리케이션이 동작하는 것과 동일하게 테스트 환경에서 동작합니다. 하지만 TestBed를 수동으로 설정하면 각 단계를 직접 조작할 수 있습니다.

      
it('should render a defer block in different states', async () => {  // defer 블록의 동작 방식을 수동으로 조작합니다. 시작 상태는 "paused" 입니다.  TestBed.configureTestingModule({deferBlockBehavior: DeferBlockBehavior.Manual});  @Component({    // ...    template: `      @defer {        <large-component />      } @placeholder {        Placeholder      } @loading {        Loading...      }    `  })  class ComponentA {}  // 컴포넌트 픽스처를 생성합니다.  const componentFixture = TestBed.createComponent(ComponentA);  // defer 블록을 모두 참조하고 그 중 첫번째 블록을 가져옵니다.  const deferBlockFixture = (await componentFixture.getDeferBlocks())[0];  // placeholder 블록이 렌더링 된 것을 확인합니다.  expect(componentFixture.nativeElement.innerHTML).toContain('Placeholder');  // 로딩 상태로 바꾸고 loading 블록이 렌더링 된 것을 확인합니다.  await deferBlockFixture.render(DeferBlockState.Loading);  expect(componentFixture.nativeElement.innerHTML).toContain('Loading');  // 최종 상태로 바꾸고 최종 렌더링을 확인합니다.  await deferBlockFixture.render(DeferBlockState.Complete);  expect(componentFixture.nativeElement.innerHTML).toContain('large works!');});

@deferNgModule과 함께 사용할 수 있나요?

@defer 블록은 독립 구성요소는 물론이고 NgModule 기반 컴포넌트, 디렉티브, 파이프와도 함께 사용할 수 있습니다. 하지만 실제로 지연 로딩되는 것은 독립 컴포넌트, 독립 디렉티브, 독립 파이프 뿐입니다. NgModule 기반으로 등록된 컴포넌트, 디렉티브, 파이프는 지연 로딩되지 않으며 즉시 로드됩니다.

서버 사이드 렌더링(SSR), 정적 사이트 생성(SSG)인 경우는 @defer가 어떻게 동작하나요?

SSR이나 SSG와 같이 애플리케이션이 서버에서 렌더링되는 경우는 기본적으로 @placeholder 블록이 있는 경우는 @placeholder 블록을 렌더링하고, @placeholder 블록이 없으면 아무것도 렌더링하지 않습니다. 그리고 클라이언트에서 애플리케이션이 실행될 때 @placeholder가 하이드레이션 되면서 트리거가 동작합니다.

서버엣 ㅓ@defer 블록을 렌더링하려면 증분 하이드레이션이나 hydrate 트리거를 사용해야 합니다.

모범사례

@defer 블록 중첩 사용을 피하세요

@defer 블록이 중첩되면 트리거가 각각 동작하기 때문에 컨텐츠를 동시에 로딩할 수 없습니다. 결국 화면이 로딩되는 전체 성능에 나쁜 영향을 줍니다.

레이아웃 변경을 피하세요

첫 로딩에서 사용자의 뷰포트에 있는 컴포넌트를 지연 로딩 하지 마세요. 이 경우는 누적 레이아웃 이동(cumulative layout shift, CLS)이 발생하면서 Core Web Vital에 부정적인 영향을 줍니다.

꼭 필요한 경우라면, 첫 화면 로딩을 방해할 수 있는 immediate, timer, viewport, 커스텀 when 트리거 사용을 피하세요.