본문 바로가기

3. Modules

1. @NgModule

앵귤러에서 모듈은 자바스크립트의 모듈과는 다른 의미로 쓰입니다.

먼저, 자바스크립트의 모듈은 단일 파일에서 모든 멤버 클래스를 정의하지만 @NgModule은 declarations이라는 속성에서 선언 가능한 클래스*들의 리스트를 가지고 있습니다.

선언가능한 클래스*: 앵귤러에서는 컴포넌트, 디렉티브, 파이프, 서비스가 이에 해당합니다.

또한 @NgModule은 import문에 선언된 모듈과 자신의 선언가능한 클래스들만을 export할 수 있으며 그 외의 클래스는 불가능합니다.

공식문서에서 자세히 설명하고 있는 것처럼 앵귤러는 컴파일러에게 @NgModule의 메타데이터로 컴포넌트가 컴파일되는 시점의 컨텍스트를 제공합니다. 컴파일러는 해당 컨텍스트(작업 흐름)에 따라 컴포넌트를 컴파일할 것입니다.

NgModule은 애플리케이션 도메인이나 작업 흐름, 기능이 연관된 Angular 구성요소들을 묶어놓은 단위입니다.

기본적으로 NgModule은 즉시 로드되는데 너무 큰 어플리케이션의 경우 앵귤러는 NgModule에 정의된 라우팅모듈을 통해 컴포넌트의 지연 로딩을 가능하게 하여 앱의 초기 실행 시간을 단축할 수 있습니다.

 

다음은 기본적인 @NgModule의 사용 코드입니다.

1.1 @NgModule 파라미터들

@NgModule의 메타데이터를 살펴보겠습니다. 대표적으로 declarations, imports, exports, providers로 이루어집니다.

  • declarations: 모듈에 포함되어 템클릿을 컴파일할 때 함께 트리거될 선언가능한 클래스 리스트를 정의합니다. 이들은 모듈에 한해서만 이용가능하기 때문에(즉, private하므로) export하지 않는 클래스는 외부 모듈에서 접근할 수 없습니다.
  • imports: 다른 모듈이 exports하는 선언가능한 클래스를 해당 모듈의 클래스에서 사용하고 싶을 때 imports 배열에 해당 모듈을 추가합니다.
  • exports: 해당 모듈을 import한 외부 모듈들이 사용할 수 있는 클래스 리스트를 정의합니다.
    • 주의 import한 모듈은 re-export되지 않습니다. 만약 A모듈을 import한 B모듈을 import하는 C모듈에서 A모듈을 사용하고 싶다면 B모듈에서 A모듈을 exports하거나, C모듈에서 A모듈을 imports해야 합니다.
  • providers: 모듈에서 사용할 서비스 프로바이더 리스트를 지정합니다. 모듈에서 사용하는 서비스는 서비스 프로바이더를 통해 생성되며 해당 서비스 인스턴스는 모듈 인젝터 트리에 생성됩니다. 4장에서 자세히 소개하겠습니다.

1.2 모듈의 종류

공식 문서에서는 모듈을 기능에 따라 분리하여 소개하고 있습니다.

  • Domain feature modules: 어플리케이션에 특정 기능을 제공하는 모듈입니다. Top 컴포넌트 아래에 많은 자식 컴포넌트들이 존재할 수 있으며 Top 컴포넌트만 export합니다.
  • Routed feature modules: 도메인 기능 모듈 중 Top 컴포넌트가 라우터의 대상이 되는 컴포넌트인 모듈입니다. 지연 로딩되는 모듈이 이에 해당합니다.
  • Routing modules: 라우팅과 관련한 기능을 주변 모듈과 분리하여 라우팅 설정을 프로바이딩하는 모듈입니다. 라우팅 모듈은 오로지 라우팅만 담당하기 때문에 다른 declarations를 포함하면 안됩니다. 동작 순서는 다음과 같습니다.
    • 라우팅 규칙 정의
    • 라우팅 규칙을 모듈의 imports에 RouterModule로 로드
    • 라우팅 guard와 resolver를 서비스 프로바이더에 등록
    • RouterModule을 re-exports하여 주변 모듈이 RouterLink, RouterOutlet으로 해당 라우터 디렉티브에 접근할 수 있도록 설정
    • 라우팅 모듈 파일명은 {{module-name}}-routing.module.ts 로 명명
  • Service feature modules: 서비스를 프로바이드하는 모듈로, 서비스 외의 다른 디렉티브나 파이프를 포함하지 않으며 루트 모듈에 임포트됩니다. 4장에서 배울 HttpClientModule이 대표적인 서비스 모듈입니다.
  • Widget feature modules: declarations를 외부 모듈이 사용할 수 있도록 정의되는 모듈로 UI 서드파티 라이브러리 등이 이에 해당합니다. 대부분 UI컴포넌트나 디렉티브, 파이프를 외부 모듈이 사용할 수 있도록 모두 export합니다.
[참고] 앵귤러에서 라우팅 모듈을 구현한 코드에서 주석으로 사용법을 아래와 같이 설명하고 있습니다.
"RouterModule can be imported multiple times: once per lazily-loaded bundle. Since the router deals with a global shared resource--location, we cannot have more than one router service active. That is why there are two ways to create the module: RouterModule.forRoot and RouterModule.forChild."

즉, RouterModule은 lazy-load되는 모듈 단위에 따라 여러번 import될 수 있는데 라우터 서비스는 어플리케이션에서 단 한번만 프로바이딩해야 합니다. 따라서 루트 모듈(AppModule)에서는 forRoot로 지연 로딩되는 모듈을 로드하고, 지연 로딩되는 또다른 라우팅 모듈에서는 forChild로 로드하여 라우터 서비스를 포함하지 말아야 합니다. 

2. [실습] 모듈 지연 로딩

이제 모듈의 지연로딩을 구현해보겠습니다. 공식문서의 기능모듈 지연로딩하기에서는 ng 명령어를 통해 자동 생성했지만 구조이해를 위해 글쓴이는 위의 라우팅 모듈과 라우터 대상이 되는 모듈을 직접 구현하면서 공부하였습니다.

 

1,2장에서 구현했던 컴포넌트는 지금 단일 페이지입니다. 이제 이 페이지를 URL 경로에 따라 라우팅할 수 있도록 변경하였습니다.

 

먼저 라우팅 규칙을 정의하였습니다.

  • /menu : 1장에서 만든 메뉴 컴포넌트
    • /menu/detail/:id : 메뉴 상세 컴포넌트
  • /cart : 장바구니 컴포넌트
  • /order: 주문 페이지
    • /order/complete: 주문 성공 페이지
    • /order/failure: 주문 실패 페이지
  • /mycoffee : 마이페이지 컴포넌트
    • /mycoffee/list : 주문 내역 컴포넌트

해당 라우팅 규칙에 따라 지연로딩되는 모듈을 만듭니다.

 

먼저 app-routing.module.ts 파일을 생성 후 아래와 같이 작성합니다.

Route 설정 값에 대해 간단히 설명하자면

  • path: 라우트 경로를 지정합니다.
  • pathMatch: path에 매칭되는 규칙을 설정할 수 있습니다. ['full', 'prefix'] 중 하나의 값을 가집니다. 기본값은 'prefix'입니다.
    • full : path 값에 정확히 일치해야 함
    • prefix : path를 포함한 경로이기만 하면 매치됨
  • children : nested route configuration으로 해당 Route의 하위 Route 리스트를 지정할 수 있습니다.
  • component : 해당 Route 경로에 매치되는 컴포넌트로 래퍼 컴포넌트를 설정할 수 있습니다. children 값이 설정되어 있다면 빈값이 가능합니다.
  • redirectTo : 해당 path에 매치되면 즉시 redirectTo 값으로 리다이렉트합니다. 

글쓴이는 루트(/) 경로에 대해 /menu로 redirectTo를 지정하여 메뉴 화면으로 리다이렉트하였습니다.

 

이제 AppRoutingModule을 AppModule에서 import합니다. RoutingModule을 루트모듈에서 import하였으니 이제 템플릿에서 router-outlet, [routerLink] 등을 사용할 수 있습니다.

app-root 템플릿에 <router-outlet>만 남기고 이전의 모든 템플릿은 제거해줍니다. 앵귤러 라우터가 router-outlet위치에 설정된 라우팅 규칙에 따라 템플릿을 렌더링할 것입니다.

위에서 설명했듯이 라우팅 모듈은 여러 번 임포트가 가능한데 실습에서는 /menu 안에 /menu/detail/:productId 규칙을 두어 하위 라우터를 설정하겠습니다.

마찬가지로 MenuModule에서 MenuRoutingModule을 import하고 view-wrapper 템플릿에 router-outlet으로 기존의 pw-home 템플릿 등을 대체하면 라우터 구성은 완료됩니다.

이제 위에서 설정한 /menu/detail/:productId를 메뉴 화면에서 더보기 버튼을 두어 해당 경로로 이동시키는 것을 구현합니다.

[참고] 원래는 productId 파라미터에 따라 상세 페이지에 렌더링하는 정보가 커피 별로 다를 것입니다. 이것은 5장의 ActivatedRoute와 ActivatedRouteSnapshot에서 더 자세히 기술할 예정이라 여기서는 라우터 파라미터를 사용하고 싶을 땐 이렇게 사용한다는 것만 인지합니다.

view-item 템플릿에 버튼 엘리먼트를 추가하고, click 이벤트를 추가하여 버튼을 누르면 /menu/detail/:productId로 이동할 것입니다.

<button class="btn btn-link btn-search" [class.openPosition]="isOpen" (click)="onClick($event)">more</button>

그러려면 먼저 생성자에서 Router 서비스를 주입하여 인젝터에 Router 서비스 인스턴스를 생성합니다. (4장에서 더 자세히 설명할 것입니다. 지금은 라우터 인스턴스를 만드는구나! 정도로만 생각합니다)

constructor(private changeDetectorRef: ChangeDetectorRef, private router:Router) {}

onClick()에 다음과 같이 router.navigate 메소드로 원하는 경로의 배열을 인자로 정의하면 해당 경로로 라우팅됩니다.

onClick(e) {
    e.preventDefault();
    this.router.navigate([`menu/detail/${this._item.productId}`])
  }

여기까지 라우팅 구현이 완료되면 화면은 다음과 같습니다.

메인에서 more 클릭 시 우측 화면으로 라우팅됩니다.

아직 위의 그림에서 설명하지 않은 하단 탭의 url 경로에 따라 활성화 여부 디자인을 설정하는 것 또한 어렵지 않습니다.

 

마찬가지로 Router 서비스 인스턴스를 생성자 함수에서 주입한 후 router.url 값이 곧 현재경로값입니다.

this.url = this.router.url;
하지만 이렇게 url을 감지하는 것은 치명적인 단점이 있습니다. 다른 컴포넌트에서 router.url을 변경하면 변화를 감지할 수 없습니다. 사실 이럴 땐 router.event Observable을 활용하여 이벤트를 구독하여 url을 변경해야 합니다. 자세한 내용은 마지막 장에서 공부하겠습니다. (아직 공부가 부족ㅠㅠ)

여기까지 라우팅 모듈 설정이었습니다. 이제 나머지 장바구니와 마이페이지 경로도 메뉴와 상세 페이지를 설정하는 것처럼 하시면 됩니다.

 

상세 페이지를 보시면 각 커피에 따라 정보가 달리 보여야 할텐데 아직 구현되지 않았습니다.

 

다음 장인 서비스와 의존성 주입에서 이를 구현해보겠습니다 :)

'Today I Learn > Angular 기초' 카테고리의 다른 글

5. 서비스와 의존성 주입 (2)  (0) 2020.07.04
4. 서비스와 의존성 주입 (1)  (0) 2020.06.30
2. Component와 Directive (2)  (0) 2020.06.20
1. Component와 Directive (1)  (2) 2020.06.19
0. Angular 공부 목록  (2) 2020.06.15