ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 7장) Angular 2 -HeroEditor- (4)
    Angular 2017. 1. 12. 14:27

    자 오늘은 Services 편 입니다. 

    서비스는 우리의 web application 에서 필요한 프로세스 중 자주 쓰는 기능을 하나로 묶어서 

    여러 컴포넌트에서 동시에 공유해서 사용하는 것 입니다. 

    현재 Hero editor 에서 영웅 데이터에 접근하는 코드들은 따로 서비스로 묶어서 사용하면  

    컴포넌트의 독립성을 지키고, 좀 더 뷰에 집중할 수 있도록 만들 수 있습니다. 


    구글에서는 현재 Hero editor 가 더욱 확장되기를 원하고 있습니다. 

    그들은 여러페이지에서 다양한 방법으로 영웅을 보여주고 싶어 하며, 최고 영웅의 랭킹이 나타나는 화면 , 

    그리고 영웅의 세부정보를 보고 편집하는 별도의 화면을 원합니다. 

    이 화면들은 전부 Hero 데이터에 접근 할 필요가 있습니다. 


    그리하여 영웅데이터에 접근하는 서비스를 만들어 여러 컴포넌트가 같이 사용할 수 있도록 합시다.

    먼저 app 폴더에 hero.service.ts 파일을 만듭시다. 

    그전에 잠깐 ! 우리는 현재 뭘 사용하고 있죠? 그렇습니다. Angular-cli 를 사용 중 입니다. 

    Angular-cli 를 사용하면 저번 컴포넌트 생성처럼 서비스도 커맨드로 생성이 가능합니다. 

    해당 프로젝트 폴더로 가서 ng -g service hero.service 명령어를 사용합시다. 


    그럼 app 폴더에 hero.service.ts 파일이 생성됩니다. 


    app/hero.service.ts

    
    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class HeroService {
    
      constructor() { }
      getHeroes() : void {}
    
    }
    


    정상적으로 파일이 생성되었습니다. 

    여기서 @Injectable() 데코레이터가 있습니다. 

    이 Injectable 은 해당 로직이 서비스로서 주입이 가능한 클래스라는 것을 표시 해 주는 것 입니다. 

    참고로 이 Injectable 이 없다고 작동하지 않는 건 아니지만, 해당 클래스가 서비스라는 것을 명확하게 표시 해 주기 

    때문에 서비스를 작성시에는 꼭 @Injectable() 을 넣어줍시다. 


    그리고 getHeroes() 라는 메소드를 추가 해 뒀습니다. 

    여기에 영웅목록을 불러오는 메소드를 넣을 겁니다. 

    그전에 우리는 app.component 에 들어있는 영웅목록 역시 분리해야 합니다. 

    먼저  app 폴더를 만들고 mock-heroes.ts 파일을 만듭시다.  그리고 app.component 에 있는 영웅목록 코드를

    가지고 옵시다. 


    app/mock-heroes.ts 

    
    import { Hero } from './hero';
    
    export const HEROES: Hero[] = [
      { id: 11, name: 'Mr. Nice' },
      { id: 12, name: 'Narco' },
      { id: 13, name: 'Bombasto' },
      { id: 14, name: 'Celeritas' },
      { id: 15, name: 'Magneta' },
      { id: 16, name: 'RubberMan' },
      { id: 17, name: 'Dynama' },
      { id: 18, name: 'Dr IQ' },
      { id: 19, name: 'Magma' },
      { id: 20, name: 'Tornado' }
    ];
    
    


    이전에 만들었던 Hero 모델을 import 시키고 app.component 에서 가져온 HEROES 목록을 Export 시켰습니다. 

    자 이러면 이제 영웅목록을 따로 관리하게 되었습니다. 

    여기서 app.component.ts 파일 역시 수정 할 부분이 있습니다. 

    바로 heroes = HEROES;  부분 입니다. HEROES 를 분리했으니 heroes 는 이제 Hero 모델의 영향을 받게 되도록 합니다. 

    그리고 영웅목록이니 그냥 Hero 가 아닌 Hero 객체의 배열로 받아야 겠죠? 

    그러니 heroes : Hero[]; 로 변경하도록 합시다. 


    자 이제 다시 hero.service 로 돌아와서 getHeroes() 메소드가 영웅목록을 반환 할 수 있도록 

    로직을 추가 합시다. 


    app/hero.service.ts

    
    import { Injectable } from '@angular/core';
    
    import { Hero } from './hero';
    import { HEROES } from './mock-heroes';
    
    @Injectable()
    export class HeroService {
    
      constructor() { }
      getHeroes() : Hero[] {
          return HEROES;
      }
    
    }
    


    코드를 살펴보면  먼저 Hero 모델을 Import 하고 분리해냈던 HEROES 역시 Import 하였습니다.

    그리고 getHeroes() 메소드의 return type 를 Hero 배열로 지정하고 HEROES 를 리턴하도록 하였습니다. 

    자 이제 HeroService 를 사용 할 준비가 되었습니다. 


    그전에 여기서 중요한 포인트가 나옵니다. 

    HeroService 를 사용하기 위해서 app.component 에서 HeroService 의 인스턴스를 가져와야 하는데 

    어떻게 사용해야 할까요?  기존에 사용하던 것 처럼 heroService = new HeroService() 로 해야 할까요? 

    Angular 에서는 new 연산자를 사용하지 않도록 권장하고 있습니다. 

    왜 그럴까요? 


    만약에 HeroService 의 생성자에서 새로운 파라메터를 받도록 수정되었다고 한다면? 

    해당 HeroService 를 사용하는 모든 컴포넌트를 찾아가서 생성자를 넣어주고 수정해야 합니다. 

    이는 WebService 가 커질수록 굉장히 부담되는 일이 될 것 입니다. 

    추가로 컴포넌트마다 new 를 사용한다면 해당 HeroService 객체는 무수히 생겨날 것 입니다. 

    그리고 해당 객체를 사용하여 받아온 Heroes 를 다른 사용자와 공유할려면 어떻게 해야 할까요? 

    마지막으로 해당 서비스 객체를 생성하는 과정이 컴포넌트에서 구현되기 때문에 어떻게 생성되는지 

    파악하기 쉽지 않습니다. 


    이를 Angular 에서는 생성자를 이용하는 것으로 해결합니다. 

    일단 코드를 추가하고 설명합니다. 

    app.component 로 가서 코드를 추가합시다. 



    app/app.component.ts

    
    import { Component } from '@angular/core';
    import { Hero } from './hero';
    
    import { HeroService } from './hero.service';
    
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
       constructor(private heroservice : HeroService){}
    
       title = 'Tour of Heroes'; 
       heroes : Hero[] = this.heroservice.getHeroes();
       selectedHero: Hero; 
       onSelect(hero:Hero) : void { 
         this.selectedHero = hero; 
      }
    
    }
    
    


    먼저 costructor , 즉 생성자를 만들어서 매개변수로 private heroservice 를 만들었고 타입은 HeroService 객체를 

    따르도록 했습니다. 

    그리고 heroes 는 heroservice 의 getHeroes() 메소드를 사용하여 영웅목록을 받아왔습니다. 

    이제 heroes 변수는 영웅목록을 리턴 받았습니다.  

    한번 제대로 동작하는지 테스트 해보겠습니다. 


    네 에러가 납니다. ㅠ 

    이유는 현재 생성자로 heroservice 매개변수가 HeroService 를 정의하도록 하였지만 생성자는 이게 다 입니다. 

    Angular 의 Injector 는 현재 HeroService 를 만드는 방법을 모릅니다.  

    지금 요리를 해야 되는데 레시피는 가지고 있으면서 , 해당 레시피를 어디서 어떻게 사용해야 하는지 모르고 있는 겁니다 !!! 

    그래서 우리는 이를 가르쳐야 합니다. 

    그러기 위해서는 app.module.ts 파일로 가서 providers 에 해당 서비스를 넣어줍시다. 

    
    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { HttpModule } from '@angular/http';
    
    import { AppComponent } from './app.component';
    import { HeroDetailComponent } from './hero-detail/hero-detail.component';
    import { HeroService } from './hero.service';
    
    @NgModule({
      declarations: [
        AppComponent,
        HeroDetailComponent
      ],
      imports: [
        BrowserModule,
        FormsModule,
        HttpModule
      ],
      providers: [
        HeroService
      ],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    


    상단에 HeroService 를 Import 하고  providers 배열에 HeroService 를 넣어줬습니다. 

    이제 Angular 는 HeroService 라는 요리의 레시피를 알고 해당레시피를 사용하는 곳에 정확하게 

    해당 레시피를 제공 할 것 입니다. 

    이제 다시 테스트 해보면 


    이제 제대로 작동되는 것을 확인 할 수 있습니다. 


    자 여기서 현재 HeroService 의 getHeroes() 메소드는 로직에서 그냥 들어가 있습니다. 

    문제 없이 컴포넌트가 로딩되고 getHeroes() 메소드가 실행 되게 하려면 어떻게 해야 될까요? 

    생성자에서 불러주는 건 좋지 않습니다. 

    생성자는 매개변수와 같은 간단한 초기화만 하고, 서버호출과 같은 무거운 작업은 분리해서 

    실행해야 합니다. 

    여기서 Angular 의 라이프사이클 hook 을 사용할 것 입니다. 


    Angular 의 라이프 사이클은 

    해당 그림처럼 되어 있습니다. 

    영어만 봐도 대충 감이 올 것 입니다. 

    연한 색깔들은 상태 체크에 관련되었고  진한 색깔은 초기화와 관련되어 있습니다. 

    간단히 설명하면 


       constructor   :   생성자 

       ngOnChanges  :  속성 바인딩의 상태가 변경될 때 호출

       ngOnInit :  컴포넌트와 지시자가 초기화 되었을 때 호출

       ngDoCheck :  Angular 가 자체적으로 감지할 수 없는 변경사항을 확인합니다. 

       ngAfterContentInit : Content 가 컴포넌트 뷰에 입력되고 호출 

       ngAfterContentChecked() : Content 가 초기화 된 뒤 외부 콘텐츠를 점검한 뒤에 호출 

       ngAfterViewInit : 컴포넌트의 뷰 부분이 초기화 되고 호출

       ngAfterViewChecked : 컴포넌트의 뷰와 자식 뷰를 검사하고 호출 

       ngOnDestory : 파.괴.한.다  그전에 호출 


    이렇습니다. 자세한건 공식홈페이지를 활용하세요 ~ 


    어쨌든 여기서 getHeroes() 메소드를 사용할 곳을 찾아봅시다. 

    영웅목록은 뷰에 나타나야 하니 뷰에 데이터가 들어가기전 데이터를 줘야 하니 ngAfterContentInit 전 의 초기화 

    즉 ngOnInit 에 넣어줘야 겠네요. 

    app.component 로 가서 코드를 수정합시다. 


    app/app.component.ts 

    
    import { Component , OnInit} from '@angular/core';
    import { Hero } from './hero';
    
    import { HeroService } from './hero.service';
    
    
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent implements OnInit{
       constructor(private heroservice : HeroService){}
    
       title = 'Tour of Heroes'; 
       heroes : Hero[];
       selectedHero: Hero; 
       onSelect(hero:Hero) : void { 
         this.selectedHero = hero; 
      }
    
      ngOnInit() : void {
         this.heroes = this.heroservice.getHeroes();
      }
    
    
    }
    


    상단에서 Component 와 함께 OnInit 를 import 시켜주고 

    로직 클래스에서 implements 를 통해 OnInit 인터페이스를 가져왔습니다. 

    그러면 해당 메소드를 구현해야 겠죠?  

    클래스 하단에 ngOnInit() 을 구현하고  heroes 를 가져오는 로직을 추가 하였습니다. 

    자 이제 서비스도 사용했도 다 되었습니다. 야호 

    하지만 여기서 또 문제점이 있습니다. 

    현재 getHeroes() 는 mock 데이터에서 가져오는 중이지만 추후에 http 메소드를 사용하여 서버에서 데이터를 

    가지고 올 것 입니다.  이때 동기식으로 데이터를 가져오게 되면 해당 데이터의 응답이 느릴 경우 

    사용자가 다른작업을 할 수 없고 기다려야 한다는 단점이 있습니다. 


    이런 현상을 방지하기 위해 우리는 비동기식으로 코드를 바꿔야 합니다. 

    여기서 Javascript 의 문법인 Promise 를 사용할 것 입니다. 

    Promise 가 뭔지 모른다구요?  그럼 Promise 에 관해 설명을 해드리겠습니다 하기에는 너무 많아요. 

    ( Promise  <-- 해당 링크를 확인하세요. )

    간단히 말해서 해당 작업이 완료되면 리턴해줄테니까 콜백을 맡겨놓고가 라는 겁니다.(?)

    일단 써봅시다. 

    먼저 hero.service 를 promise 를 사용하도록 변경해 봅시다. 


    app/hero.serivce.ts 

    
    import { Injectable } from '@angular/core';
    
    import { Hero } from './hero';
    import { HEROES } from './mock-heroes';
    
    @Injectable()
    export class HeroService {
    
      constructor() { }
      getHeroes() : Promise<Hero[]> {
          return Promise.resolve(HEROES);
      }
    
    }
    
    


    먼저 getHeroes() 메소드의 리턴 타입을 Promise<Hero[]> 배열로 변경하고 

    resolve 결과로 HEROES 를 반환하고 있습니다. 

    이제 app.component 에서 getHeroes() 메서드를 사용할 때 promise 를 받는 방식으로 변경해야 합니다. 

    app.component 파일을 수정합시다. 


    app/app.component.ts

    
    import { Component , OnInit} from '@angular/core';
    import { Hero } from './hero';
    
    import { HeroService } from './hero.service';
    
    
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent implements OnInit{
       constructor(private heroservice : HeroService){}
    
       title = 'Tour of Heroes'; 
       heroes : Hero[];
       selectedHero: Hero; 
       onSelect(hero:Hero) : void { 
         this.selectedHero = hero; 
      }
    
      ngOnInit() : void {
         this.heroservice.getHeroes()
         .then(result => this.heroes = result);
      }
    
    
    }
    
    


    바뀐 부분을 봅시다. 

    먼저 this.heroservice.getHeroes() 메소드를 불러오며 뒤에서 .then 메소드가 사용됩니다. 

    이는 promise 정상적으로 결과를 반환했을때 , 즉 현재로서는 heroservice 에서 return 된 resolve(HEROES) 가

     정상적으로 return 되었을때 .then 으로 넘어갑니다. 

    그리고 then 의 결과로 받은 result (=HEROES) 는 현재 heroes 변수로 대입됩니다. 

    대충 이해가 가시나요? 이해가 안되면 promise 에 대해 조금 공부하면 쉽게 이해 될 겁니다. 흐흐 

    이렇게 해서 테스트 해보면 정상적으로 작동하는 것을 볼 수 있습니다. 


    이번장은 여기 까지 입니다. 

    점점 Angular 의 주요요소들이 나오고 있습니다.

    다음장은 Routing 으로서 SPA의 꽃으로 라우터를 도입하여 컴포넌트를 경로에 맞게 

    재조립하는 걸 연습합니다. 

    끝 






    'Angular' 카테고리의 다른 글

    9장) Angular 2 -HeroEditor- (5-2)  (2) 2017.02.04
    8장) Angular 2 -HeroEditor- (5-1)  (2) 2017.01.14
    6장) Angular 2 -HeroEditor- (3)  (1) 2017.01.10
    5장) Angular 2 -HeroEditor- (2)  (7) 2017.01.08
    4장) Angular 2 -HeroEditor- (1)  (1) 2017.01.05

    댓글