injectAsync()를 활용하여 아키텍처 변경 없이 초기 번들 크기를 줄이는 방법 (Angular 22)
요약
Angular 22에서 새롭게 도입된 injectAsync()를 사용하여 의존성 주입을 지연시키는 방법을 설명합니다. 이를 통해 불필요한 서비스의 즉시 로딩을 방지하고 초기 번들 크기를 효과적으로 줄일 수 있습니다.
핵심 포인트
- injectAsync()를 통한 지연 주입(Lazy Injection) 구현
- 의존성 그래프 전체 로드로 인한 초기 번들 크기 증가 문제 해결
- 사용자가 특정 기능을 트리거할 때만 서비스 로드 가능
- Angular 22의 DI 개선 사항을 활용한 성능 최적화
Angular 22에서 가장 개발자 친화적인 DI 개선 사항 중 하나가 조용히 출시되었는데, 대부분의 사람들은 아직 이를 간과하고 있습니다.
컴포넌트 상단에 무거운(heavyweight) 서비스를 주입(inject)하면서도, 사용자들의 90%는 실제로 그 서비스가 필요한 기능을 트리거하지 않을 것이라는 사실을 알고 있습니까? 해당 서비스를 가져와서 Angular가 그것을 미리 부팅하고, 자체 의존성까지 끌어들이면서, 갑자기 누군가 'PDF 내보내기'를 클릭할 경우에만 로드되는 번들 청크(chunk)가 생겨납니다. 익숙한 느낌이 아닙니까?
바로 이 정확한 문제 지점을 Angular 22의 주요 추가 기능 중 하나인 injectAsync()가 해결하기 위해 설계되었습니다. 그리고 이것이 어떻게 작동하는지 알게 되면, 여러분은 코드베이스 내의 모든 inject() 호출에 의문을 품기 시작할 것입니다.
본 기사에서는 다음 내용을 학습하게 됩니다:
injectAsync()가 무엇이며 왜 도입되었는지- 친숙한
inject()함수와 어떻게 다른지 - 지연 주입(lazy injection)이 의미 있는 차이를 만드는 실제 사용 사례들
- 해당 기능을 사용하는 컴포넌트에 대한 유닛 테스트 작성 방법
- Angular 22의 DI 개선 사항을 최대한 활용하기 위한 보너스 팁들
만약 이러한 심층 분석 콘텐츠가 유용하다고 생각하신다면, Medium에서 저를 팔로우하는 것을 고려해 주세요. 저는 매주 실용적인 Angular 아티클 하나씩을 게시하며, Angular 22 시리즈는 이제 막 시작 단계입니다.
예제에 들어가기 전에 간단히 알려드립니다: 여기에 제공된 코드 스니펫은 순전히 개념 이해를 위한 것입니다. 표시된 일부 구문은 이전 Angular의 패턴을 반영할 수 있습니다. 가장 최신 API와 구문을 위해서는 항상 공식 Angular 문서를 참조하십시오.
Angular 22의 의존성 주입(Dependency Injection)에서 변경된 사항
Angular 22는 2026년 6월 3일에 출시되었으며, Signal Forms가 안정화되고, Resources가 프로덕션 준비 단계로 승격되며, OnPush가 기본 변경 감지 전략이 되는 등 주요 목록과 함께 왔습니다. 이는 큰 변화입니다. 하지만 그 옆에 조용히 자리 잡은 것이 바로 injectAsync()인데, 이는 매우 구체적이고 흔한 문제를 해결하는 기능입니다.
이것이 왜 중요한지 이해하려면, 즉시 주입 (eager injection)이 실제로 어떤 비용을 발생시키는지 이해해야 합니다.
컴포넌트 내에 inject(HeavyAnalyticsService)를 작성하면, Angular는 해당 컴포넌트가 생성되는 즉시 그 의존성 (dependency)을 해결 (resolve)합니다. 서비스가 인스턴스화되고, 해당 서비스 자체의 의존성들이 해결되며, 그 서비스가 의존하는 모든 것들이 초기 로드 시점에 함께 불러와집니다. 만약 해당 서비스가 사용자가 특정 버튼(예: 보고서 내보내기 또는 고급 설정 패널 열기)을 클릭할 때만 필요하다면, 당신은 그 버튼을 한 번도 클릭하지 않을 사용자들을 포함하여 페이지에 접속하는 모든 사용자를 위해 해당 의존성 그래프 (dependency graph) 전체를 로드한 셈이 됩니다.
injectAsync()는 이러한 패턴을 깨뜨립니다. 이 함수를 사용하면 의존성을 선언하되, 실제로 필요한 순간까지 그 해결 (resolution)을 지연 (defer)할 수 있습니다.
기존 방식: inject()와 그 한계
전통적인 패턴은 다음과 같습니다. PDF 내보내기를 트리거하는 버튼이 있는 컴포넌트가 있다고 가정해 봅시다. PdfReportService는 결코 가볍지 않습니다. PDF 생성 라이브러리와 잠재적으로 몇몇 차트 유틸리티들을 포함하고 있습니다.
// pdf-report.service.ts
import { Service } from '@angular/core';
...
// dashboard.component.ts
import { Component, inject } from '@angular/core';
import { PdfReportService } from './pdf-report.service';
...
사용자가 내보내기 버튼을 누르든 안 누르든 관계없이, DashboardComponent가 생성될 때마다 PdfReportService는 인스턴스화됩니다. 대규모 애플리케이션에서는 이러한 비용이 누적됩니다.
injectAsync() 소개: 필요할 때 수행하는 지연 DI (Lazy DI on Demand)
Angular 22는 바로 이 문제를 해결하기 위해 injectAsync()를 제공합니다. 함수 시그니처는 간단합니다. 서비스 인스턴스로 해결되는 Promise를 반환하는 팩토리 함수 (factory function)를 전달하면 됩니다. Angular는 주입 컨텍스트 (injection context)를 준수하는 것을 포함하여 DI 해결 (DI resolution)을 대신 처리해 줍니다. 즉, 당신이 DI 시스템 외부로 손을 뻗을 필요가 없습니다.
다음은 injectAsync()를 사용하여 다시 작성한 동일한 대시보드 예시입니다:
// dashboard.component.ts
import { Component, injectAsync } from '@angular/core';
import { PdfReportService } from './pdf-report.service';
...
injectAsync()가 반환하는 것은 서비스 자체가 아니라 함수입니다. 이 함수를 호출하면 서비스로 해결(resolve)되는 Promise가 반환됩니다. Angular는 인스턴스가 올바른 주입 컨텍스트 (injection context) 내에서 생성되도록 보장하므로, 여러분이 기대하는 모든 DI 보장 사항이 여전히 적용됩니다.
핵심적인 동작 차이점은 서비스(및 해당 의존성 그래프)가 exportReport()가 처음 호출될 때만 해결된다는 것입니다. 해당 버튼을 클릭하지 않는 사용자들에게는 그 코드 청크(chunk)가 결코 로드되지 않습니다.
injectAsync()의 내부 동작 원리
다음과 같은 의문이 생길 수 있습니다: 만약 injectAsync()가 Promise를 반환하는 함수를 반환한다면, Angular는 어떻게 해결된 서비스에 DI를 사용해야 하는지 알 수 있을까요?
정답은 injectAsync()가 호출되는 시점, 즉 주입 컨텍스트 (injection context) 내부인 컴포넌트 생성 시점에 주입 컨텍스트를 캡처한다는 것입니다. 실제 해결은 지연되지만, 컨텍스트 참조는 보존됩니다. 이는 toSignal()과 같은 기능이 컴포넌트 초기화 중에 호출될 때 작동할 수 있게 하는 것과 동일한 메커니즘입니다.
이는 또한 한 가지 중요한 제약 사항이 있음을 의미합니다: injectAsync()는 반드시 컴포넌트 생성 시점(또는 다른 유효한 주입 컨텍스트)에 호출해야 하며, 나중에 실행되는 메서드나 라이프사이클 훅 (lifecycle hook) 내부에서 호출해서는 안 됩니다. 반환된 함수는 언제 어디서든 호출할 수 있습니다.
// 올바른 예: 생성 중에 호출됨
export class MyComponent {
private readonly lazyService = injectAsync(() =>
...
실제 사용 사례
injectAsync()가 제 역할을 다하는 몇 가지 시나리오를 살펴보겠습니다.
사용 사례 1: 기능 제한형 관리자 도구 (Feature-Gated Admin Tools)
대부분의 사용자가 상호작용하지 않는 컴포넌트를 상상해 보세요. 예를 들어, 관리자에게만 보이는 대량 데이터 내보내기 패널이 있다고 가정해 봅시다. 이 내보내기 로직은 무거운 CSV 직렬화기 (serializer)와 커스텀 리포팅 엔진을 임포트합니다.
관리 패널을 전혀 방문하지 않는 95%의 사용자에게는 BulkExportService가 로드되지 않습니다.
유스케이스 2: 온디맨드 (On-Demand) 지도 통합
물류 대시보드는 기본적으로 배송 목록 테이블을 보여주며, 매핑 SDK (mapping SDK)를 로드하는 선택적인 지도 뷰를 제공합니다.
// shipment-tracker.component.ts
import { Component, injectAsync, signal } from '@angular/core';
...
매핑 SDK와 그와 관련된 번들(bundle) — 수백 킬로바이트(KB)에 달할 수 있음 — 은 사용자가 지도 뷰를 처음 요청할 때만 로드됩니다.
inject()와 injectAsync()의 직접 비교
차이점을 명확히 하기 위해 직접 비교해 보겠습니다:
| 측면 | inject() | injectAsync() |
|---|---|---|
| 해결 시점 (Resolution timing) | 컴포넌트 생성 시 즉시 (Eagerly) | 첫 호출 시 지연 (Lazily) |
| ... | ... | ... |
현재 Angular에서 기능 지연 로딩 (deferred feature loading)을 위해 어떤 패턴을 사용하고 계신가요? 아래에 댓글을 남겨주세요. 여러분이 지연 로딩된 라우트 (lazy-loaded routes), 동적 임포트 (dynamic imports), 또는 완전히 다른 방식을 사용하고 있는지 궁금합니다. 이것은 바로 논의할 가치가 있는 트레이드오프 (trade-off)의 종류입니다.
injectAsync()를 사용하는 컴포넌트의 단위 테스트 (Unit Testing)
injectAsync()를 테스트하려면 inject()를 테스트하는 것과는 약간 다른 접근 방식이 필요합니다. 핵심은 지연 팩토리 (lazy factory)가 Promise를 반환하므로, 테스트에서 비동기 해결 (async resolution)을 처리해야 한다는 점입니다.
먼저, 모킹 (mock)하려는 서비스를 살펴보겠습니다:
// bulk-export.service.ts
import { Service } from '@angular/core';
...
이제 AdminPanelComponent에 대한 단위 테스트를 작성하는 방법입니다:
// admin-panel.component.spec.ts
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { AdminPanelComponent } from './admin-panel.component';
...
이 테스트 접근 방식에 대해 몇 가지 유의할 점이 있습니다. injectAsync()는 생성 시점에 주입 컨텍스트 (injection context)를 캡처하고 Angular의 DI (의존성 주입) 시스템을 통해 해결(resolve)하기 때문에, TestBed.configureTestingModule의 프로바이더 (providers)가 그대로 적용됩니다. 동적 임포트 (dynamic import) 자체를 모킹 (mock)할 필요는 없으며, 테스트 모듈에서 해당 서비스를 제공하는 것만으로 충분합니다. 시나리오에 따라 fakeAsync와 async 유틸리티를 모두 사용할 수 있습니다.
injectAsync()와 새로운 @Service 데코레이터
Angular 22에는 @Service() 데코레이터도 포함되어 있으며, 이는 본질적으로 @Injectable({ providedIn: 'root' })의 축약형입니다. injectAsync()의 요구 사항 중 하나는 지연 주입 (lazily injected)되는 서비스가 @Service() 또는 @Injectable({ providedIn: 'root' })를 통해 자동으로 제공 (auto-provided)되어야 한다는 점입니다.
// analytics.service.ts
import { Service } from '@angular/core';
...
만약 컴포넌트 레벨이나 모듈 레벨에서만 제공되는 서비스에 injectAsync()를 사용하려고 하면 문제가 발생할 수 있습니다. 지연 주입을 위한 DI 해결 경로가 루트 인젝터 (root injector)에 의존하기 때문입니다. 지연 주입할 서비스를 설계할 때 이 제약 사항을 염두에 두시기 바랍니다.
Ninja Squad의 Angular 22 릴리스 커버리지에서 언급된 바와 같이, injectAsync()는 서비스가 @Service() 또는 @Injectable({ providedIn: 'root' })를 통해 자동으로 제공되어야 합니다.
번들 크기: 실질적인 이점
injectAsync()의 가장 구체적인 이점은 초기 번들 (initial bundle)에 미치는 영향입니다. 메서드 내부에서 일반적인 동적 import()를 사용하면, esbuild 및 webpack을 포함한 대부분의 번들러 (bundlers)가 해당 임포트된 모듈을 위한 별도의 청크 (chunk)를 생성합니다. injectAsync()는 바로 이 동일한 메커니즘에 직접 연결됩니다. 전달하는 팩토리 함수 (factory function)에 일반적으로 동적 임포트가 포함되어 있으며, 이는 번들러가 해당 코드를 지연 청크 (lazy chunk)로 분할하는 데 필요한 신호를 제공합니다.
실질적인 결과: 이전에는 서비스가 컴포넌트 상단에서 즉시 주입(eagerly injected)되었기 때문에 메인 번들(main bundle)의 일부였던 코드가, 이제는 해당 기능이 실제로 사용될 때만 다운로드되는 별도의 청크(chunk)로 분리됩니다.
PDF 엔진, 차트 라이브러리, 리치 텍스트 에디터(rich text editor)와 같이 대규모 서드파티 라이브러리를 사용하는 서비스의 경우, 초기 로드 시 수백 킬로바이트(KB)의 용량을 절감할 수 있습니다.
추가 팁 (Bonus Tips)
팁 1: 나중에 동기적 접근(synchronous access)이 필요하다면, 해결된 인스턴스를 직접 캐싱하세요.
injectAsync()는 이후의 호출에서도 항상 Promise를 반환합니다. 첫 번째 해결(resolution) 이후에 동기적 접근이 필요하다면, 결과를 시그널(signal)이나 클래스 속성에 저장하세요.
private resolvedService: HeavyService | null = null;
private readonly getHeavyService = injectAsync(() =>
import('./heavy.service').then(m => m.HeavyService)
...
팁 2: 최대 효율을 위해 OnPush 및 시그널(Signals)과 결합하세요.
Angular 22에서는 OnPush가 기본값이 되었으므로, 컴포넌트는 시그널 입력(signal inputs)이 변경되거나 명시적으로 변경 감지(change detection)를 트리거할 때만 다시 렌더링됩니다. injectAsync()를 시그널과 함께 사용하여 로딩 상태를 전달하면, 컴포넌트는 실제로 필요할 때만 다시 그립니다(repaint).
팁 3: 과도한 지연 로딩(lazy-loading)을 피하세요.
injectAsync()는 강력하지만 모든 문제에 적용할 수 있는 망치는 아닙니다. 라우팅(routing), 인증(auth), 핵심 데이터(core data)와 같이 거의 항상 필요한 서비스는 일반적인 inject() 호출로 유지해야 합니다. Promise 해결 및 네트워크 요청(지연 청크를 위한)에 따른 오버헤드는 서비스가 진정으로 조건부이거나 빈번하게 사용되지 않을 때만 가치가 있습니다.
팁 4: TypeScript의 타입 추론(type inference)을 활용하세요.
injectAsync()는 타입이 완전히 지정되어 있습니다. 반환되는 함수의 반환 타입은 팩토리의 Promise 타입으로부터 추론되므로, 별도의 타입 캐스팅(casting) 없이도 해결된 서비스에 대해 완전한 자동 완성 기능을 사용할 수 있습니다.
팁 5: 업그레이드하기 전에 Angular 22 마이그레이션 가이드를 확인하세요.
Angular 22는 OnPush를 기본값으로 도입합니다. 이는 명시적인 변경 감지 전략 (change detection strategy)이 없는 기존 컴포넌트들이 업그레이드 후에 다르게 동작할 수 있음을 의미합니다. ng update 명령은 전략을 지정하지 않은 컴포넌트에 changeDetection: ChangeDetectionStrategy.Eager를 자동으로 추가하여, 업그레이드 과정 동안 이전의 동작을 유지해 줍니다. 배포하기 전에 마이그레이션을 실행하세요.
요약 (Recap)
지금까지의 내용을 정리해 보겠습니다. Angular 22의 injectAsync()는 서비스 해결 (service resolution)을 실제로 필요한 시점까지 지연시킬 수 있는, DI (Dependency Injection)를 인식하는 일급 객체 방식의 방법을 제공합니다. 지금까지 다룬 내용은 다음과 같습니다:
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기