# 2026년 Angular 의존성 주입 완벽 가이드: 프로바이더, 토큰, 면접 대비 > Angular의 고급 의존성 주입(DI) 시스템을 심층 분석합니다. InjectionToken, 계층형 인젝터, 프로바이더 전략, 그리고 기술 면접에서 자주 출제되는 질문과 모범 답변을 상세히 다룹니다. - Published: 2026-09-13 - Updated: 2026-09-13 - Author: Anthony Fillion-Maillet - Tags: angular, dependency-injection, typescript, interview - Reading time: 12 min --- Angular의 의존성 주입(Dependency Injection, 이하 DI) 시스템은 프레임워크의 핵심 기능입니다. 2026년 현재 Angular 19에서는 `inject` 함수와 시그널 기반 상태 관리와의 통합을 통해 DI 활용 방식이 더욱 발전했습니다. 본 글에서는 시니어 개발자가 반드시 알아야 할 DI의 고급 패턴과 기술 면접에서 자주 출제되는 질문에 대해 상세히 다룹니다. > 의존성 주입의 기본 개념을 이해하고 있다는 전제하에 작성되었습니다. 입문자는 먼저 공식 문서의 기초 가이드를 확인하시기 바랍니다. ## InjectionToken을 활용한 타입 안전 의존성 관리 문자열 기반 토큰은 이름 충돌 위험이 있고 타입 안전성도 보장되지 않습니다. `InjectionToken`을 사용하면 이러한 문제를 해결할 수 있습니다. ```typescript import { InjectionToken, inject } from '@angular/core'; export interface AppConfig { apiUrl: string; featureFlags: { darkMode: boolean; betaFeatures: boolean; }; } export const APP_CONFIG = new InjectionToken('app.config', { providedIn: 'root', factory: () => ({ apiUrl: 'https://api.example.com', featureFlags: { darkMode: true, betaFeatures: false, }, }), }); // 컴포넌트에서의 사용 export class FeatureComponent { private config = inject(APP_CONFIG); get apiEndpoint(): string { return this.config.apiUrl; } } ``` `factory` 함수를 지정하면 토큰에 기본값을 설정할 수 있습니다. 이를 통해 명시적인 프로바이더 설정이 없어도 서비스를 안전하게 사용할 수 있습니다. ## 계층형 인젝터의 동작 원리 Angular의 DI 시스템은 계층 구조를 가지며 여러 인젝터가 트리 구조로 연결됩니다. 이 메커니즘을 이해하는 것은 대규모 애플리케이션 설계에서 매우 중요합니다. ```typescript import { Injectable, inject } from '@angular/core'; @Injectable() export class LoggerService { private prefix = 'DEFAULT'; setPrefix(prefix: string): void { this.prefix = prefix; } log(message: string): void { console.log(`[${this.prefix}] ${message}`); } } // 부모 컴포넌트 @Component({ selector: 'app-parent', providers: [LoggerService], template: ``, }) export class ParentComponent { private logger = inject(LoggerService); constructor() { this.logger.setPrefix('PARENT'); } } // 자식 컴포넌트 - 부모의 인스턴스를 상속 @Component({ selector: 'app-child', template: ``, }) export class ChildComponent { private logger = inject(LoggerService); logMessage(): void { this.logger.log('Message from child'); // [PARENT] Message from child } } ``` 자식 컴포넌트에 별도의 `providers`를 설정하면 새로운 인스턴스가 생성되어 부모와 다른 스코프에서 동작합니다. ## 프로바이더 전략의 올바른 선택 Angular에서는 여러 가지 프로바이더 설정 방법을 제공하며 각각 다른 사용 사례에 적합합니다. ### useClass를 통한 구현체 교체 ```typescript import { Injectable } from '@angular/core'; export abstract class NotificationService { abstract send(message: string): void; } @Injectable() export class EmailNotificationService extends NotificationService { send(message: string): void { console.log(`Email: ${message}`); } } @Injectable() export class PushNotificationService extends NotificationService { send(message: string): void { console.log(`Push: ${message}`); } } // 모듈에서의 설정 @NgModule({ providers: [ { provide: NotificationService, useClass: environment.mobile ? PushNotificationService : EmailNotificationService, }, ], }) export class AppModule {} ``` ### useFactory를 통한 동적 생성 ```typescript import { InjectionToken, inject } from '@angular/core'; export const API_CLIENT = new InjectionToken('api.client', { providedIn: 'root', factory: () => { const config = inject(APP_CONFIG); const http = inject(HttpClient); return new ApiClient(http, { baseUrl: config.apiUrl, timeout: 30000, retryAttempts: 3, }); }, }); ``` ### useExisting을 통한 별칭 지정 ```typescript @NgModule({ providers: [ LoggerService, { provide: 'LEGACY_LOGGER', useExisting: LoggerService, }, ], }) export class CoreModule {} ``` 이 설정을 통해 신규 토큰과 레거시 토큰 모두에서 동일한 인스턴스에 접근할 수 있습니다. 레거시 코드의 점진적 마이그레이션에 효과적입니다. ## inject 함수와 생성자 주입의 비교 Angular 14부터 도입된 `inject` 함수는 생성자 주입을 대체하는 새로운 패턴입니다. ```typescript // inject 함수를 사용한 패턴 @Component({ selector: 'app-modern', template: `...`, }) export class ModernComponent { private userService = inject(UserService); private router = inject(Router); private config = inject(APP_CONFIG); // 로직을 바로 작성 가능 } // 생성자 주입 패턴 @Component({ selector: 'app-traditional', template: `...`, }) export class TraditionalComponent { constructor( private userService: UserService, private router: Router, @Inject(APP_CONFIG) private config: AppConfig ) {} } ``` `inject` 함수의 장점은 상속 시 보일러플레이트 코드 감소와 필드 이니셜라이저에서의 직접 사용입니다. 다만 `inject`는 주입 컨텍스트 내에서만 사용 가능하다는 제약이 있습니다. ## 환경별 인젝터 활용 Angular 19에서는 `EnvironmentInjector`를 사용하여 동적 컴포넌트 생성이나 서비스 해결을 수행할 수 있습니다. ```typescript import { EnvironmentInjector, createEnvironmentInjector, inject, } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class PluginLoaderService { private parentInjector = inject(EnvironmentInjector); loadPlugin(pluginConfig: PluginConfig): EnvironmentInjector { return createEnvironmentInjector( [ { provide: PLUGIN_CONFIG, useValue: pluginConfig, }, PluginService, ], this.parentInjector ); } } ``` 이 패턴은 마이크로 프론트엔드 아키텍처나 플러그인 시스템 구현에서 강력한 효과를 발휘합니다. ## 멀티 프로바이더를 활용한 확장 가능한 설계 `multi: true` 옵션을 사용하면 동일한 토큰에 여러 값을 등록할 수 있습니다. ```typescript import { InjectionToken, inject } from '@angular/core'; export interface Validator { validate(value: unknown): boolean; errorMessage: string; } export const VALIDATORS = new InjectionToken('validators'); // 모듈에서의 등록 @NgModule({ providers: [ { provide: VALIDATORS, useClass: RequiredValidator, multi: true, }, { provide: VALIDATORS, useClass: EmailValidator, multi: true, }, { provide: VALIDATORS, useClass: MinLengthValidator, multi: true, }, ], }) export class ValidationModule {} // 서비스에서의 활용 @Injectable({ providedIn: 'root' }) export class ValidationService { private validators = inject(VALIDATORS); validateAll(value: unknown): string[] { return this.validators .filter((v) => !v.validate(value)) .map((v) => v.errorMessage); } } ``` HTTP 인터셉터나 라우트 가드도 내부적으로 이 메커니즘을 활용합니다. ## 선택적 의존성과 @Self, @SkipSelf 의존성이 존재하지 않을 때의 동작을 제어하는 데코레이터를 이해하는 것이 중요합니다. ```typescript import { Injectable, Optional, Self, SkipSelf, inject } from '@angular/core'; @Component({ selector: 'app-example', template: `...`, }) export class ExampleComponent { // 존재하지 않으면 null private optionalService = inject(OptionalService, { optional: true }); // 자신의 인젝터만 검색 private selfService = inject(SelfService, { self: true, optional: true }); // 부모 인젝터부터 검색 시작 private parentService = inject(ParentService, { skipSelf: true }); } ``` 이러한 옵션들을 조합하면 복잡한 의존성 해결 시나리오에 대응할 수 있습니다. ## 기술 면접 빈출 질문과 모범 답변 ### Q1: providedIn: 'root'와 NgModule의 providers 차이점은? `providedIn: 'root'`는 서비스가 루트 인젝터에 등록되어 애플리케이션 전체에서 싱글톤으로 공유됩니다. Tree-shaking이 활성화되어 사용되지 않는 서비스는 번들에서 제외됩니다. 반면 NgModule의 `providers` 배열에 등록하면 해당 모듈이 로드될 때까지 서비스가 등록되지 않습니다. 지연 로드 모듈에서는 별도의 인스턴스가 생성될 수 있습니다. ### Q2: InjectionToken을 사용하는 이유는? 인터페이스나 타입 별칭은 런타임에 존재하지 않으므로 문자열이나 클래스 외의 값을 DI 토큰으로 사용할 수 없습니다. `InjectionToken`은 타입 안전성을 유지하면서 설정 객체, 함수, 프리미티브 값 등 임의의 값을 주입 가능하게 합니다. 또한 이름 충돌 위험을 제거하고 팩토리 함수를 통한 기본값 설정도 지원합니다. ### Q3: forwardRef는 어떤 경우에 사용하나요? 순환 참조나 정의 전에 클래스를 참조해야 하는 경우에 사용합니다. TypeScript의 호이스팅으로 인해 클래스가 정의되기 전에 참조하면 `undefined`가 되는 문제를 회피할 수 있습니다. ```typescript import { forwardRef, Inject } from '@angular/core'; @Injectable() export class ServiceA { constructor( @Inject(forwardRef(() => ServiceB)) private serviceB: ServiceB ) {} } @Injectable() export class ServiceB { constructor(private serviceA: ServiceA) {} } ``` ### Q4: 컴포넌트 레벨에서 서비스를 제공하는 장점은? 컴포넌트의 `providers` 배열에 서비스를 등록하면 컴포넌트마다 새로운 인스턴스가 생성됩니다. 이를 통해 상태 캡슐화가 실현되고 컴포넌트 파괴 시 서비스도 자동으로 정리됩니다. 리스트 항목처럼 반복되는 컴포넌트에서 각 항목이 독립적인 상태를 가져야 할 때 효과적입니다. ### Q5: Resolution Modifier의 역할을 설명해주세요 `@Optional`, `@Self`, `@SkipSelf`, `@Host`는 의존성 해결 동작을 변경합니다. `@Optional`은 의존성을 찾지 못하면 null을 반환하고, `@Self`는 자신의 인젝터만 검색하며, `@SkipSelf`는 부모부터 검색을 시작하고, `@Host`는 호스트 컴포넌트까지의 범위에서 검색을 수행합니다. ## 결론 Angular의 의존성 주입 시스템은 단순한 서비스 공유부터 복잡한 플러그인 아키텍처까지 폭넓은 사용 사례에 대응할 수 있는 유연성을 갖추고 있습니다. 2026년 현재 `inject` 함수의 보급으로 더욱 간결하고 타입 안전한 코드 작성이 가능해졌습니다. 계층형 인젝터의 이해, 적절한 프로바이더 전략의 선택, 그리고 InjectionToken의 효과적인 활용은 유지보수성 높은 Angular 애플리케이션을 구축하기 위한 필수 역량입니다. 기술 면접에서는 이러한 개념의 이론적 이해뿐만 아니라 실제 사용 사례에 기반한 설명이 가능해야 합니다. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/angular/angular-dependency-injection-advanced-providers-tokens-interview