라우팅 테이블 설계
지금까지는 메뉴가 하나였다면 메뉴를 하나 더 추가하고 hero-detail 화면도 자식 컴포넌트가 아니라 별도의 메뉴 화면으로 분리하겠다. 라우팅은 SPA의 핵심이다. 왜 라우팅을 구성해야 하는지는 back 버튼과 즐겨찾기 를 생각해본다면 답을 찾을수 있을것이다.
먼저 home, todo 화면을 만들고 라우팅 테이블을 설계한다.
1 2 3 |
ng g component home ng g component todo |
1 2 3 4 5 6 7 8 9 10 11 12 |
const routes: Routes = [ {path: '', component: HomeComponent}, {path: 'heroes', component: HeroesComponent}, {path: 'todo', component: TodoComponent} ]; imports: [ BrowserModule, FormsModule, NgbModalModule.forRoot(), RouterModule.forRoot(routes) ], |
그리고 component를 담을 router-outlet을 추가한다.
1 2 3 |
<router-outlet></router-outlet> <!--<app-votetaker></app-votetaker>--> |
주소창에 http://localhost:4200 그담에 /, /todo, /heroes 유알엘을 테스트해보자.
브라우저에서 /todo 로 접근하게 되면 root component가 로딩이 되고 root component의 router-outlet에 해당 되는 패스의 TodoComponent에 주입이 된다는 것을 기억하자
내비게이션 바 추가
bootstrap으로 메뉴바 구성
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<nav class="navbar navbar-expand-sm bg-dark navbar-dark justify-content-between"> <a class="navbar-brand" href="#">TOH</a> <ul class="navbar-nav"> <li class="nav-item"> <a class="nav-link" href="#">todo</a> </li> <li class="nav-item"> <a class="nav-link" href="#">heroes</a> </li> </ul> </nav> <router-outlet></router-outlet> |
angular router module에서 제공하는 링크를 연결한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<nav class="navbar navbar-expand-sm bg-dark navbar-dark justify-content-between"> <a class="navbar-brand" href routerLink="/">TOH</a> <ul class="navbar-nav"> <li class="nav-item" routerLinkActive="active"> <a class="nav-link" href routerLink="/todo">todo</a> </li> <li class="nav-item" routerLinkActive="active"> <a class="nav-link" href routerLink="/heroes">heroes</a> </li> </ul> </nav> <router-outlet></router-outlet> |
routerLinkActive는 해당 링크가 활성화가 되었을때 class=”active” 를 추가해준다.
active에 대한 스타일링을 추가하기만 하면 된다.
1 2 3 |
li.active { font-weight: bold; } |
hero-detail 라우팅 테이블 설계
먼저 왜 hero 상세화면을 라우팅으로 구성하는지 생각해보자.
예를 들어, 1번 hero를 보고, 2번, 3번 을 본 다음 이전 화면으로 돌아가기 위해서 back 버튼을 눌렀다면 3번화면에서 2번으로 돌아가는게 아니라 이전 화면으로 돌아간다. 직접 테스트해보자.
그래서 또한 2번 상세화면을 보다가 사용자가 즐겨찾기를 했다가 나중에 다시 돌아오면 heroes 화면으로 돌아오지 2번 화면으로 돌아올수가 없다. 라우팅 path가 지정되어있지 않기 때문이다.
hero-detail 컴포넌트는 이미 만들어져 있으니 라우팅 테이블을 만들자.
먼저 별도의 화면으로 만든다음 heroes 컴포넌트의 자식으로 들어가는 SPA 구조로 만들어 본다.
1 2 3 4 5 6 |
const routes: Routes = [ {path: '', component: HomeComponent}, {path: 'heroes', component: HeroesComponent}, {path: 'todo', component: TodoComponent}, {path: 'detail/:hero_id', component: HeroDetailComponent} ]; |
hero 리스트 클릭시 routerLink 추가
1 2 3 4 5 6 7 |
<ul> <li class="d-flex m-1" *ngFor="let hero of heroes" (click)="onSelect(hero)" [class.selected]="hero===selectedHero" routerLink="/detail/{{hero.id}}"> <span class="rounded-left p-2 bg-primary text-light">{{hero.id}}</span> <div class="rounded-right p-2 bg-light text-dark w-50">{{hero.name}}</div> </li> </ul> |
이제 라우팅을 테스트해본다. 상단 url 의 path가 정상적으로 바뀌는지 확인한다.
– /detail/11 의 hero id 추출하기
url 뒤에 ?key=value 형태는 Query parameter 라고 하고 유알엘 형태로 붙는 형태는 parameter라고 한다.
이 파라메터는 observable 형태로 추출한다.
1 2 3 4 5 6 |
constructor(private route: ActivatedRoute) { this.route.params .subscribe(params => { console.log(params); }); } |
콘솔에 찍어보면 {hero_id: “11”} 형태로 찍히는 것을 볼수 있다. 넘어오는 데이터는 json 이며 라우팅테이블 구성시 지정한 hero_id가 넘어오며 value는 스트링값으로 넘어온다.
service에서 해당 id를 추출하는 메서드를 추가한다.
1 2 3 |
getHero(hero_id: number): Observable<Hero> { return of(HEROES.find(hero => hero.id === hero_id)); } |
detail 컴포넌트에서 parameter를 추출후 서비스를 호출해서 데이터를 획득하자.
1 2 3 4 5 6 7 8 9 10 11 12 |
constructor(private route: ActivatedRoute, private heroService: HeroService) { this.route.params .subscribe(params => { console.log(params); this.getHero(+params['hero_id']); }); } getHero(id: number) { this.heroService.getHero(id) .subscribe(hero => this.selectedHero = hero); } |
– 목록위에 상세화면을 표시하기
목록이 아래에 표시되고 상단에 상세화면을 표시하기 위해서는 상세화면이 목록의 자식이 되어야 한다.
먼저 부모역할을 하는 heroes에 router-outlet을 추가한다.
1 2 3 |
<div class="mb-3"> <router-outlet></router-outlet> </div> |
라우팅 테이블 설계시 heroes의 자식으로 detail을 구성한다. detail을 생략하고 :hero_id만 넣으면
url은 부모인 /heroes + 자식인 /hero_id 가 된다.
예) /heroes/1 (1번 아이디)
1 2 3 4 5 6 7 |
const routes: Routes = [ {path: '', component: HomeComponent}, {path: 'heroes', component: HeroesComponent, children: [ {path: 'detail/:hero_id', component: HeroDetailComponent} ]}, {path: 'todo', component: TodoComponent} ]; |
heroes에서 클릭시 접근할 유알엘도 변경한다.
1 2 |
<li class="d-flex m-1" *ngFor="let hero of heroes" (click)="onSelect(hero)" [class.selected]="hero===selectedHero" routerLink="/heroes/{{hero.id}}"> |
back 버튼 구현
detail 화면을 라우팅 path로 구성하면 차이점이 back이 가능하다는 것이였다. back 버튼을 구현해보자.
1 |
<button class="btn btn-info btn-sm" (click)="goBack()">back</button> |
1 2 3 |
goBack() { history.back(); } |
하단 부모 목록 리스트를 클릭하여 상단 자식 컴포넌트가 로딩되는것을 확인후에 다시 back 버튼을 눌러서 되돌아 가보자. 잘 되는거 같지만 한가지 문제가 있다. 2번에서 1번으로 돌아가면 자식 컴포넌트는 잘 로딩 되었지만 부모 목록은 그대로 2번이 active로 되어있다. 이것을 해결하기 위해서 angular 에서 제공해주는 navigation 이벤트를 이용해도 되지만 아주 범용적인 경우에는 Observable을 사용해서 해결할 수 있다.
커스텀 Pub Sub 구현
자식 목록이 바뀐건지 이벤트에 관심이 있는 컴포넌트는 부모 컴포넌트이다. 부모 컴포넌트가 subscribe가 되고 자식이 이벤트를 발생하는 publisher 가 된다.
먼저 서비스에 이벤트를 발생할수 있는 Subject를 만들고 publisher는 이 Subject를 이용해서 이벤트를 발생하고 이 이벤트에 subscribe한 컴포넌트에게 이벤트를 보낸다.
서비스
1 2 |
refresh = new Subject<number>(); // publisher: next() 함수로 데이터 발생 refresh$ = this.refresh.asObservable(); // subscriber: subscribe()로 데이터 수신 |
subscriber
1 2 3 4 5 6 7 |
constructor(private heroService: HeroService) { // subscriber this.heroService.refresh$.subscribe(data => { console.log(data); this.selectedHero = this.heroes.find(item => item.hero_id === data ? true : false); }); } |
publisher
1 2 3 4 5 6 7 8 9 |
constructor(private route: ActivatedRoute, private heroService: HeroService) { this.route.params .subscribe(params => { console.log(params); this.getHero(+params['hero_id']); // 변경된 데이터를 부모에게 전달 this.heroService.refresh.next(+params['hero_id']); }); } |
자식이 없이 부모만 호출될때 selected를 null로 만들어주기 위해서 Router 이벤트를 이용한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
constructor(private heroService: HeroService, private router: Router) { // subscriber: 자식컴포넌트가 변경됨을 감지 this.heroService.refresh$.subscribe(data => { console.log(data); this.selectedHero = this.heroes.find(item => item.hero_id === data ? true : false); }); // 부모 목록으로 되돌아 올때 감지가 안되므로 추가 this.router.events.subscribe(events => { // 부모, 자식 경로가 호출될때마다 여러가지 이벤트 발생. NavigationStart -> NavigationReconized -> NavigationEnd if (events instanceof NavigationStart) { console.log('nagigation start:' + events.url); if (events.url === '/heroes') { this.selectedHero = null; } } }); } |