Angular의 고급 종속성 주입
La 의존성 주입(DI) 이는 Angular의 아키텍처 핵심입니다. 버전 2부터,
프레임워크는 강력하고 정교한 DI 시스템을 제공했지만 Angular 14+와
독립형 구성 요소로 인해 시스템이 급격하게 발전했습니다. 다음과 같은 새로운 API inject(),
트리 셰이크 가능한 공급자, 가드 및 기능적 인터셉터는 우리가 관리하는 방식을 변화시켰습니다.
최신 Angular 애플리케이션의 종속성.
이 시리즈의 아홉 번째 기사에서는 현대적인 각도, 의존성 주입을 살펴보겠습니다.
고급: 새로운 기능에서 inject() 계층 구조에서 사용자 정의 토큰까지
다중 공급자 패턴에 대한 인젝터, 테스트 및 구성 요소의 동적 생성까지
주입 컨텍스트. 이미 DI의 기본 사항을 마스터했다면 이 기사를 통해 최신 정보를 얻을 수 있습니다.
다음.
무엇을 배울 것인가
- 기능
inject()제조사를 통한 주입을 대체하기 때문에 - 생성 및 사용 방법
InjectionToken전형적인 - 인젝터의 전체 계층 구조: 루트, 플랫폼, 요소, 구성 요소
- 옵션
providedIn그리고 번들에 미치는 영향 - 패턴
MULTI인터셉터, 가드 및 여러 공급자용 - 트리 셰이크 가능 제공자 및 번들 크기 최적화
- 기능성 가드 및 인터셉터
CanActivateFnewithInterceptors() EnvironmentInjector구성 요소의 동적 생성runInInjectionContext유틸리티 기능의 경우inject()- DI 테스트
TestBedeoverrideProvider
최신 Angular 시리즈 개요
| # | Articolo | 집중하다 |
|---|---|---|
| 1 | 각도 신호 | 세분화된 응답성 |
| 2 | 영역 없는 변경 감지 | Zone.js 삭제 |
| 3 | 새로운 템플릿 @if, @for, @defer | 현대적인 제어 흐름 |
| 4 | 독립형 구성 요소 | NgModule이 없는 아키텍처 |
| 5 | 신호 형태 | 신호가 포함된 반응형 양식 |
| 6 | SSR 및 증분 수화 | 서버 측 렌더링 |
| 7 | Angular의 핵심 웹 바이탈 | 성능 및 지표 |
| 8 | 각도 PWA | 프로그레시브 웹 앱 |
| 9 | 현재 위치 - 고급 종속성 주입 | DI 트리 셰이크 가능 |
| 10 | Angular 17에서 21로 마이그레이션 | 마이그레이션 가이드 |
1. inject() 함수: 안녕 생성자
기능 inject()Angular 14에서 안정적인 API로 도입된 은
현대 DI의 가장 중요한 변화. 종속성을 매개변수로 선언하는 대신
생성자의 경우 어디에서나 호출할 수 있는 함수를 사용합니다.
주입 컨텍스트 active: 생성자, 필드 이니셜라이저,
공장 기능과 유틸리티 기능에서.
// PRIMA: Iniezione via costruttore (approccio tradizionale)
@Component({
selector: 'app-user-list',
standalone: true,
template: '...'
})
export class UserListComponent {
constructor(
private userService: UserService,
private router: Router,
private route: ActivatedRoute,
@Inject(API_URL) private apiUrl: string,
@Optional() private logger?: LoggerService
) {}
}
// DOPO: Funzione inject() (approccio moderno)
@Component({
selector: 'app-user-list',
standalone: true,
template: '...'
})
export class UserListComponent {
private userService = inject(UserService);
private router = inject(Router);
private route = inject(ActivatedRoute);
private apiUrl = inject(API_URL);
private logger = inject(LoggerService, { optional: true });
// I campi sono già disponibili, non serve il costruttore
users = this.userService.getUsers();
}
생성자에 비해 inject()의 장점
| 나는 기다린다 | 빌더 | 주입() |
|---|---|---|
| 가독성 | 매개변수가 길어 관리가 어려움 | 명확하고 간결한 인라인 문 |
| 유전 | 필요하다 super() 모든 매개변수 포함 |
문제 없습니다. 캠프는 자율적입니다. |
| 재사용 | Angular 클래스 생성자에서만 | 기능, 공장, 유틸리티에서 사용할 수 있습니다. |
| 유형 안전 | 데코레이터로 완성 | 완전한 자동 유형 추론 |
| 흔들리는 나무 | 데코레이터 메타데이터로 제한됨 | 클래스에 대한 최적의 직접 참조 |
| 선택사항/자체/SkipSelf | 데코레이터: @Optional(), @Self() |
항목 옵션: { optional: true, self: true } |
inject()를 사용할 수 있는 곳
기능 inject() 하나만 작동합니다. 주입 컨텍스트.
이 컨텍스트 외부에서 호출하면 런타임 오류가 발생합니다.
NG0203: inject() must be called from an injection context.
- 허용된: 구성요소/서비스/지시문/파이프 생성자, 필드 이니셜라이저, 공급자 팩토리 함수, 다음에서 실행되는 함수
runInInjectionContext() - 허용되지 않음: 초기화 후 호출되는 구성요소 메소드, 이벤트 콜백, setTimeout/setInterval, 컨텍스트가 없는 독립형 함수
특히 강력한 패턴 중 하나는 inject() 창조하다
재사용 가능한 유틸리티 함수 이는 주입 논리를 캡슐화합니다.
// utils/navigation.ts
export function injectRouteParams() {
const route = inject(ActivatedRoute);
return toSignal(
route.paramMap.pipe(
map(params => ({
id: params.get('id'),
slug: params.get('slug')
}))
)
);
}
export function injectQueryParams() {
const route = inject(ActivatedRoute);
return toSignal(route.queryParamMap);
}
// Uso nel componente
@Component({ ... })
export class ProductDetailComponent {
params = injectRouteParams();
// params() restituisce { id: '123', slug: 'my-product' }
}
2. 주입토큰(InjectionToken): 클래스가 아닌 값에 대한 형식화된 토큰
종속성이 클래스가 아닌 기본 값(문자열, 숫자, 객체)인 경우
구성), Angular는 클래스 자체를 조회 키로 사용할 수 없습니다.
InjectionToken 고유한 유형의 토큰을 생성하여 이 문제를 해결합니다.
DI 레지스터에서 키 역할을 합니다.
import { InjectionToken, inject } from '@angular/core';
// Token con valore statico
export const API_BASE_URL = new InjectionToken<string>(
'API_BASE_URL'
);
// Token con factory function (tree-shakable!)
export const IS_MOBILE = new InjectionToken<boolean>(
'IS_MOBILE',
{
providedIn: 'root',
factory: () => {
const platform = inject(PLATFORM_ID);
if (isPlatformBrowser(platform)) {
return window.innerWidth < 768;
}
return false;
}
}
);
// Token per configurazione complessa
export interface AppConfig {
apiUrl: string;
production: boolean;
features: {
darkMode: boolean;
analytics: boolean;
};
}
export const APP_CONFIG = new InjectionToken<AppConfig>(
'APP_CONFIG',
{
providedIn: 'root',
factory: () => ({
apiUrl: 'https://api.example.com',
production: false,
features: {
darkMode: true,
analytics: true
}
})
}
);
// Uso nei componenti
@Component({ ... })
export class AppComponent {
private config = inject(APP_CONFIG);
private isMobile = inject(IS_MOBILE);
}
InjectionToken을 사용해야 하는 경우
- 구성 값: API URL, 기능 플래그, 시간 초과, 제한
- 브라우저 API:
window,document,localStorage(SSR 호환성을 위해) - 상호 교환 가능한 구현: 로깅 전략, 스토리지 어댑터, 인증 공급자
- 동적 팩토리: 다른 서비스에 의존하는 런타임 시 계산되는 값
3. 인젝터 계층 구조
Angular에서 DI의 가장 강력하고 복잡한 측면 중 하나는 계층적 구조. 단일 종속성 컨테이너가 없습니다. Angular는 애플리케이션의 구조를 반영하는 인젝터 트리를 유지 관리합니다. 구성 요소에 종속성이 필요한 경우 Angular는 이를 찾을 때까지 트리 위로 이동합니다.
Angular의 인젝터 계층 구조
+---------------------------------------------+
| Platform Injector |
| (condiviso tra applicazioni Angular) |
| Provider: PLATFORM_ID, PLATFORM_INITIALIZER|
+----------------------+----------------------+
|
+----------------------v----------------------+
| Root Injector |
| (providedIn: 'root', app.config.ts) |
| Provider: HttpClient, Router, servizi root |
+----------------------+----------------------+
|
+---------------+---------------+
| |
+------v------+ +-------v-----+
| Module | | Module |
| Injector | | Injector |
| (lazy mod.) | | (lazy mod.) |
+------+------+ +------+------+
| |
+------v------+ +-------v-----+
| Element | | Element |
| Injector | | Injector |
| (component) | | (component) |
+------+------+ +------+------+
| |
+------v------+ +-------v-----+
| Element | | Element |
| Injector | | Injector |
| (child) | | (child) |
+-------------+ +-------------+
계층 구조 수준
| 수준 | 빗자루 | 창조 | 일반적인 사용 |
|---|---|---|---|
| 플랫폼 | 페이지의 모든 Angular 앱 | platformBrowserDynamic() |
마이크로 프런트엔드 간 공유 서비스 |
| 뿌리 | 전체 애플리케이션(싱글톤) | providedIn: 'root' o app.config.ts |
HttpClient, 라우터, 글로벌 서비스 |
| 기준 치수 | 지연 로드 모듈 | 배열 providers 형태의 |
기능별 서비스 |
| 요소 | 회원과 그 자녀 | 배열 providers 구성 요소의 |
로컬 상태, 다중 인스턴스 |
종속성 해결은 정확한 경로를 따릅니다. Angular는 인젝터에서 시작됩니다.
종속성을 요구하고 공급자를 찾을 때까지 트리 위로 올라가는 구성 요소
해당. 공급자를 찾을 수 없으면 오류가 생성됩니다.
NullInjectorError (종속성이 선택사항으로 표시되지 않은 경우)
// Un servizio che gestisce lo stato locale
@Injectable()
export class FormStateService {
private fields = signal<Map<string, any>>(new Map());
private dirty = signal(false);
setValue(key: string, value: any) {
this.fields.update(m => {
const newMap = new Map(m);
newMap.set(key, value);
return newMap;
});
this.dirty.set(true);
}
isDirty = this.dirty.asReadonly();
}
// Ogni istanza del componente riceve il proprio FormStateService
@Component({
selector: 'app-editable-card',
standalone: true,
providers: [FormStateService], // Istanza LOCALE
template: `
<div class="card" [class.dirty]="formState.isDirty()">
<ng-content />
</div>
`
})
export class EditableCardComponent {
protected formState = inject(FormStateService);
}
4. 제공되는 옵션
데코레이터 @Injectable() 옵션을 수락 providedIn 그
서비스가 자동으로 등록되는 인젝터를 결정합니다. 이 옵션
그것은 기본이다 흔들리는 나무: Angular는 비서비스를 삭제할 수 있습니다.
최종 번들에서 사용됩니다.
제공됨 옵션 사용 가능
| Valore | 빗자루 | 나무 흔들림 가능 | 사용 사례 |
|---|---|---|---|
'root' |
전체 애플리케이션에 대한 싱글톤 | Si | 글로벌 서비스(인증, API, 상태) |
'platform' |
Angular 애플리케이션 간에 공유됨 | Si | 마이크로 프론트엔드, 요소 |
'any' |
지연 로드 모듈당 인스턴스 1개 | Si | 게으른 기능을 위한 격리 상태 |
| 요소 | 구성요소별 인스턴스 | No | 로컬 상태, 양식, 대화 상자 |
// Singleton globale - l'approccio più comune
@Injectable({ providedIn: 'root' })
export class AuthService {
private currentUser = signal<User | null>(null);
isAuthenticated = computed(() => this.currentUser() !== null);
}
// Condiviso tra app Angular sulla stessa pagina
@Injectable({ providedIn: 'platform' })
export class SharedThemeService {
theme = signal<'light' | 'dark'>('dark');
}
// Un'istanza per ogni modulo lazy-loaded
@Injectable({ providedIn: 'any' })
export class FeatureAnalyticsService {
private events: AnalyticsEvent[] = [];
// Ogni feature lazy ha il proprio tracker indipendente
}
// Istanza per componente (non tree-shakable)
@Injectable() // Nessun providedIn
export class DialogStateService {
isOpen = signal(false);
data = signal<any>(null);
}
// Va registrato nell'array providers del componente:
@Component({
providers: [DialogStateService]
})
export class MyDialogComponent {
state = inject(DialogStateService);
}
ProvideIn: 'any'에 주의하세요.
옵션 'any' 지연 로드된 각 모듈에 대해 별도의 인스턴스를 만듭니다.
싱글톤이 필요한 경우 이로 인해 예기치 않은 동작이 발생할 수 있습니다. 그냥 사용하세요
기능 모듈 간의 상태를 명시적으로 분리하려는 경우. 건축과 함께
독립형 및 게으른 경로, 'any' 거의 필요하지 않습니다.
당신은 선호 'root' 싱글톤이나 나에게 providers 구성 요소의
지역 국가를 위해.
5. 다중 제공자: MULTI 패턴
패턴 다중 공급자 여러 값을 동일한 값에 연결할 수 있습니다. 주입 토큰. 토큰을 요청할 때 Angular는 정렬 모든 값이 기록되어 있습니다. 이 패턴은 다음과 같은 중요한 기능의 기초가 됩니다. HTTP 인터셉터, 라우팅 가드 및 애플리케이션 이니셜라이저.
import { InjectionToken, inject } from '@angular/core';
// Token multi-provider per validatori custom
export const FORM_VALIDATORS = new InjectionToken<FormValidator[]>(
'FORM_VALIDATORS'
);
export interface FormValidator {
name: string;
validate(value: any): string | null;
}
// Registrazione di più provider con multi: true
export const appConfig: ApplicationConfig = {
providers: [
{
provide: FORM_VALIDATORS,
useValue: {
name: 'required',
validate: (v: any) => v ? null : 'Campo obbligatorio'
},
multi: true
},
{
provide: FORM_VALIDATORS,
useValue: {
name: 'minLength',
validate: (v: string) =>
v?.length >= 3 ? null : 'Minimo 3 caratteri'
},
multi: true
},
{
provide: FORM_VALIDATORS,
useFactory: () => {
const http = inject(HttpClient);
return {
name: 'uniqueEmail',
validate: async (v: string) => {
// Validazione asincrona via API
return null;
}
};
},
multi: true
}
]
};
// Consumo nel servizio
@Injectable({ providedIn: 'root' })
export class ValidationService {
private validators = inject(FORM_VALIDATORS);
validateAll(value: any): string[] {
return this.validators
.map(v => v.validate(value))
.filter((err): err is string => err !== null);
}
}
Angular에 통합된 다중 공급자
HTTP_INTERCEPTORS- HTTP 인터셉터(레거시 클래스 기반 접근 방식)APP_INITIALIZER- 앱 부트스트랩 전에 수행되는 기능ENVIRONMENT_INITIALIZER- 환경 주입기용 초기화 프로그램NG_VALIDATORS/NG_ASYNC_VALIDATORS- 양식 유효성 검사기ROUTES- 동적 경로 구성
6. Tree-Shakable 공급자
Angular에서 최신 DI의 주요 이점 중 하나는
흔들리는 나무 공급자의. 서비스가 등록되면
providedIn: 'root' 데코레이터의 팩토리인 번들러는
가져오는 구성 요소가 없으면 최종 빌드에서 완전히 삭제하세요.
// TREE-SHAKABLE: il servizio si auto-registra
// Se nessun componente/servizio lo importa, viene eliminato dal bundle
@Injectable({ providedIn: 'root' })
export class AnalyticsService {
track(event: string) {
// Se AnalyticsService non e importato da nessuna parte,
// tutto questo codice viene rimosso dal bundle
console.log('Track:', event);
}
}
// TREE-SHAKABLE con factory: dipendenze calcolate
export const FEATURE_FLAGS = new InjectionToken<FeatureFlags>(
'FEATURE_FLAGS',
{
providedIn: 'root',
factory: () => {
const http = inject(HttpClient);
// La factory viene eliminata se FEATURE_FLAGS
// non viene mai iniettato
return { darkMode: true, newDashboard: false };
}
}
);
// NON TREE-SHAKABLE: registrato nell'array providers
// Sempre incluso nel bundle, anche se non usato
export const appConfig: ApplicationConfig = {
providers: [
LegacyService, // Sempre nel bundle!
{ provide: LoggerService, useClass: ConsoleLoggerService }
]
};
번들에 대한 트리 쉐이킹의 영향
| 접근하다 | 등록 | 나무 흔들림 | 사용하지 않을 경우 번들에 포함 |
|---|---|---|---|
providedIn: 'root' |
서비스 데코레이터에서 | Si | 아니요(삭제됨) |
InjectionToken 공장과 함께 |
토큰 생성자에서 | Si | 아니요(삭제됨) |
배열 providers |
app.config.ts 또는 구성 요소에서 | No | 예(항상 존재함) |
NgModule providers |
형태로 | No | 예(항상 존재함) |
트리 쉐이킹 모범 사례
- 항상 사용
providedIn: 'root'글로벌 서비스를 위한 - 공급자 배열이 아닌 토큰 자체에 팩토리를 정의합니다.
- 배열에 서비스를 등록하지 마세요.
providersdiapp.config.ts필요하지 않은 경우(재정의, 다중 공급자) - 다음을 사용하여 번들 크기를 모니터링합니다.
ng build --stats-jsonewebpack-bundle-analyzer
7. 기능적 가드와 인터셉터
Angular 15부터 가드와 인터셉터는 인터페이스가 있는 클래스에서 진화했습니다.
에 간단한 기능. 이 기능적 접근 방식은 상용구를 줄이고
구성성을 향상하고 기본적으로 기능을 활용합니다. inject().
7.1 기능 가드
// PRIMA: Guard come classe (deprecato)
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(
private auth: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean | UrlTree {
if (this.auth.isAuthenticated()) {
return true;
}
return this.router.createUrlTree(['/login']);
}
}
// DOPO: Guard come funzione
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isAuthenticated()) {
return true;
}
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
};
// Guard con parametri: factory function
export function roleGuard(requiredRole: string): CanActivateFn {
return (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.hasRole(requiredRole)) {
return true;
}
return router.createUrlTree(['/forbidden']);
};
}
// Configurazione delle rotte
export const routes: Routes = [
{
path: 'dashboard',
canActivate: [authGuard],
loadComponent: () => import('./dashboard/dashboard.component')
.then(m => m.DashboardComponent)
},
{
path: 'admin',
canActivate: [authGuard, roleGuard('admin')],
loadComponent: () => import('./admin/admin.component')
.then(m => m.AdminComponent)
}
];
7.2 기능적 인터셉터
import { HttpInterceptorFn } from '@angular/common/http';
// Interceptor per autenticazione
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const token = auth.getToken();
if (token) {
const cloned = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next(cloned);
}
return next(req);
};
// Interceptor per logging
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
const startTime = Date.now();
return next(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
const duration = Date.now() - startTime;
console.log(`${req.method} ${req.url} - ${duration}ms`);
}
})
);
};
// Interceptor per retry con backoff
export const retryInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
retry({
count: 3,
delay: (error, retryCount) => {
if (error.status === 429 || error.status >= 500) {
return timer(Math.pow(2, retryCount) * 1000);
}
throw error;
}
})
);
};
// Registrazione in app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([
authInterceptor,
loggingInterceptor,
retryInterceptor
])
)
]
};
클래스 기반 및 기능적 인터셉터
| 나는 기다린다 | 클래스 기반(레거시) | 기능적(현대적) |
|---|---|---|
| 통사론 | 클래스 + 인터페이스 HttpInterceptor |
간단한 기능 HttpInterceptorFn |
| 등록 | HTTP_INTERCEPTORS 다중 공급자 |
withInterceptors([]) |
| DI | 빌더 | inject() |
| 구성 | 매개변수화하기 어려움 | 매개변수에 대한 팩토리 함수 |
| 흔들리는 나무 | 제한된 | 최적 |
8. EnvironmentInjector 및 동적 구성 요소
L'EnvironmentInjector Angular 14에 도입된 인젝터 유형입니다.
수준이 아닌 "환경" 수준(루트, 경로, 게으른 모듈)에서 작동합니다.
DOM 요소. 이는 동적으로 구성요소를 생성할 때 중요합니다.
createComponent() o ViewContainerRef, 이는 다음을 허용하므로
올바른 주입 컨텍스트에 액세스하기 위해 구성요소를 생성했습니다.
import {
Component,
EnvironmentInjector,
createEnvironmentInjector,
createComponent,
ViewContainerRef,
inject
} from '@angular/core';
// Servizio per creare componenti con un contesto DI personalizzato
@Injectable({ providedIn: 'root' })
export class DynamicComponentService {
private parentInjector = inject(EnvironmentInjector);
createIsolatedComponent<T>(
component: Type<T>,
vcr: ViewContainerRef,
providers: StaticProvider[] = []
) {
// Crea un nuovo EnvironmentInjector con provider aggiuntivi
const childInjector = createEnvironmentInjector(
[
{ provide: DIALOG_DATA, useValue: { title: 'My Dialog' } },
...providers
],
this.parentInjector
);
// Crea il componente nel nuovo contesto
const componentRef = createComponent(component, {
environmentInjector: childInjector,
hostElement: document.createElement('div')
});
// Inserisci nella view
vcr.insert(componentRef.hostView);
// Importante: distruggi l'injector quando il componente viene distrutto
componentRef.onDestroy(() => childInjector.destroy());
return componentRef;
}
}
// Uso in un componente host
@Component({
selector: 'app-dialog-host',
standalone: true,
template: `<ng-container #outlet />`
})
export class DialogHostComponent {
private dynamicService = inject(DynamicComponentService);
private vcr = inject(ViewContainerRef);
openDialog(dialogComponent: Type<any>, data: any) {
this.dynamicService.createIsolatedComponent(
dialogComponent,
this.vcr,
[{ provide: DIALOG_DATA, useValue: data }]
);
}
}
EnvironmentInjector를 사용해야 하는 경우
- 동적 대화상자/모달: 데이터 주입이 필요한 런타임 시 생성된 구성 요소
- 플러그인 시스템: 격리된 공급자를 사용한 구성 요소의 동적 로드
- 마이크로 프론트엔드: 각 마이크로앱에는 자체 주입 컨텍스트가 있습니다.
- 고급 테스트: 테스트를 위해 격리된 주입 컨텍스트 생성
9. runInInjectionContext: 어디서나 주입
기능 runInInjectionContext(), Angular 16에 도입된 기능을 사용하면 다음을 수행할 수 있습니다.
주입 컨텍스트 내에서 임의의 코드를 실행합니다. 이걸로 하나 해결됨
한계의 inject(): 동안에만 그녀에게 전화할 수 있는 기능
건설 단계. 와 함께 runInInjectionContext(), 모든 기능
종속성에 액세스할 수 있습니다.
import {
runInInjectionContext,
EnvironmentInjector,
inject,
DestroyRef
} from '@angular/core';
// Funzione utility che richiede inject()
function createAutoSaveEffect(formId: string) {
const http = inject(HttpClient);
const destroyRef = inject(DestroyRef);
const intervalId = setInterval(() => {
const formData = getFormData(formId);
http.post('/api/autosave', formData).subscribe();
}, 30000);
// Cleanup automatico quando il contesto viene distrutto
destroyRef.onDestroy(() => clearInterval(intervalId));
}
// Uso nel componente: funziona nel costruttore (contesto attivo)
@Component({ ... })
export class EditorComponent {
constructor() {
createAutoSaveEffect('editor-form');
}
}
// Uso al di fuori del costruttore: runInInjectionContext
@Component({ ... })
export class DashboardComponent {
private envInjector = inject(EnvironmentInjector);
onTabSelected(tabId: string) {
// Questo metodo viene chiamato da un click handler,
// quindi inject() non funzionerebbe direttamente
runInInjectionContext(this.envInjector, () => {
createAutoSaveEffect(tabId);
});
}
}
// Pattern avanzato: creare un "service locator" basato su funzioni
export function createServiceAccessor<T>(token: ProviderToken<T>) {
let cachedInstance: T | undefined;
return {
init: () => {
cachedInstance = inject(token);
},
get: () => {
if (!cachedInstance) {
throw new Error('Service not initialized. Call init() first.');
}
return cachedInstance;
}
};
}
runInInjectionContext 사용에 주의하세요.
강력하긴 하지만, runInInjectionContext() 과도하게 사용해서는 안 됩니다.
주입된 코드를 외부에서 실행해야 하는 구체적인 이유가 있을 때 사용하세요.
건설 단계의. 자주 사용하신다면 아마도
부품 디자인을 개선할 수 있습니다. 또한 인젝터가 통과했다는 것을 기억하십시오
이어야 합니다 EnvironmentInjector (아님 ElementInjector),
따라서 구성 요소 수준에서 등록된 공급자에는 액세스할 수 없습니다.
10. 의존성 주입 테스트
DI의 가장 큰 장점 중 하나는 테스트 가능성입니다. Angular는 다음을 위한 강력한 도구를 제공합니다.
단위 테스트에서 종속성을 구성, 교체 및 확인합니다. 접근 방식으로
현대를 기반으로 한 inject() 독립형 구성 요소, 테스트 설정
훨씬 쉬워졌습니다.
import { TestBed } from '@angular/core/testing';
describe('UserService', () => {
let service: UserService;
let httpMock: jest.Mocked<HttpClient>;
beforeEach(() => {
// Mock dell'HttpClient
const mockHttp = {
get: jest.fn(),
post: jest.fn()
};
TestBed.configureTestingModule({
providers: [
UserService,
{ provide: HttpClient, useValue: mockHttp },
{ provide: API_BASE_URL, useValue: 'http://test-api.com' }
]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpClient) as jest.Mocked<HttpClient>;
});
it('should fetch users from the API', () => {
const mockUsers = [{ id: 1, name: 'Alice' }];
httpMock.get.mockReturnValue(of(mockUsers));
service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
expect(httpMock.get).toHaveBeenCalledWith(
'http://test-api.com/users'
);
});
});
});
// Test di un componente standalone con override
describe('DashboardComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DashboardComponent],
})
.overrideComponent(DashboardComponent, {
set: {
providers: [
{
provide: AnalyticsService,
useValue: { track: jest.fn() }
}
]
}
})
.compileComponents();
});
it('should track page view on init', () => {
const fixture = TestBed.createComponent(DashboardComponent);
const analytics = fixture.debugElement.injector.get(AnalyticsService);
fixture.detectChanges();
expect(analytics.track).toHaveBeenCalledWith('dashboard_view');
});
});
// Test di guard funzionali
describe('authGuard', () => {
it('should redirect to login when not authenticated', () => {
TestBed.configureTestingModule({
providers: [
{
provide: AuthService,
useValue: { isAuthenticated: () => false }
},
provideRouter([])
]
});
const injector = TestBed.inject(EnvironmentInjector);
// Esegui il guard nel contesto di injection
const result = runInInjectionContext(injector, () =>
authGuard(
{} as ActivatedRouteSnapshot,
{ url: '/dashboard' } as RouterStateSnapshot
)
);
expect(result).toBeInstanceOf(UrlTree);
});
it('should allow access when authenticated', () => {
TestBed.configureTestingModule({
providers: [
{
provide: AuthService,
useValue: { isAuthenticated: () => true }
},
provideRouter([])
]
});
const injector = TestBed.inject(EnvironmentInjector);
const result = runInInjectionContext(injector, () =>
authGuard(
{} as ActivatedRouteSnapshot,
{ url: '/dashboard' } as RouterStateSnapshot
)
);
expect(result).toBe(true);
});
});
DI 테스트 패턴
| 대본 | 방법 | Esempio |
|---|---|---|
| 서비스 교체 | provide + useValue |
{ provide: AuthService, useValue: mockAuth } |
| 구성 요소 수준 재정의 | overrideComponent |
지역 부품 공급업체 |
| 테스트 주입 토큰 | provide + useValue |
{ provide: API_URL, useValue: 'http://test' } |
| 기능 가드 테스트 | runInInjectionContext |
TestBed 컨텍스트에서 가드 실행 |
| 실제 방법에 대한 스파이 | inject + spyOn |
spyOn(TestBed.inject(Svc), 'method') |
DI 테스트 모범 사례
- 수동 모의를 선호합니다 a
jasmine.createSpyObj유형에 대한 더 나은 제어를 위해 - 격리된 테스트 서비스: 모든 종속성을 모의하고 서비스 로직만 테스트하세요.
- 미국
TestBed.inject()테스트 모듈에 등록된 서비스에 접근하기 위해 - 기능적 가드 및 인터셉터의 경우:
runInInjectionContext()TestBed 인젝터를 사용하여 - 오류를 확인하세요. 그 머리로
NullInjectorError필수 공급자가 누락된 경우 생성됩니다.
요약 및 다음 단계
이 기사에서는 Angular의 고급 종속성 주입을 살펴보았습니다.
새로운 API에서 inject() 테스트까지. Angular e의 최신 DI
단순한 객체 생성 메커니즘 그 이상입니다. 정교한 시스템입니다.
이는 애플리케이션의 아키텍처, 성능 및 유지 관리 가능성에 영향을 미칩니다.
주요 개념
- 기능
inject()생성자 주입을 대체하여 더 나은 가독성, 재사용 및 트리 쉐이킹을 제공합니다. InjectionToken강력한 타이핑과 팩토리 트리 셰이크 기능을 통해 클래스가 아닌 값을 주입할 수 있습니다.- 인젝터의 계층 구조(플랫폼, 루트, 모듈, 요소)에 따라 종속성의 범위와 수명이 결정됩니다.
providedIn: 'root'트리 셰이크 가능한 싱글톤 서비스에 선호되는 옵션- 패턴
multi: true이는 인터셉터, 가드 및 초기화 프로그램의 기초입니다. - 기능적 가드(
CanActivateFn) 및 기능적 인터셉터(HttpInterceptorFn) 상용구를 줄이고 퇴비화 가능성을 향상시킵니다. EnvironmentInjectorecreateEnvironmentInjector()격리된 DI 컨텍스트를 사용하여 구성 요소를 동적으로 생성할 수 있습니다.runInInjectionContext()당신이 사용할 수 있습니다inject()건설 단계 밖에서- DI 테스트 익스플로잇
TestBed,overrideComponenterunInInjectionContext완전한 모의 및 검증을 위해
시리즈의 다음 기사이자 마지막 기사에서는 Angular 17에서 21로 마이그레이션, 이 시리즈에서 얻은 모든 지식을 마이그레이션에 적용하는 방법을 살펴보겠습니다. 단계별 가이드와 함께 Angular의 최신 API에 대한 기존 애플리케이션 전환의 모든 측면을 다루고 있습니다.







