[feconf korea 2017]angular 컴포넌트 대화법
TRANSCRIPT
![Page 1: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/1.jpg)
Angular 컴포넌트 대화법Jeado Ko
![Page 2: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/2.jpg)
+jeado.ko (고재도)[email protected]
- “Google Developer Expert” WebTech
- “Kakao Bank 빅데이터 파트” Developer
![Page 3: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/3.jpg)
질문이 있습니다!
![Page 4: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/4.jpg)
컴포넌트
![Page 5: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/5.jpg)
● 명세specification를 가진 재사용할 수 있는reusable 소프트웨어 구성요소 (위키피디아)● 웹 애플리케이션의 기본 구성요소로 HTML 요소들을 포함● 독립된 구성요소로 뷰와 로직으로 구성됨● 컴포넌트들은 단방향 트리형태로 구성되고 최상위 루트 컴포넌트가 존재
Public API
사진 출처: https://v1.vuejs.org/guide/overview.html
컴포넌트 개요
![Page 6: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/6.jpg)
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!!';
}
![Page 7: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/7.jpg)
컴포넌트 계층구조간 커뮤니케이션
부모
컴포넌트
자식 컴포넌트
부모
컴포넌트
자식 컴포넌트
부모 컴포넌트
자식 컴포넌트
자식 컴포넌트
손주컴포넌트
부모 컴포넌트
자식 컴포넌트
자식 컴포넌트
![Page 8: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/8.jpg)
컴포넌트 계층구조간 커뮤니케이션
부모
컴포넌트
자식 컴포넌트
부모
컴포넌트
자식 컴포넌트
![Page 9: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/9.jpg)
![Page 10: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/10.jpg)
컴포넌트 커뮤니케이션 (부모 → 자식)
부모
컴포넌트
자식 컴포넌트
TodosComponent
TodoComponent
![Page 11: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/11.jpg)
● 자식컴포넌트에서 부모가 전달할 속성에 @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() { }}
![Page 12: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/12.jpg)
● 부모 컴포넌트에서는 속성 바인딩을 통해 데이터 전달
컴포넌트 커뮤니케이션 (부모 → 자식)
<!-- 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: '공부하기'} ]; }|
![Page 13: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/13.jpg)
● 자식 컴포넌트에서 @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() {}}
![Page 14: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/14.jpg)
● 부모컴포넌트에서 자식 컴포넌트 인스턴스를 @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 = '나의 하루' }}
![Page 15: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/15.jpg)
컴포넌트 커뮤니케이션 (자식 → 부모)
부모
컴포넌트
자식 컴포넌트
TodosComponent
AddTodoComponent
![Page 16: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/16.jpg)
● 자식컴포넌트에서 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 = ''; }}
![Page 17: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/17.jpg)
● 부모 컴포넌트는 $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}); }}
![Page 18: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/18.jpg)
● 자식컴포넌트에서 부모컴포넌트를 주입받음
컴포넌트 커뮤니케이션 (자식 → 부모)
@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 = ''; }}
![Page 19: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/19.jpg)
부모 컴포넌트
자식 컴포넌트
자식 컴포넌트
부모 컴포넌트
자식 컴포넌트
자식 컴포넌트
손주컴포넌트
![Page 20: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/20.jpg)
![Page 21: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/21.jpg)
AppComponent
CartComponentHomeComponent
ProductComponent ProductComponent
![Page 22: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/22.jpg)
라우터를 연결하고 다른 모듈을 넣었다면?!
<!-- 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>
![Page 23: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/23.jpg)
AppComponent CartComponent
HomeComponent
ProductComponent ProductComponent
RouterOutlet
App Module
Home Module
Route{ path: 'home', component: HomeComponent}
![Page 24: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/24.jpg)
서비스를 활용한 커뮤니케이션
부모 컴포넌트
자식 컴포넌트
자식 컴포넌트
서비스
부모 컴포넌트
자식 컴포넌트
자식 컴포넌트
손주컴포넌트
서비스
![Page 25: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/25.jpg)
● 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[]>
![Page 26: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/26.jpg)
● 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);
}
// 생략}
![Page 27: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/27.jpg)
● 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(); } // 생략}
![Page 28: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/28.jpg)
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));
}
}
![Page 29: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/29.jpg)
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);
}
}
![Page 30: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/30.jpg)
하지만 서비스와 컴포넌트가 아주 많아지면?
부모 컴포넌트
자식 컴포넌트
자식 컴포넌트
서비스
부모 컴포넌트
자식 컴포넌트
자식 컴포넌트
손주컴포넌트
서비스
서비스서비스서비스서비스서비스
서비스서비스서비스서비스서비스
![Page 31: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/31.jpg)
![Page 32: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/32.jpg)
자바스크립트 앱을 위한 예측가능한 상태
컨테이너
PREDICTABLE STATE CONTAINERFOR JAVASCRIPT APPS
![Page 33: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/33.jpg)
WITHOUT REDUX
● 컴포넌트간 직접적인 통신
(속성 바인딩, eventEmitter 활용)
![Page 34: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/34.jpg)
WITH REDUX
● 컴포넌트간의 직접 통신이 없다.
● 스토어를 통한 단 하나의 상태로
관리
![Page 35: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/35.jpg)
REDUX Store
1. 컴포넌트에서 액션action을
보냄dispatch
2. 스토어는 변화를 적용한다.
3. 컴포넌트는 관련된 상태를
전달받는다. (subscribe에 의해서)
![Page 36: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/36.jpg)
REDUX Reducer
(state, action) => state
![Page 37: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/37.jpg)
Actions
Reducers
Store
View(Component)
subscribe
change state dispatch
![Page 38: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/38.jpg)
angular-reduxreact-redux ngrx
![Page 40: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/40.jpg)
![Page 41: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/41.jpg)
참고 : https://gist.github.com/btroncone/a6e4347326749f938510
![Page 42: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/42.jpg)
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 {}
![Page 43: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/43.jpg)
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; }}
![Page 44: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/44.jpg)
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;
![Page 45: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/45.jpg)
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)); }}
![Page 46: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/46.jpg)
![Page 47: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/47.jpg)
DashboardComponent
TableComponentGraphComponent
![Page 48: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/48.jpg)
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 { }
![Page 49: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/49.jpg)
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
![Page 50: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/50.jpg)
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
![Page 51: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/51.jpg)
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
![Page 52: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/52.jpg)
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
![Page 53: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/53.jpg)
@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
![Page 54: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/54.jpg)
@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
![Page 55: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/55.jpg)
코드에 박힌 데이터 말고 실제 서비스는 어떻게 하는데…
![Page 56: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/56.jpg)
@ngrx/effects
출처: https://blog.nextzy.me/manage-action-flow-in-ngrx-with-ngrx-effects-1fda3fa06c2f
![Page 57: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/57.jpg)
Actions
Reducers
Store
View(Component)
subscribe
change statedispatch
Effect
@ngrx/effects flow
Service
subscribe
dispatch
![Page 58: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/58.jpg)
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
![Page 59: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/59.jpg)
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
![Page 60: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/60.jpg)
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
![Page 61: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/61.jpg)
![Page 62: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/62.jpg)
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
![Page 63: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/63.jpg)
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
// TableComponentselect(p: Order) { if (p === this.selectedOrder) { this.store.dispatch(new orderActions.Reset()); } else { this.store.dispatch(new orderActions.SelectOrder(p)); } }}
![Page 64: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/64.jpg)
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) { }
}
![Page 65: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/65.jpg)
Sidebar 모듈!라우팅이 존재!
![Page 66: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/66.jpg)
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 { }
![Page 67: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/67.jpg)
@ngrx/router-store
![Page 68: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/68.jpg)
/**
* 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>;
};
![Page 69: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/69.jpg)
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)
}
}
![Page 70: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/70.jpg)
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>
![Page 71: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/71.jpg)
EventEffects는 에디터에서 보겠습니다.
![Page 72: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/72.jpg)
감사합니다.
![Page 73: [FEConf Korea 2017]Angular 컴포넌트 대화법](https://reader031.vdocuments.pub/reader031/viewer/2022013109/5a64bb297f8b9ac21c8b50a3/html5/thumbnails/73.jpg)
● 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