이 문서는 Angular 의존성 주입 시스템을 의존성 객체 판단 규칙, 수정자, 고급 패턴을 포함하여 깊이 있게 다룹니다.
참고: 인젝터 계층의 기본 개념이나 프로바이더 범위를 확인하려면 의존성 주입 프로바이더 문서를 참고하세요.
인젝터 계층의 종류
Angular의 인젝터 계층은 두 종류입니다:
| 인젝터 계층 | 설명 |
|---|---|
EnvironmentInjector 계층 |
@Service() 이나 ApplicationConfig의 providers 배열을 사용하면 EnvironmentInjector를 구성할 수 있습니다. |
ElementInjector 계층 |
개별 DOM 엘리먼트마다 암묵적으로 구성됩니다. @Directive()나 @Component()의 providers 프로퍼티에서 구성하지 않는 한 ElementInjector는 기본적으로 비어 있습니다. |
NgModule 기반 애플리케이션
NgModule 방식으로 개발한 애플리케이션이라면 @NgModule() 이나 @Injectable()에서 ModuleInjector 계층을 구성할 수 있습니다.
EnvironmentInjector
EnvironmentInjector는 다음 두 가지 방법 중 하나로 구성할 수 있습니다:
@Service()ApplicationConfigproviders배열로
트리 셰이킹과 @Service()
ApplicationConfig providers 배열보다는 @Service() 데코레이터 사용을 더 권장합니다.
@Service()를 사용하면 최적화 툴이 트리 셰이킹을 수행할 수 있으며, 이 서비스가 실제로 애플리케이션에 사용되지 않으면 빌드 결과물에 포함하지 않습니다.
결과적으로 빌드 결과물의 크기가 꼭 필요한 수준으로 줄어들 수 있습니다.
트리 셰이킹은 라이브러리인 경우 특히 유용합니다. 애플리케이션이 라이브러리를 사용하지 않으면 빌드 결과물에도 포함되지 않기 때문입니다.
EnvironmentInjector는 ApplicationConfig.providers에서 구성할 수 있습니다.
@Service()를 사용해서 다음과 같이 서비스를 등록할 수 있습니다:
import {Service} from '@angular/core';
@Service() // <-- 이 서비스를 최상위 EnvironmentInjector에 등록합니다.
export class ItemService {
name = 'telephone';
}
@Service()나 @Injectable() 데코레이터가 붙은 클래스는 Angular 서비스 클래스입니다.
ModuleInjector
NgModule 기반 애플리케이션은 다음 두 가지 방법으로 ModuleInjector를 구성할 수 있습니다:
@Service()데코레이터를 사용할 때@Injectable()providedIn프로퍼티 값으로root이나platform을 사용할 때@NgModule()providers배열을 사용할 때
ModuleInjector는 @NgModule.providers와 @NgModule.imports 프로퍼티를 사용해서 구성됩니다.
ModuleInjector는 @NgModule.imports를 재귀적으로 순회하면서 프로바이더 배열에 있는 모든 프로바이더를 평탄화(flattening)합니다.
@NgModule이 지연 로딩되는 경우에는 자식 ModuleInjector 계층이 구성됩니다.
플랫폼 인젝터
root 인젝터 상위 계층에는 EnvironmentInjector와 NullInjector()가 존재합니다.
main.ts 파일에서 Angular가 애플리케이션을 부트스트랩하는 과정을 살펴봅시다:
bootstrapApplication(App, appConfig);
bootstrapApplication 메서드는 ApplicationConfig 인스턴스 값에 따라 플랫폼 인젝터의 자식 인젝터를 생성합니다.
이것이 최상위 인젝터인 EnvironmentInjector입니다.
bootstrapApplication() 메서드는 플랫폼별 의존성이 있는 플랫폼 인젝터를 생성하거나 기존 인젝터를 재사용합니다.
이 방식에 따라 플랫폼 환경설정을 공유하는 애플리케이션은 여러 개가 될 수도 있습니다.
예를 들면, 브라우저에서 실행되는 애플리케이션 숫자에 관계없이 브라우저에는 주소표시줄이 하나만 존재합니다.
그리고 platformBrowser() 함수를 실행할 때 extraProviders 옵션을 사용해서 플랫폼 계층에 플랫폼별 프로바이더를 추가로 구성할 수 있습니다.
그다음으로 트리 최상단에 존재하는 부모 인젝터는 NullInjector() 입니다.
@Optional()을 사용하지 않은 채로 의존성 객체를 찾다가 NullInjector()를 만나면 오류가 발생합니다.
왜냐하면 NullInjector()는 의존성 객체를 찾는 마지막 지점이기 때문에 @Optional()을 사용한 경우 null을 반환하거나, 아니면 오류를 반환하기 때문입니다.
@Optional()에 대해 자세하게 알아보려면 이 문서의 @Optional() 섹션을 참고하세요.
아래 다이어그램은 최상위 ModuleInjector와 그 부모 인젝터들의 관계를 표현합니다.
root라는 이름은 특별한 별칭이지만, EnvironmentInjector와 같은 계층에는 별칭이 없습니다.
Router와 같이 컴포넌트를 동적으로 로딩할 때마다 EnvironmentInjector 계층 구조를 생성할 수 있습니다.
bootstrapApplication() 메서드에 ApplicationConfig 인스턴스가 전달되거나, 각 서비스에서 root 계층으로 등록하면 인스턴스 주입 요청은 최상위 인젝터까지 전달됩니다.
@Injectable() vs. ApplicationConfig
bootstrapApplication() 메서드와 ApplicationConfig를 사용해서 앱 전역 프로바이더를 설정하면 @Injectable() 메타데이터에서 root로 설정한 것을 오버라이드합니다.
이 방식을 활용하면 애플리케이션 여러 곳에 사용되는 서비스 프로바이더 구성을 변경할 수 있습니다.
아래 예제처럼 ApplicationConfig에서 providers 배열에 프로바이더를 등록하면 컴포넌트 라우터 설정 중 location 정책을 변경할 수 있습니다.
providers: [{provide: LocationStrategy, useClass: HashLocationStrategy}];
NgModule 기반으로 개발한 애플리케이션이라면, AppModule providers에 프로바이더를 설정하면 동일한 효과가 됩니다.
ElementInjector
Angular는 개별 DOM 엘리먼트마다 ElementInjector 계층을 구성합니다.
그래서 @Component() 데코레이터의 providers 프로퍼티나 viewProviders 프로퍼티를 사용하면 ElementInjector 계층이 구성됩니다.
예를 들어 다음과 같이 TestComponent 코드를 작성하면 해당 서비스가 ElementInjector 계층에 등록됩니다:
@Component({
/* … */
providers: [{ provide: ItemService, useValue: { name: 'lamp' } }]
})
export class TestComponent
참고: EnvironmentInjector 트리와 ModuleInjector, ELementInjector 트리 관계를 이해하려면 의존성 객체 결정 규칙 섹션을 참고하세요.
서비스를 컴포넌트 계층에 등록하면, 해당 서비스는 컴포넌트 인스턴스의 ElementInjector를 통해 사용할 수 있습니다.
그래서 이 컴포넌트의 자식 컴포넌트나 디렉티브라면 의존성 객체 결정 규칙에 따라 해당 서비스의 인스턴스에 접근할 수 있습니다.
이렇게 등록된 서비스의 인스턴스는 컴포넌트 인스턴스가 종료되면 함께 종료됩니다.
@Directive(), @Component()
컴포넌트는 디렉티브의 특수한 형태라고 볼 수 있습니다.
그렇다는 것은 @Directive()에 providers 프로바이더가 있는 것처럼 @Component() 에서도 providers 배열을 활용해서 프로바이더를 등록할 수 있습니다.
컴포넌트나 디렉티브에서 providers 프로퍼티로 프로바이더를 등록하면, 이 프로바이더는 해당 컴포넌트/디렉티브의 ElementInjector에 등록됩니다.
엘리먼트에 컴포넌트와 디렉티브가 동시에 존재하면 이 컴포넌트와 디렉티브는 인젝터를 공유합니다.
의존성 객체 결정 규칙
컴포넌트/디렉티브에서 의존성 객체 토큰을 결정할 때는 두 단계로 진행됩니다:
ElementInjector계층을 따라 부모 방향으로 탐색합니다.EnvironmentInjector계층을 따라 부모 방향으로 탐색합니다.
어떤 컴포넌트에 의존성 객체가 등록되면 Angular는 해당 ElementInjector에서 의존성 객체를 찾습니다.
그리고 컴포넌트 인젝터에서 프로바이더를 찾지 못하면, 부모 컴포넌트의 ElementInjector를 따라 의존성 객체를 찾습니다.
이런 요청은 부모 ElementInjector 계층을 따라 거슬러 올라가며 원하는 의존성 객체를 찾을 때까지 계속됩니다.
Angular가 ElementInjector 계층을 따라 탐색했지만 의존성 객체를 찾지 못하면, 다시 시작 엘리먼트로 돌아가서 EnvironmentInjector 계층을 따라 다시 한 번 탐색합니다.
그리고 마지막까지 의존성 객체를 찾지 못하면 오류가 발생합니다.
같은 의존성 객체 토큰을 다른 계층에 등록하면, 먼저 만나는 의존성 객체를 사용합니다. 그래서 어떤 서비스가 컴포넌트 계층에 등록되어 있다면, Angular는 해당 컴포넌트에 등록된 프로바이더를 사용하며 다른 곳을 탐색하지 않습니다.
참고: NgModule 기반 애플리케이션은 ElementInjector 계층에서 의존성 객체를 찾지 못했을 때 ModuleInjector 계층을 탐색합니다.
의존성 객체 결정에 영향을 주는 심볼
optional, self, skipSelf, host를 사용하면 의존성 객체 결정 방식을 변경할 수 있습니다.
이 심볼들은 @angular/core 패키지로 제공되며, inject 함수를 사용할 때 사용할 수 있습니다.
종류
의존성 객체에 영향을 주는 심볼은 세 종류로 구분할 수 있습니다:
- Angular가 의존성 객체를 찾지 못할 때:
optional - 의존성 객체 탐색 시작 위치를 변경할 때:
skipSelf - 의존성 객체 탐색을 멈출 위치를 변경할 때:
host,self
기본적으로 Angular는 현재 위치의 Injector부터 의존성 객체를 찾기 시작하지만, 이런 심볼들을 사용하면 의존성 탐색의 시작 위치와 종료 위치를 변경할 수 있습니다.
그리고 다음 조합을 제외한 심볼은 동시에 사용할 수 있습니다:
host와selfskipSelf와self
optional
의존성 객체를 찾지 못해도 오류가 발생하지 않으며, 의존성으로 요청한 객체의 인스턴스는 null이 반환됩니다.
아래 예제 코드에서 OptionalService는 ApplicationConfig, @NgModule(), 컴포넌트 클래스 어디에도 등록되지 않았기 때문에 의존성 객체의 인스턴스를 찾을 수 없습니다.
src/app/optional/optional.ts
export class Optional {
public optional? = inject(OptionalService, {optional: true});
}
self
self를 사용하면 Angular는 해당 컴포넌트/디렉티브의 ElementInjector에서만 의존성 객체를 찾습니다.
self는 현재 호스트 엘리먼트에서 사용할 수 있는 서비스 인스턴스를 참조할 때 사용하면 좋습니다.
이때 오류가 발생하는 것을 방지하려면 self와 optional을 함께 사용하세요.
예를 들어 아래 코드에서 SelfNoDataComponent는 LeafService를 의존성 객체로 요청합니다.
self-no-data.ts
@Component({
selector: 'app-self-no-data',
templateUrl: './self-no-data.html',
styleUrls: ['./self-no-data.css'],
})
export class SelfNoData {
public leaf = inject(LeafService, {optional: true, self: true});
}
이 예제에서 부모 프로바이더가 있고 서비스 객체도 존재한다면 의존성 객체의 인스턴스가 주입되겠지만, self와 optional을 사용하면 현재 호스트 엘리먼트까지 탐색하고 의존성 객체를 찾지 않기 때문에 의존성 객체 인스턴스로 null을 사용합니다.
FlowerService를 사용하는 예제를 살펴봅시다.
아래 예제 코드에서 인젝터는 현재 계층의 ElementInjector에서만 의존성 객체를 찾기 때문에, FlowerService는 튤립(🌷)을 반환합니다.
src/app/self/self.ts
@Component({
selector: 'app-self',
templateUrl: './self.html',
styleUrls: ['./self.css'],
providers: [{provide: FlowerService, useValue: {emoji: '🌷'}}],
})
export class Self {
public flower = inject(FlowerService, {self: true});
}
skipSelf
skipSelf는 self와 반대입니다.
skipSelf를 사용하면 Angular는 현재 컴포넌트/디렉티브의 부모 ElementInjector부터 의존성 객체를 찾기 시작합니다.
그래서 현재 컴포넌트의 providers 배열에 단풍잎 🍁이 의존성 객체로 등록되어 있고, 부모 ElementInjector에 양치식물 🌿이 등록되어 있으면, Angular는 단풍잎 🍁을 건너뛰고 양치식물 🌿을 의존성 객체로 사용합니다.
코드로 보면, 부모 컴포넌트에 다음과 같이 등록되었다고 합시다.
leaf.service.ts
export class LeafService {
emoji = '🌿';
}
자식 컴포넌트에는 LeafService에 대해 단풍잎 🍁이 등록되어 있지만, skipSelf를 사용하면 이 값 대신 부모 계층의 값이 대신 사용됩니다:
skipself.ts
@Component({
selector: 'app-skipself',
templateUrl: './skipself.html',
styleUrls: ['./skipself.css'],
// 이 계층의 LeafService 인스턴스는 무시합니다.
providers: [{provide: LeafService, useValue: {emoji: '🍁'}}],
})
export class Skipself {
// skipSelf 옵션을 사용했습니다.
public leaf = inject(LeafService, {skipSelf: true});
}
이 경우 SkipselfComponent에 주입되는 의존성 객체의 값은 단풍잎 🍁이 아니라 양치식물 🌿입니다.
skipSelf와 optional을 함께 사용하는 경우
skipSelf 옵션을 사용할 때 optional을 함께 사용하면 의존성 객체를 찾지 못해 null을 반환하더라도 오류가 발생하지 않습니다.
아래 코드는 프로퍼티 초기화 시점에 Person 서비스를 의존성으로 주입받는 예제 코드입니다.
이때 skipSelf를 사용했기 때문에 현재 계층의 인젝터는 건너뛰지만, optional을 함께 사용했기 때문에 Person 서비스를 찾지 못해 null을 반환하더라도 오류가 발생하지는 않습니다.
class Person {
parent = inject(Person, {optional: true, skipSelf: true});
}
host
host를 사용하면 의존성 객체 탐색 지점의 마지막 위치를 지정할 수 있습니다.
그래서 host를 사용하면 해당 계층 위쪽으로 의존성 객체의 인스턴스가 존재하더라도 위 계층을 탐색하지 않습니다.
host는 이런 경우에 사용합니다:
host.component.ts
@Component({
selector: 'app-host',
templateUrl: './host.html',
styleUrls: ['./host.css'],
// 서비스 프로바이더를 등록합니다.
providers: [{provide: FlowerService, useValue: {emoji: '🌷'}}],
})
export class Host {
// 의존성 객체를 참조하면서 host 옵션을 사용합니다.
flower = inject(FlowerService, {host: true, optional: true});
}
HostComponent에는 host 옵션을 사용했기 때문에, HostComponent 부모 컴포넌트에서 flower.emoji 값을 무엇으로 정해도 아무 영향을 주지 않습니다.
HostComponent가 받는 의존성 객체의 값은 튤립 🌷이 됩니다.
생성자 주입 시점에 사용하기
위에서 다룬 것처럼, @Optional(), @Self(), @SkipSelf(), @Host()는 생성자에서 의존성 객체를 주입할 때도 사용할 수 있습니다.
개별 심볼은 @angular/core 패키지로 제공되며, 컴포넌트 클래스 생성자에 주입하는 서비스마다 다음과 같이 지정하면 됩니다.
self-no-data.ts
export class SelfNoData {
constructor(@Self() @Optional() public leaf?: LeafService) {}
}
템플릿의 논리 구조
컴포넌트 클래스에 서비스 프로바이더를 등록했다면, 이 서비스는 ElementInjector 트리 안에서 접근할 수 있습니다.
Angular 템플릿의 논리 구조를 이해하면 서비스를 어떻게 구성하는지, 어느 범위에서 접근할 수 있는지 더 명확하게 이해할 수 있습니다.
컴포넌트는 템플릿에 이렇게 사용됩니다:
<app-root> <app-child />; </app-root>
참고: 일반적으로 컴포넌트 클래스 코드와 템플릿은 별도 파일로 작성합니다.
의존성 주입 시스템이 어떻게 동작하는지 이해하려면 이 둘을 논리적 트리로 합쳐서 바라보는 것이 좋습니다.
이때 논리적(logical) 이라는 것은 애플리케이션 DOM 트리인 렌더링 트리와 구분하기 위한 용어입니다.
그리고 컴포넌트 템플릿이 위치한 곳을 표시하기 위해 예제 코드에서는 <#VIEW> 라는 임시 엘리먼트를 사용하겠습니다.
이 엘리먼트는 렌더링 트리에 실제로 존재하는 것은 아니며, 이해를 돕기 위한 목적입니다.
트리 하나로 구성되는 <app-root>와 <app-child> 뷰 트리를 표현해보면 이렇습니다:
<app-root>
<#VIEW>
<app-child>
<#VIEW>
…내용물은 여기에 들어갑니다…
</#VIEW>
</app-child>
</#VIEW>
</app-root>
컴포넌트 클래스에 서비스를 등록한다면 <#VIEW>라는 개념을 구분해서 이해하는 것이 중요합니다.
예제: 서비스를 @Component()에 등록하기
@Component()나 @Directive() 데코레이터에 서비스 프로바이더를 등록하면 이 서비스의 접근 범위가 결정됩니다.
다음 섹션에서는 providers나 viewProviders, skipSelf와 host를 사용해서 서비스의 접근 범위를 변경하는 방법을 알아봅시다.
서비스 프로바이더를 컴포넌트 클래스에 등록하는 방법은 두 가지입니다:
| 배열 | 설명 |
|---|---|
providers |
@Component({ providers: [SomeService] }) |
viewProviders |
@Component({ viewProviders: [SomeService] }) |
아래 예제 코드에서는 Angular 애플리케이션의 논리 트리를 확인할 수 있습니다.
템플릿 컨텍스트에서 인젝터가 어떻게 동작하는지 설명하기 위해, 논리 트리는 HTML 구조로 표현하겠습니다.
예를 들어 논리 트리에 있는 <child-component>는 <parent-component>의 직접 자식 엘리먼트인 것을 의미합니다.
논리 트리에서 @Provide, @Inject, @ApplicationConfig 와 같은 특수 어트리뷰트를 확인할 수 있습니다.
이 어트리뷰트들이 실제 어트리뷰트는 아니지만, 개념을 설명하기 위한 것입니다.
| Angular 서비스 어트리뷰트 | 설명 |
|---|---|
@Inject(Token)=>Value |
어떤 논리 트리에 Token이 의존성으로 주입되면, 실제 주입되는 값은 Value입니다. |
@Provide(Token=Value) |
어떤 논리 트리에 Token 객체를 Value라는 값으로 등록합니다. |
@ApplicationConfig |
어떤 논리 트리에 기본값으로 사용하는EnvironmentInjector 를 설정합니다. |
예제 앱의 구조
예제 애플리케이션은 FlowerService를 root 계층에 등록하며, 이 서비스에 있는 emoji 프로퍼티 값은 히비스커스 🌺 입니다.
flower.service.ts
@Service()
export class FlowerService {
emoji = '🌺';
}
예시 애플리케이션에는 App과 Child만 있다고 합시다.
일반적으로 구성하는 방법을 따르면 다음과 같이 구성할 수 있을 것입니다:
<app-root>
<!-- App 셀렉터-->
<app-child> <!-- Child 셀렉터 --> </app-child>
</app-root>
하지만 Angular가 내부적으로 의존성 객체를 처리하기 위해 구성하는 논리적 뷰를 표현하면 이렇습니다:
<app-root> <!-- App 셀렉터 -->
<#VIEW>
<app-child> <!-- Child 셀렉터 -->
<#VIEW>
</#VIEW>
</app-child>
</#VIEW>
</app-root>
<#VIEW>는 템플릿 인스턴스를 의미합니다.
개별 컴포넌트는 각각의 <#VIEW>를 갖습니다.
이 구조를 이해하면 서비스를 어떻게 등록해야 하는지, 어떻게 주입할 수 있는지, 어느 범위에서 접근할 수 있는지 이해하는 데에 큰 도움이 됩니다.
이제 FlowerService를 주입받는 <app-root>를 봅시다:
export class App {
flower = inject(FlowerService);
}
<app-root> 컴포넌트의 flower 프로퍼티는 템플릿에 이렇게 바인딩됩니다:
<p>Emoji from FlowerService: {{flower.emoji}}</p>
그리고 최종 결과물은 이렇습니다:
Emoji from FlowerService: 🌺
논리 트리 관점에서 보면 이렇게 표현할 수 있습니다:
<app-root @ApplicationConfig
@Inject(FlowerService) flower=>"🌺">
<#VIEW>
<p>Emoji from FlowerService: {{flower.emoji}} (🌺)</p>
<app-child>
<#VIEW>
</#VIEW>
</app-child>
</#VIEW>
</app-root>
<app-root>에서 FlowerService를 의존성 객체로 요청하면, 인젝터가 FlowerService 토큰을 찾기 시작합니다.
이 토큰은 두 단계로 결정됩니다:
인젝터가 논리 트리에서 검색할 시작 위치와 종료 위치를 결정합니다. 그리고 인젝터는 검색 시작 위치에서 토큰을 찾는데, 토큰을 발견하면 토큰에 연결된 값을 반환합니다.
토큰을 찾지 못하면 인젝터는 가장 가까운 부모
EnvironmentInjector에서 다시 토큰을 찾습니다.
예제 코드로 설명하면 이렇습니다:
<app-root>의<#VIEW>범위에서만 토큰을 찾습니다.- 일반적으로는 의존성 객체를 요청한 계층부터 의존성 객체의 토큰을 찾기 시작합니다.
하지만,
<app-root>는 컴포넌트이며,viewProviders가 설정되어 있습니다. 따라서 의존성 객체는<app-root>컴포넌트의<#VIEW>부터 찾기 시작합니다. 같은 계층에 존재하는 디렉티브는 해당되지 않습니다. - 의존성 객체를 검색하는 마지막 지점이 같은 컴포넌트인 이유는, 이 컴포넌트가 애플리케이션의 최상위 컴포넌트이기 때문입니다.
- 일반적으로는 의존성 객체를 요청한 계층부터 의존성 객체의 토큰을 찾기 시작합니다.
하지만,
ElementInjector계층에서 의존성 토큰을 찾지 못하면,ApplicationConfig로 구성된EnvironmentInjector가 대체 인젝터로서 요청을 처리합니다.
providers 배열을 활용하는 방법
이제 좀 더 복잡한 경우를 살펴보기 위해 Child 클래스에 FlowerService의 프로바이더를 추가해 봅시다:
@Component({
selector: 'app-child',
templateUrl: './child.html',
styleUrls: ['./child.css'],
// providers 배열을 사용해서 서비스 프로바이더를 등록합니다.
providers: [{provide: FlowerService, useValue: {emoji: '🌻'}}],
})
export class Child {
// 서비스를 의존성으로 주입합니다.
flower = inject(FlowerService);
}
이제 FlowerService가 @Component() 데코레이터에 등록되었기 때문에, <app-child>에서 의존성 객체를 요청하면 인젝터는 <app-child>에 있는 ElementInjector 에서만 의존성 객체를 찾습니다.
인젝터 트리를 따라 위로 향하지 않습니다.
그다음 단계는 Child 템플릿에 바인딩을 추가해 봅시다:
<p>Emoji from FlowerService: {{flower.emoji}}</p>
이제 새 값을 렌더링하기 위해 App 템플릿 제일 아래에 <app-child>를 추가합니다.
그러면 해바라기가 화면에 표시될 것입니다:
Child Component
Emoji from FlowerService: 🌻
논리 트리로 표현하면 이렇습니다:
<app-root @ApplicationConfig
@Inject(FlowerService) flower=>"🌺">
<#VIEW>
<p>Emoji from FlowerService: {{flower.emoji}} (🌺)</p>
<app-child @Provide(FlowerService="🌻")
@Inject(FlowerService)=>"🌻"> <!-- 검색이 여기에서 종료됩니다 -->
<#VIEW> <!-- 검색이 여기에서 시작됩니다 -->
<h2>Child Component</h2>
<p>Emoji from FlowerService: {{flower.emoji}} (🌻)</p>
</#VIEW>
</app-child>
</#VIEW>
</app-root>
<app-child>가 FlowerService를 의존성 객체로 요청하면, 인젝터는 <app-child>의 <#VIEW>부터 의존성 객체를 찾기 시작합니다.
<#VIEW>는 @Component()에서 주입되기 때문에 검색 범위에 포함되며, <app-child>에서 검색이 종료됩니다.
이 예제의 경우는 FlowerService가 <app-child> 단계에서 providers 배열에 등록되었기 때문에 해바라기 🌻 가 의존성 객체로 결정됩니다.
그래서 인젝터는 이보다 상위 인젝터 트리는 탐색하지 않습니다.
히비스커스 🌺가 등록된 FlowerService는 전혀 참조하지 않습니다.
viewProviders 배열을 활용하는 방법
@Component() 데코레이터에서 viewProviders 배열을 활용해도 서비스를 의존성 객체로 등록할 수 있습니다.
viewProviders를 활용하면 <#VIEW>에서도 서비스에 접근할 수 있습니다.
참고: 방법은 providers 배열 대신 viewProviders 배열을 활용한다는 것 외에는 providers 배열을 활용할 때와 동일합니다.
단계별 설정방법은 이 섹션을 계속 진행하면 되며, 직접 설정하는 단계로 진행하려면 서비스 접근범위 변경하기 섹션을 참고하세요.
viewProviders를 설명하기 위해 AnimalService를 만들어 봅시다.
먼저, AnimalService에 emoji 프로퍼티 값으로 고래 🐳를 할당합니다:
그리고 FlowerService 때와 마찬가지로, App 클래스에 AnimalService 를 의존성으로 주입합니다:
export class App {
public flower = inject(FlowerService);
public animal = inject(AnimalService);
}
참고: FlowerService 코드는 그대로 두고 AnimalService와 비교해봐도 좋습니다.
이번에는 <app-child> 클래스에 viewProviders 배열을 추가하고 AnimalService를 등록하는데, emoji 값을 강아지 🐶로 설정해 봅시다.
@Component({
selector: 'app-child',
templateUrl: './child.html',
styleUrls: ['./child.css'],
// 서비스 프로바이더를 등록합니다.
providers: [{provide: FlowerService, useValue: {emoji: '🌻'}}],
viewProviders: [{provide: AnimalService, useValue: {emoji: '🐶'}}],
})
export class Child {
// inject services
flower = inject(FlowerService);
animal = inject(AnimalService);
}
그리고 App 템플릿에 Child를 바인딩합니다.
Child 템플릿에는 이런 바인딩 문구가 있습니다:
<p>Emoji from AnimalService: {{animal.emoji}}</p>
그리고 App 템플릿에도 같은 내용을 추가합니다:
<p>Emoji from AnimalService: {{animal.emoji}}</p>
이제 브라우저에서 두 값이 어떻게 표시되는지 확인해 보세요:
App
Emoji from AnimalService: 🐳
Child Component
Emoji from AnimalService: 🐶
viewProviders 논리 트리를 보면 이렇습니다:
<app-root @ApplicationConfig
@Inject(AnimalService) animal=>"🐳">
<#VIEW>
<app-child>
<#VIEW @Provide(AnimalService="🐶")
@Inject(AnimalService=>"🐶")>
<!-- ^^viewProviders를 사용하면 <#VIEW>에서도 AnimalService에 접근할 수 있습니다.-->
<p>Emoji from AnimalService: {{animal.emoji}} (🐶)</p>
</#VIEW>
</app-child>
</#VIEW>
</app-root>
FlowerService 예제에서 본 것처럼, AnimalService는 <app-child>의 @Component() 데코레이터에 등록되어 있습니다.
이 말은, 인젝터가 컴포넌트의 ElementInjector에서 의존성 객체를 제일 먼저 찾기 때문에, 값이 강아지 🐶인 AnimalService를 찾게 된다는 것을 의미합니다.
상위 ElementInjector 트리나 ModuleInjector 트리를 탐색할 필요는 없습니다.
providers vs. viewProviders
viewProviders는 providers와 개념적으로 유사하지만, 중요한 차이가 있습니다.
viewProviders에 프로바이더를 등록하면 컴포넌트의 논리적 자식으로 추가되어 프로젝션되는 컨텐츠에서는 접근할 수 없습니다.
providers와 viewProviders의 차이를 확인하기 위해 Inspector를 추가해 봅시다.
Inspector는 Child의 자식 컴포넌트로 구성하며, inspector.ts 파일에서 프로퍼티를 선언하면서 FlowerService와 AnimalService를 의존성 객체로 할당합니다:
export class Inspector {
flower = inject(FlowerService);
animal = inject(AnimalService);
}
이때 providers 배열이나 viewProviders 배열은 필요하지 않습니다.
그리고 inspector.html 파일에서 이전에 추가했던 것과 같은 마크업을 추가합니다:
<p>Emoji from FlowerService: {{flower.emoji}}</p>
<p>Emoji from AnimalService: {{animal.emoji}}</p>
Inspector를 Child imports 배열에 추가해야 하는 것을 잊지 마세요.
@Component({
...
imports: [Inspector]
})
그리고 child.html 파일의 내용을 다음과 같이 구성합니다:
...
<div class="container">
<h3>Content projection</h3>
<ng-content></ng-content>
</div>
<h3>Inside the view</h3>
<app-inspector />
<ng-content>는 컨텐츠를 프로젝션할 때 사용하며, <app-inspector>가 Child의 템플릿 안에 사용되었기 때문에 Inspector는 Child의 자식 컴포넌트로 구성됩니다.
이제 app.html 파일에 컨텐츠를 프로젝션하는 코드를 추가합니다.
<app-child>
<app-inspector />
</app-child>
이제 브라우저는 간결하게 다음과 같이 표시됩니다:
...
Content projection
Emoji from FlowerService: 🌻
Emoji from AnimalService: 🐳
Emoji from FlowerService: 🌻
Emoji from AnimalService: 🐶
이렇게 네 가지로 바인딩한 결과를 보면 providers와 viewProviders의 차이점을 확인할 수 있습니다.
강아지 이모지 🐶는 Child의 <#VIEW> 안에 선언되어 있기 때문에 프로젝션된 컨텐츠 안에서는 접근할 수 없습니다.
프로젝션된 컨텐츠 안에서는 고래 🐳를 의존성 객체로 참조할 수 있습니다.
프로젝션된 <app-inspector>에서 왜 루트에 등록된 AnimalService인 🐳를 참조할 수 있는지 의문이 들 수 있습니다.
Angular의 의존성 주입 시스템은 컴포넌트가 렌더링되는 곳 이 아니라 컴포넌트가 선언된 곳 을 추적하기 때문입니다.
<app-inspector>는 App의 템플릿 안, 즉 App의 <#VIEW> 안에 선언되어 있으므로 의존성을 탐색할 때 Child의 <#VIEW>를 거치지 않습니다.
Child 안으로 프로젝션된 컨텐츠는 Child의 viewProviders에 등록된 🐶에는 접근할 수 없지만, 트리 상위의 루트 AnimalService인 🐳에는 접근할 수 있습니다.
그런데 다음 섹션을 보면, Inspector는 Child의 자식 컴포넌트이며, Inspector는 <#VIEW> 안에 존재하기 때문에, 이때 참조하는 AnimalService는 강아지 🐶 값이 됩니다.
로직 트리로 표현해보면 이렇습니다:
<app-root @ApplicationConfig
@Inject(AnimalService) animal=>"🐳">
<#VIEW>
<app-child>
<#VIEW @Provide(AnimalService="🐶")
@Inject(AnimalService=>"🐶")>
<!-- ^^viewProviders를 사용하면 <#VIEW> 안에서 AnimalService에 접근할 수 있습니다.-->
<p>Emoji from AnimalService: {{animal.emoji}} (🐶)</p>
<div class="container">
<h3>Content projection</h3>
<app-inspector @Inject(AnimalService) animal=>"🐳">
<p>Emoji from AnimalService: {{animal.emoji}} (🐳)</p>
</app-inspector>
</div>
<app-inspector>
<#VIEW @Inject(AnimalService) animal=>"🐶">
<p>Emoji from AnimalService: {{animal.emoji}} (🐶)</p>
</#VIEW>
</app-inspector>
</#VIEW>
</app-child>
</#VIEW>
</app-root>
프로젝션된 <app-inspector>는 🐳를 참조합니다.
🐶는 Child의 뷰에 속하므로 프로젝션된 컨텐츠에서는 접근할 수 없습니다.
<app-inspector>가 App의 템플릿에 선언되어 있기 때문에 트리 상위의 루트 AnimalService인 🐳에는 접근할 수 있습니다.
프로젝션되지 않고 Child의 템플릿 안에 직접 선언된 <app-inspector>는 🐶를 참조합니다.
이 컴포넌트는 <#VIEW> 안에 있으므로 뷰 경계를 넘을 필요가 없습니다.
프로젝션된 콘텐츠에서 뷰 인젝터에 접근하기
<ng-content>로 프로젝션한 콘텐츠에서는 컴포넌트의 viewProviders에 접근할 수 없습니다.
Angular는 콘텐츠가 렌더링되는 위치가 아니라 선언된 위치의 인젝터를 기준으로 의존성을 찾기 때문입니다.
프로젝션한 콘텐츠에서 뷰 계층의 서비스나 콘텐츠를 프로젝션하는 컴포넌트 인스턴스 자체에 접근해야 한다면, <ng-content> 대신 템플릿으로 콘텐츠를 받아서 인젝터를 명시적으로 지정해 렌더링하세요.
Child의 viewProviders에 AnimalService를 제공하고, 프로젝션한 템플릿을 쿼리로 조회하세요.
그런 다음 NgTemplateOutlet으로 렌더링하면서 ngTemplateOutletInjector에 해당 서비스에 접근할 수 있는 인젝터를 지정하세요.
@Component({
selector: 'app-child',
viewProviders: [AnimalService],
imports: [NgTemplateOutlet],
template: `
<ng-container [ngTemplateOutlet]="content()" [ngTemplateOutletInjector]="injector" />
`,
})
export class Child {
readonly content = contentChild.required(TemplateRef);
readonly injector = inject(Injector);
}
사용하는 쪽에서는 프로젝션할 마크업을 <ng-template>으로 감쌉니다.
<app-child>
<ng-template>
<app-inspector />
</ng-template>
</app-child>
이제 Child가 자체 인젝터를 사용해서 아웃렛에 <app-inspector>를 생성합니다. 따라서 마크업을 App의 템플릿에 작성했더라도 AnimalService는 강아지 🐶로 결정됩니다.
다만 <ng-content>를 사용할 때보다 양쪽에 마크업을 조금 더 작성해야 합니다.
의존성 토큰의 접근 범위
접근 범위를 조절하는 데코레이터는 의존성 토큰을 로직 트리에서 찾는 시작 지점과 종료 지점을 변경합니다.
이렇게 접근 범위를 조절하려면, 선언 시점이 아니라 inject()를 실행해서 의존성 객체를 주입하는 시점에 데코레이터를 사용해야 합니다.
인젝터가 FlowerService를 찾는 시작 지점을 변경하려면 <app-child>에서 inject()를 실행해서 FlowerService를 주입하는 코드에 skipSelf를 추가하면 됩니다.
아래 예제 코드에서 보면 child.ts 파일의 <app-child> 컴포넌트에서 프로퍼티를 선언하는 시점에 해당됩니다:
flower = inject(FlowerService, {skipSelf: true});
skipSelf를 사용하면 <app-child>의 인젝터가 자신의 계층에서 FlowerService를 탐색하지 않습니다.
대신에, 인젝터는 <app-root> 계층의 ElementInjector에서 FlowerService를 찾기 시작하며, 여기에서는 의존성 토큰을 찾지 못합니다.
그러면 이제 <app-child>와 <app-root>가 공유하고 있는 ModuleInjector로 돌아가서 빨간 히비스커스 🌺 값을 찾게 됩니다.
결과적으로 화면이 다음과 같이 표시됩니다:
Emoji from FlowerService: 🌺
논리 트리로 표현해보면 이렇습니다:
<app-root @ApplicationConfig
@Inject(FlowerService) flower=>"🌺">
<#VIEW>
<app-child @Provide(FlowerService="🌻")>
<#VIEW @Inject(FlowerService, SkipSelf)=>"🌺">
<!-- skipSelf를 사용하면 인젝터는 상위(app-root) 인젝터부터 의존성 토큰을 탐색합니다 -->
</#VIEW>
</app-child>
</#VIEW>
</app-root>
<app-child>에 해바라기 🌻가 등록되어 있지만, skipSelf가 현재 인젝터(app-child)를 건너뛰고 부모 계층에서 의존성을 찾게 하므로 애플리케이션에는 빨간 히비스커스 🌺가 표시됩니다.
이 예제에서 skipSelf와 host를 함께 사용하면 null을 반환합니다.
왜냐하면 host는 app-child의 <#VIEW> 상위 계층의 탐색을 제한하기 때문입니다.
논리 트리로 표현하면 이렇습니다:
<app-root @ApplicationConfig
@Inject(FlowerService) flower=>"🌺">
<#VIEW> <!-- 탐색이 여기에서 종료되고 null을 반환합니다 -->
<app-child @Provide(FlowerService="🌻")> <!-- 여기에서 탐색을 시작합니다 -->
<#VIEW inject(FlowerService, {skipSelf: true, host: true, optional:true})=>null>
</#VIEW>
</app-child>
</#VIEW>
</app-root>
이 예제를 보면, 서비스와 서비스에 연결된 값은 동일하지만, host가 지정되었기 때문에 인젝터가 현재 계층의 <#VIEW> 위쪽을 탐색하지 않기 때문에, FlowerService를 찾지 못하고 null을 반환합니다.
skipSelf, viewProviders
<app-child>의 viewProviders 배열에는 AnimalService가 강아지 🐶 값으로 등록되어 있습니다.
인젝터는 <app-child>의 ElementInjector에서만 AnimalService를 탐색하기 때문에, 고래 🐳 값은 절대 발견할 수 없습니다.
그래서 FlowerService 예제와 마찬가지로, inject()를 실행해서 AnimalService를 주입하는 코드에 skipSelf를 추가하면, 인젝터는 현재 <app-child> 계층의 ElementInjector에서 AnimalService를 찾지 못합니다.
대신, 인젝터는 <app-root>의 ElementInjector 부터 탐색을 시작합니다.
@Component({
selector: 'app-child',
…
viewProviders: [
{ provide: AnimalService, useValue: { emoji: '🐶' } },
],
})
논리 트리로 표현하면 이렇습니다:
<app-root @ApplicationConfig
@Inject(AnimalService=>"🐳")>
<#VIEW><!-- 여리부터 탐색을 시작합니다 -->
<app-child>
<#VIEW @Provide(AnimalService="🐶")
@Inject(AnimalService, SkipSelf=>"🐳")>
<!-- skipSelf를 추가 -->
</#VIEW>
</app-child>
</#VIEW>
</app-root>
<app-child>에 skipSelf를 사용하면, 인젝터는 <app-root>의 ElementInjector 부터 AnimalService를 탐색하기 때문에, 고래 🐳 값을 찾을 수 있습니다.
host, viewProviders
AnimalService를 주입할 때 host를 사용하면 인젝터가 <app-child>의 <#VIEW>부터 의존성 객체를 찾기 때문에 강아지 🐶 값을 찾습니다.
이 값은 Child의 viewProviders에 등록되어 있으며, host는 inject()를 실행할 때 사용합니다:
@Component({
selector: 'app-child',
…
viewProviders: [
{ provide: AnimalService, useValue: { emoji: '🐶' } },
]
})
export class Child {
animal = inject(AnimalService, { host: true })
}
host: true 옵션을 사용하면 인젝터는 <#VIEW>의 영역까지만 의존성 객체를 찾습니다.
<app-root @ApplicationConfig
@Inject(AnimalService=>"🐳")>
<#VIEW>
<app-child>
<#VIEW @Provide(AnimalService="🐶")
@inject(AnimalService, {host: true}=>"🐶")> <!-- host를 사용하면 탐색이 여기에서 중단됩니다 -->
</#VIEW>
</app-child>
</#VIEW>
</app-root>
app.ts 파일 @Component() 메타데이터의 viewProviders 배열에 세번째 동물로 고슴도치 🦔를 추가해 봅시다:
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrls: [ './app.css' ],
viewProviders: [
{ provide: AnimalService, useValue: { emoji: '🦔' } },
],
})
다음에는 child.ts 파일에서 inject()를 실행해서 AnimalService를 주입할 때 host와 함께 skipSelf를 추가해 봅시다.
animal 프로퍼티를 초기화할 때 host와 skipSelf를 사용하면 이렇습니다:
export class Child {
animal = inject(AnimalService, {host: true, skipSelf: true});
}
providers 배열에 FlowerService를 등록했지만 host와 skipSelf를 사용하면 null이 반환되는 이유는, skipSelf를 사용했기 때문에 <app-child> 인젝터를 건너뛰지만, host를 사용했기 때문에 해당 계층의 <#VIEW>에서 탐색을 멈추기 때문입니다.
논리 트리로 보면 <app-child>에서는 FlowerService에 접근할 수 있지만 <app-child>의 <#VIEW>는 그렇지 않습니다.
하지만, AnimalService는 App의 viewProviders 배열에 등록되었기 때문에 접근할 수 있습니다.
논리 트리로 표현하면 이렇습니다:
<app-root @ApplicationConfig
@Inject(AnimalService=>"🐳")>
<#VIEW @Provide(AnimalService="🦔")
@Inject(AnimalService, @Optional)=>"🦔">
<!-- ^^skipSelf가 사용되어 탐색이 여기에서 시작되지만, host가 사용되어 여기에서 종료됩니다.^^ -->
<app-child>
<#VIEW @Provide(AnimalService="🐶")
@inject(AnimalService, {skipSelf:true, host: true, optional: true})=>"🦔">
<!-- skipSelf가 사용되었습니다 ^^-->
</#VIEW>
</app-child>
</#VIEW>
</app-root>
skipSelf를 사용하면 인젝터는 <app-child>가 아니라 <app-root>에서부터 AnimalService를 탐색하고, host를 사용하면 <app-root>의 <#VIEW>에서 탐색을 종료합니다.
결국 AnimalService는 viewProviders 배열에 등록되어 있기 때문에 <#VIEW>에서 접근할 수 있는 의존성 객체의 값은 고슴도치 🦔가 됩니다.
예제: ElementInject 사용 사례
여러 상황을 고려하면서 프로바이더를 다양하게 구성해 봅시다.
시나리오: 서비스 분리
설계 측면에서 특정 서비스를 애플리케이션의 영역에서 분리해야 하는 경우가 있습니다.
빌런의 목록을 표시하는 VillainsList를 예로 들어 봅시다.
빌런 목록은 VillainsService에서 가져옵니다.
이때 VillainsService를 AppModule에 등록하면, VillainsService는 애플리케이션 전체 범위에서 자유롭게 접근할 수 있습니다.
그래서 이후에 VillainsService를 수정하면 다른 컴포넌트에서 문제가 발생할 수 있습니다.
대신, VillainService를 VillainsList의 providers에 등록하는 방법을 사용할 수도 있습니다:
@Component({
selector: 'app-villains-list',
templateUrl: './villains-list.html',
providers: [VillainsService],
})
export class VillainsList {}
VillainsService를 VillainsList에 등록하면 이제 이 컴포넌트와 자식 컴포넌트가 아닌 곳에서는 VillainsService에 접근할 수 없습니다.
VillainService는 이 서비스가 등록된 VillainsList의 기준에서 싱글턴으로 동작합니다.
그래서 VillainsList가 종료되지 않는 한 VillainService는 동일한 인스턴스가 유지되지만, VillainsList의 인스턴스가 여러 개가 되면 개별 컴포넌트 인스턴스마다 VilalinService의 인스턴스가 생성됩니다.
시나리오: 세션 여러 개 유지하기
대부분의 애플리케이션은 보통 동시에 여러 작업을 처리합니다. 세금 신고 애플리케이션이라면, 신고 담당자는 하루 종일 여러 작업을 전환하면서 세금 신고서를 동시에 작업할 수도 있습니다.
이와 비슷하게, 슈퍼 히어로 목록을 표시하는 HeroList를 생각해 봅시다.
목록에서 히어로의 이름을 클릭하면 히어로의 세금 신고서가 표시되고 편집할 수 있는 컴포넌트가 열립니다. 그러면 각 히어로의 세금 신고서는 동시에 각각의 컴포넌트로 열립니다.
이때 개별 컴포넌트는 이런 특징이 있습니다:
- 독립적인 편집 세션을 유지합니다.
- 다른 컴포넌트에 영향을 주지 않고 세금 신고서를 수정할 수 있습니다.
- 변경사항을 저장하거나 취소합니다.
HeroTaxReturnEditor에 변경사항을 관리하고 복원하는 로직이 있다고 합시다.
히어로의 세금 신고서를 편집하는 화면이라면 간단한 작업입니다.
하지만 실제 운영되는 애플리케이션이라면 복잡한 데이터 모델을 사용하기 때문에 변경사항을 관리하는 것이 까다로워질 수 있습니다.
이런 경우 관리 로직은 헬퍼 서비스로 분리하는 것이 좋습니다.
HeroTaxReturnService는 개별 HeroTaxReturn을 캐싱하고 변경사항을 추적하며, 변경한 내용을 저장하거나 복원할 수 있습니다.
이 작업은 애플리케이션 전역에 싱글턴으로 존재하는 HeroService가 담당합니다.
import {inject, Service} from '@angular/core';
import {HeroTaxReturn} from './hero';
import {HeroesService} from './heroes.service';
@Service({autoProvided: false})
export class HeroTaxReturnService {
private currentTaxReturn!: HeroTaxReturn;
private originalTaxReturn!: HeroTaxReturn;
private heroService = inject(HeroesService);
set taxReturn(htr: HeroTaxReturn) {
this.originalTaxReturn = htr;
this.currentTaxReturn = htr.clone();
}
get taxReturn(): HeroTaxReturn {
return this.currentTaxReturn;
}
restoreTaxReturn() {
this.taxReturn = this.originalTaxReturn;
}
saveTaxReturn() {
this.taxReturn = this.currentTaxReturn;
this.heroService.saveTaxReturn(this.currentTaxReturn).subscribe();
}
}
HeroTaxReturnService를 활용하는 HeroTaxReturnEditor 코드는 이렇습니다.
import {Component, effect, inject, input, output} from '@angular/core';
import {HeroTaxReturn} from './hero';
import {HeroTaxReturnService} from './hero-tax-return.service';
@Component({
selector: 'app-hero-tax-return',
templateUrl: './hero-tax-return.html',
styleUrls: ['./hero-tax-return.css'],
providers: [HeroTaxReturnService],
})
export class HeroTaxReturnEditor {
message = '';
close = output<void>();
taxReturn = input.required<HeroTaxReturn>();
private heroTaxReturnService = inject(HeroTaxReturnService);
constructor() {
effect(() => {
this.heroTaxReturnService.taxReturn = this.taxReturn();
});
}
onCanceled() {
this.flashMessage('Canceled');
this.heroTaxReturnService.restoreTaxReturn();
}
onClose() {
this.close.emit();
}
onSaved() {
this.flashMessage('Saved');
this.heroTaxReturnService.saveTaxReturn();
}
flashMessage(msg: string) {
this.message = msg;
setTimeout(() => (this.message = ''), 500);
}
}
수정하려는 세금 신고서는 taxReturn 시그널 입력 프로퍼티로 전달됩니다.
입력값이 변경될 때마다 effect가 전달받은 신고서로 컴포넌트 자체의 HeroTaxReturnService 인스턴스를 초기화합니다.
서비스의 게터는 서비스가 관리하는 현재 상태를 반환합니다.
이 컴포넌트는 세금 신고서를 저장하거나 복원할 때도 서비스를 활용합니다.
서비스가 애플리케이션 전역 범위에 싱글턴으로 존재하면 이런 방식은 동작하지 않습니다. 모든 컴포넌트가 서비스 인스턴스 하나를 공유하게 되면, 개별 컴포넌트에서 수정하는 세금 신고서가 다른 히어로의 세금 신고서를 덮어쓰게 될 것입니다.
그래서 이런 경우는 HeroTaxReturnEditor 계층에서 providers 배열로 서비스 프로바이더를 등록해야 합니다.
providers: [HeroTaxReturnService];
HeroTaxReturnEditor는 HeroTaxReturnService를 자체 프로바이더로 등록했습니다.
컴포넌트 인스턴스마다 인젝터가 존재한다는 것을 떠올려 보세요.
컴포넌트 계층에 서비스 프로바이더를 등록하면 개별 컴포넌트 인스턴스마다 독자적인 서비스 인스턴스를 갖게 됩니다.
결국 다른 컴포넌트의 작업을 덮어쓰지 않습니다.
참고: 예제 코드에서 다루지 않은 부분은 다른 문서에서 설명합니다.
시나리오: 프로바이더 오버라이드
서비스 프로바이더를 다른 계층에 다시 등록하는 경우가 있는데, 컴포넌트 트리 안쪽에서 서비스의 구현체를 더 특화된 로직으로 대체하는 경우가 있습니다.
예를 들어, 타이어 서비스를 포함해서 여러 서비스를 활용하는 Car 컴포넌트가 있다고 합시다.
그리고 최상위 인젝터를 (A)라고 하고 CarService, EngineService와 같은 서비스가 있다고 합시다.
Car컴포넌트 (A). 컴포넌트 (A)는 타이어 서비스의 데이터를 표시하고, 다른 서비스를 활용해서 자동차의 정보를 표시합니다.자식 컴포넌트 (B). 컴포넌트 (B)에는 컴포넌트 (B)에 특화된
CarService와EngineService를 프로바이더로 등록했습니다.자식 컴포넌트 (C)는 자식 컴포넌트 (B)와 비슷하게 조금 더 특화된
CarService를 프로바이더로 등록했습니다.
내부적으로 컴포넌트는 각각 독립적인 인젝터를 구성하며, 이 컴포넌트 계층에 프로바이더를 자유롭게 구성할 수 있습니다.
그리고 가장 안쪽에 있는 컴포넌트 (C)에서 Car 인스턴스를 찾게 되면 인젝터가 이렇게 동작합니다:
- 인젝터 (C)가
Car인스턴스를 생성합니다. - 인젝터 (B)가
Engine인스턴스를 생성합니다. - 최상위 인젝터 (A)가
Tires인스턴스를 생성합니다.