@defer 블록은 지연 로딩 뷰를 선언하는 블록입니다.
애플리케이션 초기 렌더링에 꼭 필요하지 않은 코드를 나중에 불러오는 방식으로 애플리케이션 첫 실행에 필요한 파일의 크기를 줄일 수 있습니다.
뷰를 지연 로딩 하면 Core Web Vitals(CWV)나 Largest Contentful Paint(LCP), Time to First Byte(TTFB)가 향상되는 경우가 많습니다.
뷰를 지연 로딩하려면 템플릿을 @defer 블록으로 감싸면 됩니다:
@defer { <large-component />}
@defer 블록 안에 있는 컴포넌트나 디렉티브, 파이프는 별도 JavaScript 파일로 분리되며, 나머지 템플릿이 렌더링 된 후 이 구성요소가 필요한 경우에 로드됩니다.
지연 로딩 뷰를 지정하면서 다양한 트리거, 사전 로딩 옵션, 플레이스 홀더, 로딩, 에러 상태 관리 등의 기능을 활용할 수 있습니다.
어떤 항목을 지연 로딩 할 수 있나요?
컴포넌트, 디렉티브, 파이프, 컴포넌트 CSS 스타일은 애플리케이션 최초 실행에서 제외하여 지연 로딩 할 수 있습니다.
@defer 블록 안에 있는 구성요소를 정말 지연 로딩하려면, 다음 조건을 만족해야 합니다:
- 독립(standalone) 구성요소여야 합니다. 그렇지 않으면
@defer블록 안에 있더라도 로딩이 지연되지 않고 즉시 로드 됩니다. - 같은 파일의
@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 조건으로 동작합니다.
트리거는 크게 on과 when 으로 구분할 수 있습니다.
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!');});
@defer를 NgModule과 함께 사용할 수 있나요?
@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 트리거 사용을 피하세요.