[Angular] Provider 비교(NgModule.providers vs. Component.providers vs. Component.viewProviders)

2021. 7. 5. 22:29Frontend/Angular

** 초보 개발자로 글에 수정해야 할 부분이 있을 수 있습니다. 정정해야 할 부분은 댓글로 소통 부탁드립니다!

 

정말 코딩의 세계는 무궁무진한 것 같습니다.

 

provider는 당연히 module에서만 import 하는 것이라고 생각했는데

component 별로 import가 가능하더군요! 

(lazy loading의 진수를 보여주는! 👍🏻)

 

구글링 하다가 좋은 영문글을 발견하여 번역해보려 합니다! 

 


STEP 01 _ 준비하기

먼저, 각 provider를 비교하기 위한 예제 코드(5개 파일)를 이해 해봅시다.

 

  • SimpleService
  • ChildComponent
  • ParentComponent
  • AppComponent
  • AppModule

 

1. SimpleService

class SimpleService {
  value: string;
}

 

2. Child Component

@Component({
 selector: 'child',
 template: `
<div class="child">
   <p>Child</p>
   {{ service.value }} // (1)
</div>

})
class ChildComponent {
  constructor(private service: SimpleService) { } // (2) 
}

 

(1) : {{ }} 를 사용해 string interpolation from simple service

(2) : 생성자에 service 를 주입

 

3. Parent Component

@Component({
 selector: 'parent',
 template: `
<div class="parent">
   <p>Parent</p>
   <form novalidate>
        <div class="form-group">
        <input type="text"
               class="form-control"
               name="value"
               [(ngModel)]="service.value"> // (1) 
      </div>
  </form>
  <child></child> // (2) 
</div>
`
})
class ParentComponent {
  constructor(private service: SimpleService) { } // (3)
}

 

(1) : two-way binding to bind the value of SimpleService

(2) : rendering ChildComponent

(3) : 생성자에 service 를 주입

 

4. AppComponent

@Component({
 selector: 'app',
 template: `
 <div class="row">
  <div class="col-xs-6">
    <parent></parent>
  </div>
  <div class="col-xs-6">
    <parent></parent>
  </div>
</div>
 `
})
class AppComponent {
}

 

5. AppModule

 

@NgModule({
  imports:  [ BrowserModule, FormsModule ],
  declarations:  [ AppComponent, ParentComponent, ChildComponent ],
  bootstrap:  [ AppComponent ]
})
class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

 

AppComponent 의 모습은 다음 그림과 같이 출력됩니다.

 

STEP 02 _ NgModule.providers

가장 보편적으로 사용하는 방법이죠! (저는 그랬습니다.. ㅎㅎ)

위에서 생성한 SimpleService 를 module 의 providers 로 import 하는 방식입니다.

 

@NgModule({
  imports:  [ BrowserModule, FormsModule ],
  declarations:  [ AppComponent, ParentComponent, ChildComponent ],
  bootstrap:  [ AppComponent ],
  providers: [ SimpleService ] // (1)
})
class AppModule { }

 

(1) : AppModule의 providers 에 SimpleService를 import

 

이 경우 service가 root NgModule 에 주입되었으므로 결국 root injector에 주입된 것입니다.

그래서 SimpleService로 들어오는 모든 요청은 하나의 root injector로 전달 됩니다.

 

 

이 경우 하나의 injector(root injector 내의 SimpleService) 만 존재하기 때문에

각 component 에서 SimpleService instance를 요청할 때마다(= constructor 에서 주입 했었죠!)

같은 instance가 반환 됩니다.

 

! Important
같은 injector 에서 같은 token을 요청하면, 같은 instance가 반환됩니다.

(하나의 input에만 입력했는데 동시에 값이 update 됩니다. wow 🥲 )

 

Tip
전체 application 에서 하나의 service instance 를 공유하고 싶다면, NgModule 에 service를 import 하면 됩니다.

 

STEP 03 _ Component.providers

그렇다면, ParentComponent 의 providers에 SimpleService 를 import 하는 경우에는 어떤 일이 일어나는지 확인해봅시다!

 

@Component({
 selector: 'parent',
 template: `...`,
 providers: [ SimpleService ]
})
class ParentComponent {
  constructor(private service: SimpleService) { }
}

 

이 경우에는 하나의 ParentComponent 가 하나의 SimpleService를 가지게 됩니다. 요렇게 :

 

 

실제 출력 결과 또한 앞의 경우와 차이가 있습니다.

input 이 각각 동작하게 되죠!

 

ParentComponet가 자신 고유의 SimpleService를 가지게 되므로 state 이 global 하게 공유되지 않고 

ParentComponentChildComponent 간에서만 공유되는 것을 확인할 수 있습니다.

 

! Important
다른 injector 에서 같은 token 을 요청할 때, 다른 instance가 반환됩니다.

 

ParentComponent 에서 SimpleService를 import 하면, child injector를 생성합니다.

그리고 ParentComponent 의 생성자에서 SimpleService를 주입하면,

child injector로 부터 SimpleService instance를 생성합니다.

 

Tip
각 component 마다 하나의 service instance 를 생성하여 child component 와 공유하고 싶다면, 
component decorator 의 providers property 에 service를 import 하세요. 

 

STEP 03 _ Component.viewProviders

ParentComponent 의 viewProviders property에 SimpleService 를 import 하면, 전과 동일하게 동작합니다.

변화를 주기 위해 ng-content 를 사용해 봅시다.

 

그러려면 앞선 예제 코드를 변경해야 합니다.

 

AppComponet의 html 을 다음과 같이 변경합니다.

 

<div class="row">
  <div class="col-xs-6">
    <parent><child></child></parent> // (*) change
  </div>
  <div class="col-xs-6">
    <parent><child></child></parent> // (*) change
  </div>
</div>

 

 

Parent Component 도 다음과 같이 변경합니다.

<div class="parent">
   <p>Parent</p>
   <form novalidate>
        <div class="form-group">
        <input type="text"
               class="form-control"
               name="value"
               [(ngModel)]="service.value">
      </div>
  </form>
  <ng-content></ng-content> // (1)
</div>

 

(1) : 전에 hard coding 되어 있던 ChildComponent 를 content projection 으로 대체했습니다.

 

동일하게 child component 가 출력되지만, view child 가 아닌 content child 로 인식됩니다.

 

ParentComponet의 Providers 를 ViewProviders 로 변경해봅시다.

 

@Component({
 selector: 'parent',
 template: `...`,
 viewProviders: [ SimpleService ]
})
class ParentComponent {
  constructor(private service: SimpleService) { }
}

 

그렇다면, 이 경우는 어떻게 화면에서 동작할까요?

ParentComponent 에서 input에 입력하더라도 

ChildComponent 에서 자동으로 갱신되지 않습니다.

 

 

왜냐하면 ! viewProviders 를 사용하면, 현재 컴포넌트 view children 에서만 사용되는 injector를 생성합니다.

현재 child component 와 같이 content child 의 경우에는 NgModule 의 injector 를 사용합니다.

 

 

Tip
각 component 마다 하나의 service instance 를 생성하여 view child component 와 공유하고 싶다면,
( content child component 는 제외하고)
component decorator 의 viewProviders property 에 service를 import 하세요. 

 

 

[References]

 

NgModule.providers vs Component.providers vs Component.viewProviders • Angular

We can configure the DI framework in Angular in three main ways. We can configure a provider on the NgModule, on a component’s or directive’s providers property, and on a component’s viewProviders property. Deciding where to configure your provider a

codecraft.tv