Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

[FEConf Korea 2017]Angular 컴포넌트 대화법

1,272 views

Published on

FEConf Korea 2017(https://2017.feconf.kr/)에서 발표한 Angular 컴포넌트간의 데이터를 어떻게 전달받는지에 대한 내용입니다.

Published in: Technology
  • Be the first to comment

[FEConf Korea 2017]Angular 컴포넌트 대화법

  1. 1. Angular 컴포넌트 대화법 Jeado Ko
  2. 2. +jeado.ko (고재도) haibane84@gmail.com - “Google Developer Expert” WebTech - “Kakao Bank 빅데이터 파트” Developer
  3. 3. 질문이 있습니다!
  4. 4. 컴포넌트
  5. 5. ● 명세specification 를 가진 재사용할 수 있는reusable 소프트웨어 구성요소 (위키피디아) ● 웹 애플리케이션의 기본 구성요소로 HTML 요소들을 포함 ● 독립된 구성요소로 뷰와 로직으로 구성됨 ● 컴포넌트들은 단방향 트리형태로 구성되고 최상위 루트 컴포넌트가 존재 Public API 사진 출처: https://v1.vuejs.org/guide/overview.html 컴포넌트 개요
  6. 6. Angular의 Hello World 컴포넌트 import { Component } from '@angular/core' ; @Component({ selector: 'my-hello-world' , template: '<h1>{{title}}</h1>' , styles: ['h1 { color: red }' ] }) export class HelloWorldComponent { title = 'Hello World!!' ; }
  7. 7. 컴포넌트 계층구조간 커뮤니케이션 부모 컴포넌트 자식 컴포넌트 부모 컴포넌트 자식 컴포넌트 부모 컴포넌트 자식 컴포넌트 자식 컴포넌트 손주 컴포넌트 부모 컴포넌트 자식 컴포넌트 자식 컴포넌트
  8. 8. 컴포넌트 계층구조간 커뮤니케이션 부모 컴포넌트 자식 컴포넌트 부모 컴포넌트 자식 컴포넌트
  9. 9. 컴포넌트 커뮤니케이션 (부모 → 자식) 부모 컴포넌트 자식 컴포넌트 TodosComponent TodoComponent
  10. 10. ● 자식컴포넌트에서 부모가 전달할 속성에 @Input() 데코레이터를 사용 컴포넌트 커뮤니케이션 (부모 → 자식) import {Component, Input, OnInit} from '@angular/core'; import {Todo} from '../../share/todo.model'; @Component({ selector: 'app-todo', template: ` <input type="checkbox" [checked]="todo.done"> <label>{{ todo.text }}</label> `, styles: [`...`] // 생략 }) export class TodoComponent { @Input() todo: Todo; constructor() { } }
  11. 11. ● 부모 컴포넌트에서는 속성 바인딩을 통해 데이터 전달 컴포넌트 커뮤니케이션 (부모 → 자식) <!-- todos.component.html 일부 --> <div *ngFor="let todo of todos" > <app-todo [todo]="todo"></app-todo> </div> // todos.compotonent.ts @Component({ selector: 'app-todos' , templateUrl: './todos.component.html' , styleUrls: ['./todos.component.css' ] }) export class TodosComponent implements OnInit { todos: Todo[]; constructor () { this.todos = [ { done: false, text: '운동하기' }, { done: true, text: '공부하기'} ]; } |
  12. 12. ● 자식 컴포넌트에서 @Input을 Getter/Setter에 사용 컴포넌트 커뮤니케이션 (부모 → 자식) @Component({ // 생략 }) export class TodoComponent { private _todo: Todo; get todo(): Todo { return this._todo; } @Input() set todo(v: Todo) { this._todo = v; v.text += " !!!"; } constructor() {} }
  13. 13. ● 부모컴포넌트에서 자식 컴포넌트 인스턴스를 @ViewChild()로 가져옴 컴포넌트 커뮤니케이션 (부모 → 자식) <!-- todos.component.html 일부 --> <div class="title"> <app-title></app-title> <h2>{{ today | date:'M월 d일' }}</h2> </div> <!-- todos.component.ts 일부 --> export class TodosComponent implements OnInit { // 생략 @ViewChild(TitleComponent) titleComp :TitleComponent; ngOnInit() { this.titleComp.text = '나의 하루' } }
  14. 14. 컴포넌트 커뮤니케이션 (자식 → 부모) 부모 컴포넌트 자식 컴포넌트 TodosComponent AddTodoComponent
  15. 15. ● 자식컴포넌트에서 EventEmitter를 통해 부모가 전달 받을 이벤트를 발생하는 속성에 @Output() 데코레이터를 사용 컴포넌트 커뮤니케이션 (자식 → 부모) @Component({ selector: 'app-add-todo' , template: `<button (click)="btnClicked(newText)">+</button> <input type="text" placeholder=" 할 일 추가" [(ngModel)]="newText">` , styles: ['...'] // 생략 }) export class AddTodoComponent { @Output() onTodoAdded = new EventEmitter(); newText: string; constructor () { } btnClicked(newText: string) { this.onTodoAdded .emit(newText); this.newText = ''; } }
  16. 16. ● 부모 컴포넌트는 $event로 이벤트의 데이터를 전달 받음 컴포넌트 커뮤니케이션 (자식 → 부모) <!-- todos.component.html 일부 --> <div> <app-add-todo (onTodoAdded)="addTodo($event)"></app-add-todo> </div> <!-- todos.component.ts 일부 --> export class TodosComponent { // 생략 addTodo(text: string) { this.todos.push({done : false, text}); } }
  17. 17. ● 자식컴포넌트에서 부모컴포넌트를 주입받음 컴포넌트 커뮤니케이션 (자식 → 부모) @Component({ selector: 'app-add-todo' , template: `<button (click)="btnClicked(newText)">+</button> <input type="text" placeholder=" 할 일 추가" [(ngModel)]="newText">` , styles: ['...'] // 생략 }) export class AddTodoComponent { @Output() onTodoAdded = new EventEmitter (); newText: string; constructor(private todosComponent: TodosComponent) { } btnClicked(newText: string) { // this.onTodoAdded.emit(newText); this.todosComponent.addTodo(newText); this.newText = ''; } }
  18. 18. 부모 컴포넌트 자식 컴포넌트 자식 컴포넌트 부모 컴포넌트 자식 컴포넌트 자식 컴포넌트 손주 컴포넌트
  19. 19. AppComponent CartComponentHomeComponent ProductComponent ProductComponent
  20. 20. 라우터를 연결하고 다른 모듈을 넣었다면?! <!-- app.component.html --> <app-drawer #drawer> <app-cart (onClose)="drawer.close()"></fc-cart> </app-drawer> <app-navi></app-navi> <main [ngClass]="{'m-t-main': !isHome}"> <router-outlet></router-outlet> </main>
  21. 21. AppComponent CartComponent HomeComponent ProductComponent ProductComponent RouterOutlet App Module Home Module Route { path: 'home', component: HomeComponent }
  22. 22. 서비스를 활용한 커뮤니케이션 부모 컴포넌트 자식 컴포넌트 자식 컴포넌트 서비스 부모 컴포넌트 자식 컴포넌트 자식 컴포넌트 손주 컴포넌트 서비스
  23. 23. ● CartService를 통하여 카트아이템 배열CartItem[] 을 구독 CartComponent @Component({ selector: 'app-cart', templateUrl: './cart.component.html' , styleUrls: ['./cart.component.css' ] }) export class CartComponent { cart: CartItem[] = []; constructor (private cartService : CartService ) { this.cartService .cartItems .subscribe(v => this.cart = v) } remove(cartItem: CartItem) { this.cartService .remove(cartItem); } // 생략 } Observable<CartItem[]>
  24. 24. ● CartService를 통하여 카트아이템 배열CartItem[] 을 구독 HomeComponent @Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.css'] }) export class HomeComponent { constructor( private cartService: CartService) { } addCart(product: Product) { this.cartService.addCart(product); } // 생략 }
  25. 25. ● BehaviorSubject를 이용하여 로컬스토리지의 초기 로드값이나 마지막 값을 발행 CartService (1) @Injectable() export class CartService { private _items: CartItem[] = []; private cartSubject: BehaviorSubject<CartItem[]>; public cartItems: Observable<CartItem[]>; constructor() { const itemJson = localStorage.getItem(storageKey) if (itemJson) this._items = JSON.parse(itemJson); this.cartSubject = new BehaviorSubject(this._items); this.cartItems = this.cartSubject.asObservable(); } // 생략 }
  26. 26. CartService (2) @Injectable() export class CartService { // 생략 addCart(product: Product) { const foundProduct = this._items.find(c => c.product.id === product.id); if (foundProduct) foundProduct.counts += 1; else this._items.push({ product, counts: 1 }); this.updateLocalStorage(this._items); this.cartSubject.next(this._items); } private updateLocalStorage(cartItems: CartItem[]) { localStorage.setItem(storageKey, JSON.stringify(cartItems)); } }
  27. 27. CartService (3) @Injectable() export class CartService { // 생략 remove(cartItem: CartItem) { const foudnItem = this.cart.find(v => v.product.id === cartItem.product.id) if (foudnItem && foudnItem.counts > 1) { foudnItem.counts -= 1; } else { const index = this.cart.indexOf(foudnItem); this.cart.splice(index, 1); } this.updateLocalStorage(); this.cartSubject.next(this.cart); } }
  28. 28. 하지만 서비스와 컴포넌트가 아주 많아지면? 부모 컴포넌트 자식 컴포넌트 자식 컴포넌트 서비스 부모 컴포넌트 자식 컴포넌트 자식 컴포넌트 손주 컴포넌트 서비스 서비스 서비스 서비스 서비스 서비스 서비스 서비스 서비스 서비스 서비스
  29. 29. 자바스크립트 앱을 위한 예측가능한 상태 컨테이너 PREDICTABLE STATE CONTAINER FOR JAVASCRIPT APPS
  30. 30. WITHOUT REDUX ● 컴포넌트간 직접적인 통신 (속성 바인딩, eventEmitter 활용)
  31. 31. WITH REDUX ● 컴포넌트간의 직접 통신이 없다. ● 스토어를 통한 단 하나의 상태로 관리
  32. 32. REDUX Store 1. 컴포넌트에서 액션action 을 보냄dispatch 2. 스토어는 변화를 적용한다. 3. 컴포넌트는 관련된 상태를 전달받는다. (subscribe에 의해서)
  33. 33. REDUX Reducer (state, action) => state
  34. 34. Actions Reducers Store View (Component) subscribe change state dispatch
  35. 35. angular-reduxreact-redux ngrx
  36. 36. ● @ngrx - Reactive Extensions for Angular ngrx (https://ngrx.github.io/)
  37. 37. 참고 : https://gist.github.com/btroncone/a6e4347326749f938510
  38. 38. Setup ● npm install @ngrx/store --save후 StoreModule 모듈 임포트 import { NgModule } from '@angular/core' import { StoreModule } from '@ngrx/store'; import { counterReducer } from './counter'; @NgModule({ imports: [ BrowserModule, StoreModule.forRoot({ counter: counterReducer }) // ActionReducerMap 전달 ] }) export class AppModule {}
  39. 39. Reducer (counter.reducer.ts) import { Action } from'@ngrx/store'; import * as CounterActions from './coutner.actions'; export function counterReducer(state: number = 0, action: CounterActions.All): number { switch(action.type) { case CounterActions.INCREMENT: return state + 1; case CounterActions.DECREMENT: return state - 1; case CounterActions.RESET: return action.payload default: return state; } }
  40. 40. Action (counter.actions.ts) import { Action } from '@ngrx/store'; export const INCREMENT = '[Counter] Increment'; export const DECREMENT = '[Counter] Decrement'; export const RESET = '[Counter] Reset'; export class Increment implements Action { readonly type = INCREMENT; } export class Decrement implements Action { readonly type = DECREMENT; } export class Reset implements Action { readonly type = RESET; constructor(public payload: number) {} } export type All = Increment | Decrement | Reset;
  41. 41. Component export interface AppState { counter: number } @Component({ selector: 'my-app', template: ` <button (click)="increment()">Increment</button> <div>Current Count: {{ counter | async }}</div> <button (click)="decrement()">Decrement</button> <button (click)="reset()">Reset Counter</button> `}) export class CounterComponent { counter: Observable<number>; constructor (private store: Store<AppState>) { this.counter = store.select('counter'); } increment(){ this.store.dispatch(new Counter.Increment()); } decrement(){ this.store.dispatch(new Counter.Decrement()); } reset(){ this.store.dispatch(new Counter.Reset(1)); } }
  42. 42. DashboardComponent TableComponentGraphComponent
  43. 43. AppModule import { StoreModule } from '@ngrx/store'; import * as fromRoot from './store'; @NgModule({ imports: [ CommonModule, StoreModule.forRoot(fromRoot.reducers, {initialState: fromRoot.getInitialState()}), ], providers: [ // 생략 ], declarations: [] }) export class AppModule { }
  44. 44. import * as fromSales from './sales/sales.reducer' ; import * as fromOrder from './order/order.reducer' ; export interface AppState { sales: fromSales.State; orders: fromOrder.OrderState; } export const initialState : AppState = { sales: fromSales.initialState , orders: fromOrder.initialState }; export function getInitialState (): AppState { return initialState ; } export const reducers: ActionReducerMap <AppState> = { sales: fromSales.reducer, orders: fromOrder.reducer }; store/index.ts
  45. 45. import { Order } from '../../models/order.model'; import * as moment from 'moment'; export interface OrderState { orders: Order[]; selectedOrder: Order; from: Date; to: Date; } export const initialState: OrderState = { orders: [], selectedOrder: null, from: moment().toDate(), to: moment().startOf('day').toDate() }; order/order.reducer.ts
  46. 46. export const SEARCH_ORDERS = '[Order] SEARCH_ORDERS'; export const SELECT_ORDER = '[Order] SELECT_ORDER'; export const RESET = '[Order] RESET'; export class SearchOrder implements Action { readonly type = SEARCH_ORDERS; } export class SelectOrder implements Action { readonly type = SELECT_ORDER; constructor(public order: Order) {} } export class Reset implements Action { readonly type = RESET; } export type All = ReqSearchOrders | SearchOrder | SelectOrder | Reset; order/order.action.ts
  47. 47. import * as orderActions from './order.action'; export function reducer(state = initialState, action: orderActions.All): OrderState { switch (action.type) { case orderActions.SEARCH_ORDERS: return Object.assign({}, state, { orders: [ /* 오더 데이터 생략… */ ]); case orderActions.SELECT_ORDER: return Object.assign({}, state, { selectedOrder: action.order }); case orderActions.RESET: return Object.assign({}, state, { selectedOrder: null }); default: return state; } } order/order.reducer.ts
  48. 48. @Component({ selector: 'app-table' , templateUrl: './table.component.html' , styleUrls: ['./table.component.scss' ] }) export class TableComponent implements OnInit { orders: Observable<Order[]>; selectedOrder : Order; constructor (private store: Store<AppState>) { this.orders = store.select(v => v.orders.orders) this.store.select(v => v.orders.selectedOrder ) .subscribe(v => this.selectedOrder = v); } select(p: Order) { if (p === this.selectedOrder ) { this.store.dispatch(new orderActions .Reset()); } else { this.store.dispatch(new orderActions .SelectOrder (p)); } } } TableComponent
  49. 49. @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss'] }) export class DashboardComponent implements OnInit { constructor(private store: Store<AppState>) { } ngOnInit() { this.store.dispatch(new SearchOrders()) this.store.dispatch(new GetTotalSales()) } } DashboardComponent
  50. 50. 코드에 박힌 데이터 말고 실제 서비스는 어떻게 하는데…
  51. 51. @ngrx/effects 출처: https://blog.nextzy.me/manage-action-flow-in-ngrx-with-ngrx-effects-1fda3fa06c2f
  52. 52. Actions Reducers Store View (Component) subscribe change state dispatch Effect @ngrx/effects flow Service subscribe dispatch
  53. 53. export const REQ_SEARCH_ORDERS = '[Order] REQ_SEARCH_ORDERS'; export const REQ_SEARCH_SUCCESS = '[Order] REQ_SEARCH_SUCCESS'; export const SELECT_ORDER = '[Order] SELECT_ORDER'; export const RESET = '[Order] RESET'; export class ReqSearchOrders implements Action { readonly type = REQ_SEARCH_ORDERS; constructor(public orderNumber?: string) {} } export class ReqSearchSuccess implements Action { readonly type = REQ_SEARCH_SUCCESS; constructor(public orders: Order[]) {} } // 생략 order/order.action.ts
  54. 54. export function reducer(state = initialState, action: orderActions.All): OrderState { switch (action.type) { case orderActions.REQ_SEARCH_ORDERS: return state; case orderActions.REQ_SEARCH_SUCCESS: return Object.assign({}, state, { orders: action.orders }); case orderActions.SELECT_ORDER: return Object.assign({}, state, { selectedOrder: action.order }); case orderActions.RESET: return Object.assign({}, state, { selectedOrder: null }); default: return state; } } order/order.reducer.ts
  55. 55. import { Effect, Actions } from '@ngrx/effects'; import * as orderAction from './order.action'; @Injectable() export class OrderEffects { @Effect() request$: Observable<Action> = this.actions$ .ofType<orderAction.ReqSearchOrders>(orderAction.REQ_SEARCH_ORDERS) .map(v => v.orderNumber) .mergeMap(num => { return this.orderService.getOrders(num) .map((orders: Order[]) => new orderAction.ReqSearchSuccess(orders)) .catch(() => Observable.of(new orderAction.ReqSearchSuccess([]))); }); constructor(private actions$: Actions, private orderService: OrderService) { } } order/order.effects.ts
  56. 56. import * as salesAction from '../sales/sales.action'; @Effect() forward$: Observable<Action> = this.actions$ .ofType<orderAction.SelectOrder|orderAction.Reset>(orderAction.SELECT_ORDER, orderAction.RESET) .map((a) => { if (a instanceof orderAction.SelectOrder) { return a.order.name; } else { return null; } }) .map(name => new salesAction.ReqGivenItemSales(name)); order/order.effects.ts
  57. 57. import * as salesAction from '../sales/sales.action'; @Effect() forward$: Observable<Action> = this.actions$ .ofType<orderAction.SelectOrder|orderAction.Reset>(orderAction.SELECT_ORDER, orderAction.RESET) .map((a) => { if (a instanceof orderAction.SelectOrder) { return a.order.name; } else { return null; } }) .map(name => new salesAction.ReqGivenItemSales(name)); order/order.effects.ts // TableComponent select(p: Order) { if (p === this.selectedOrder) { this.store.dispatch(new orderActions.Reset()); } else { this.store.dispatch(new orderActions.SelectOrder(p)); } } }
  58. 58. sales/sales.effects.ts @Injectable() export class SalesEffects { @Effect() requestByItem$: Observable<Action> = this.actions$ .ofType<ReqGivenItemSales>(salesAction.REQ_GIVEN_ITEM_SALES) .map(v => v.itemNum) .mergeMap(itemName => { return this.selesService.getSales(itemName) .map((sales: Sales[]) => new salesAction.ReqGetSalesSuccess(sales)) .catch(() => Observable.of(new salesAction.ReqGetSalesSuccess([]))); }); constructor(private actions$: Actions, private selesService: SalesService) { } }
  59. 59. Sidebar 모듈! 라우팅이 존재!
  60. 60. sidebar-routing.module.ts const routes: Routes = [{ path: 'events', component: EventListComponent }, { path: 'events/:num', component: EventDetailComponent }, { path: '', redirectTo: 'events' }]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class SidebarRoutingModule { }
  61. 61. @ngrx/router-store
  62. 62. /** * Payload of ROUTER_NAVIGATION. */ export declare type RouterNavigationPayload<T> = { routerState: T; event: RoutesRecognized; }; /** * An action dispatched when the router navigates. */ export declare type RouterNavigationAction<T = RouterStateSnapshot> = { type: typeof ROUTER_NAVIGATION; payload: RouterNavigationPayload<T>; };
  63. 63. EventListComponent @Component({ selector: 'app-event-list', templateUrl: './event-list.component.html', styleUrls: ['./event-list.component.scss'] }) export class EventListComponent { orderEvents: Observable<OrderEvent[]> selectedOrderEvent: OrderEvent; constructor(private store: Store<AppState>, private route: ActivatedRoute) { this.orderEvents = store.select(v => v.events.orderEvents) } }
  64. 64. event-list.component.html 일부 <ul class="sidebar-list"> <li *ngFor="let event of orderEvents | async" [routerLink]="[event.orderNumber]"> <a> <span class="label label-primary pull-right">NEW</span> <h4>주문번호: {{event.orderNumber}}</h4> {{event.text}} <div class="small">수량: {{event.salesNumber}}</div> <div class="small text-muted m-t-xs">판매시간 - {{event.date | date:'medium'}}</div> </a> </li> </ul>
  65. 65. EventEffects는 에디터에서 보겠습니다.
  66. 66. 감사합니다.
  67. 67. ● https://css-tricks.com/learning-react-redux/ ● https://github.com/ngrx/platform ● https://gist.github.com/btroncone/a6e4347326749f938510 ● https://blog.nextzy.me/manage-action-flow-in-ngrx-with-ngrx-effects-1fda3fa06c2f reference

×