Firebase를 사용한 Angular 상태 관리
게시 됨: 2022-03-11상태 관리는 웹 앱을 개발할 때 고려해야 할 매우 중요한 아키텍처입니다.
이 튜토리얼에서는 Firebase를 백엔드로 사용하는 Angular 애플리케이션에서 상태를 관리하는 간단한 접근 방식을 살펴보겠습니다.
상태, 상점 및 서비스와 같은 몇 가지 개념을 살펴보겠습니다. 바라건대 이것은 당신이 이러한 용어를 더 잘 이해하고 NgRx 및 NgX와 같은 다른 상태 관리 라이브러리를 더 잘 이해하는 데 도움이 될 것입니다.
우리는 몇 가지 다른 상태 관리 시나리오와 이를 처리할 수 있는 접근 방식을 다루기 위해 직원 관리 페이지를 만들 것입니다.
Angular의 구성 요소, 서비스, Firestore 및 상태 관리
일반적인 Angular 애플리케이션에는 구성 요소와 서비스가 있습니다. 일반적으로 구성요소는 뷰 템플릿으로 사용됩니다. 서비스에는 비즈니스 로직이 포함되거나 외부 API 또는 기타 서비스와 통신하여 작업을 완료하거나 데이터를 검색합니다.
구성 요소는 일반적으로 데이터를 표시하고 사용자가 앱과 상호 작용하여 작업을 실행할 수 있도록 합니다. 이 작업을 수행하는 동안 데이터가 변경될 수 있으며 앱은 보기를 업데이트하여 이러한 변경 사항을 반영합니다.
Angular의 변경 감지 엔진은 뷰에 바인딩된 구성 요소의 값이 변경된 시점을 확인하고 그에 따라 뷰를 업데이트합니다.
앱이 성장함에 따라 우리는 점점 더 많은 구성 요소와 서비스를 갖게 될 것입니다. 종종 데이터가 어떻게 변하는지 이해하고 그러한 변화가 일어나는 위치를 추적하는 것은 까다로울 수 있습니다.
앵귤러와 파이어베이스
Firebase를 백엔드로 사용하면 실시간 애플리케이션을 구축하는 데 필요한 대부분의 작업과 기능이 포함된 정말 깔끔한 API가 제공됩니다.
@angular/fire
는 공식 Angular Firebase 라이브러리입니다. Angular 앱에서 Firebase SDK 사용을 간소화하는 Firebase JavaScript SDK 라이브러리 상단의 레이어입니다. Firebase에서 구성 요소로 데이터를 가져오고 표시하기 위해 Observable을 사용하는 것과 같은 Angular 모범 사례에 잘 맞습니다.
상점 및 주
"상태"는 앱의 특정 시점에 표시되는 값으로 생각할 수 있습니다. 저장소는 단순히 해당 응용 프로그램 상태의 보유자입니다.
상태는 애플리케이션의 값을 반영하여 단일 일반 객체 또는 일련의 객체로 모델링될 수 있습니다.
Angular/Firebase 샘플 앱
빌드해 보겠습니다. 먼저 Angular CLI를 사용하여 기본 앱 스캐폴드를 만들고 Firebase 프로젝트와 연결합니다.
$ npm install -g @angular/cli $ ng new employees-admin` Would you like to add Angular routing? Yes Which stylesheet format would you like to use? SCSS $ cd employees-admin/ $ npm install bootstrap # We'll add Bootstrap for the UI
그리고 styles.scss
에서 :
// ... @import "~bootstrap/scss/bootstrap";
다음으로 @angular/fire
를 설치합니다.
npm install firebase @angular/fire
이제 Firebase 콘솔에서 Firebase 프로젝트를 생성하겠습니다.
그런 다음 Firestore 데이터베이스를 만들 준비가 되었습니다.
이 튜토리얼에서는 테스트 모드에서 시작하겠습니다. 프로덕션으로 출시할 계획이라면 부적절한 액세스를 금지하는 규칙을 시행해야 합니다.
프로젝트 개요 → 프로젝트 설정으로 이동하여 Firebase 웹 구성을 로컬 environments/environment.ts
에 복사합니다.
export const environment = { production: false, firebase: { apiKey: "<api-key>", authDomain: "<auth-domain>", databaseURL: "<database-url>", projectId: "<project-id>", storageBucket: "<storage-bucket>", messagingSenderId: "<messaging-sender-id>" } };
이 시점에서 앱에 대한 기본 스캐폴드가 있습니다. ng serve
하면 다음을 얻을 수 있습니다.
Firestore 및 Store 기본 클래스
우리는 두 개의 일반 추상 클래스를 만든 다음 서비스를 빌드하기 위해 입력하고 확장할 것입니다.
제네릭을 사용하면 바인딩된 유형 없이 동작을 작성할 수 있습니다. 이렇게 하면 코드에 재사용성과 유연성이 추가됩니다.
일반 Firestore 서비스
TypeScript 제네릭을 활용하기 위해 우리는 @angular/fire
firestore
서비스를 위한 기본 제네릭 래퍼를 생성할 것입니다.
app/core/services/firestore.service.ts
생성해 보겠습니다.
코드는 다음과 같습니다.
import { Inject } from "@angular/core"; import { AngularFirestore, QueryFn } from "@angular/fire/firestore"; import { Observable } from "rxjs"; import { tap } from "rxjs/operators"; import { environment } from "src/environments/environment"; export abstract class FirestoreService<T> { protected abstract basePath: string; constructor( @Inject(AngularFirestore) protected firestore: AngularFirestore, ) { } doc$(id: string): Observable<T> { return this.firestore.doc<T>(`${this.basePath}/${id}`).valueChanges().pipe( tap(r => { if (!environment.production) { console.groupCollapsed(`Firestore Streaming [${this.basePath}] [doc$] ${id}`) console.log(r) console.groupEnd() } }), ); } collection$(queryFn?: QueryFn): Observable<T[]> { return this.firestore.collection<T>(`${this.basePath}`, queryFn).valueChanges().pipe( tap(r => { if (!environment.production) { console.groupCollapsed(`Firestore Streaming [${this.basePath}] [collection$]`) console.table(r) console.groupEnd() } }), ); } create(value: T) { const id = this.firestore.createId(); return this.collection.doc(id).set(Object.assign({}, { id }, value)).then(_ => { if (!environment.production) { console.groupCollapsed(`Firestore Service [${this.basePath}] [create]`) console.log('[Id]', id, value) console.groupEnd() } }) } delete(id: string) { return this.collection.doc(id).delete().then(_ => { if (!environment.production) { console.groupCollapsed(`Firestore Service [${this.basePath}] [delete]`) console.log('[Id]', id) console.groupEnd() } }) } private get collection() { return this.firestore.collection(`${this.basePath}`); } }
이 abstract class
는 Firestore 서비스의 일반 래퍼로 작동합니다.
이것은 AngularFirestore
를 주입해야 하는 유일한 장소여야 합니다. 이렇게 하면 @angular/fire
라이브러리가 업데이트될 때 영향을 최소화할 수 있습니다. 또한 어느 시점에서 라이브러리를 변경하려는 경우 이 클래스만 업데이트하면 됩니다.
doc$
, collection$
, create
및 delete
를 추가했습니다. 그들은 @angular/fire
의 메소드를 래핑하고 Firebase가 데이터를 스트리밍할 때 로깅을 제공합니다. 이는 디버깅에 매우 유용할 것이며, 객체가 생성되거나 삭제된 후입니다.
일반 매장 서비스
우리의 일반 저장소 서비스는 RxJS의 BehaviorSubject
를 사용하여 구축됩니다. BehaviorSubject
를 사용하면 구독자가 구독하자마자 마지막으로 내보낸 값을 얻을 수 있습니다. 우리의 경우 상점을 구독할 때 모든 구성 요소에 대한 초기 값으로 상점을 시작할 수 있기 때문에 이것은 유용합니다.
저장소에는 patch
및 set
의 두 가지 메서드가 있습니다. (나중에 get
메소드를 생성할 것입니다.)
app/core/services/store.service.ts
생성해 보겠습니다.
import { BehaviorSubject, Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; export abstract class StoreService<T> { protected bs: BehaviorSubject<T>; state$: Observable<T>; state: T; previous: T; protected abstract store: string; constructor(initialValue: Partial<T>) { this.bs = new BehaviorSubject<T>(initialValue as T); this.state$ = this.bs.asObservable(); this.state = initialValue as T; this.state$.subscribe(s => { this.state = s }) } patch(newValue: Partial<T>, event: string = "Not specified") { this.previous = this.state const newState = Object.assign({}, this.state, newValue); if (!environment.production) { console.groupCollapsed(`[${this.store} store] [patch] [event: ${event}]`) console.log("change", newValue) console.log("prev", this.previous) console.log("next", newState) console.groupEnd() } this.bs.next(newState) } set(newValue: Partial<T>, event: string = "Not specified") { this.previous = this.state const newState = Object.assign({}, newValue) as T; if (!environment.production) { console.groupCollapsed(`[${this.store} store] [set] [event: ${event}]`) console.log("change", newValue) console.log("prev", this.previous) console.log("next", newState) console.groupEnd() } this.bs.next(newState) } }
제네릭 클래스로서 적절하게 확장될 때까지 타이핑을 연기할 것입니다.
생성자는 Partial<T>
유형의 초기 값을 받습니다. 이렇게 하면 상태의 일부 속성에만 값을 적용할 수 있습니다. 생성자는 또한 내부 BehaviorSubject
방출을 구독하고 모든 변경 후에 내부 상태를 업데이트된 상태로 유지합니다.
patch()
는 Partial<T>
유형의 newValue
를 수신하고 저장소의 현재 this.state
값과 병합합니다. 마지막으로 newState
를 next()
하고 모든 스토어 구독자에게 새 상태를 내보냅니다.
set()
은 상태 값을 패치하는 대신 수신한 newValue
로 설정한다는 점에서 매우 유사하게 작동합니다.
변경이 발생하면 상태의 이전 및 다음 값을 기록하여 상태 변경을 디버그하고 쉽게 추적하는 데 도움이 됩니다.
함께 모아서
자, 이 모든 것을 실제로 보겠습니다. 우리가 할 일은 직원 목록과 새 직원을 추가하는 양식이 포함된 직원 페이지를 만드는 것입니다.
간단한 탐색 모음을 추가하도록 app.component.html
을 업데이트해 보겠습니다.
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-3"> <span class="navbar-brand mb-0 h1">Angular + Firebase + State Management</span> <ul class="navbar-nav mr-auto"> <li class="nav-item" [routerLink]="['/employees']" routerLinkActive="active"> <a class="nav-link">Employees</a> </li> </ul> </nav> <router-outlet></router-outlet>
다음으로 Core 모듈을 생성합니다.
ng gm Core
core/core.module.ts
에서 앱에 필요한 모듈을 추가합니다.
// ... import { AngularFireModule } from '@angular/fire' import { AngularFirestoreModule } from '@angular/fire/firestore' import { environment } from 'src/environments/environment'; import { ReactiveFormsModule } from '@angular/forms' @NgModule({ // ... imports: [ // ... AngularFireModule.initializeApp(environment.firebase), AngularFirestoreModule, ReactiveFormsModule, ], exports: [ CommonModule, AngularFireModule, AngularFirestoreModule, ReactiveFormsModule ] }) export class CoreModule { }
이제 직원 모듈로 시작하여 직원 페이지를 생성해 보겠습니다.
ng gm Employees --routing
employees-routing.module.ts
에서 employees
경로를 추가해 보겠습니다.
// ... import { EmployeesPageComponent } from './components/employees-page/employees-page.component'; // ... const routes: Routes = [ { path: 'employees', component: EmployeesPageComponent } ]; // ...
그리고 employees.module.ts
에서 ReactiveFormsModule
을 가져올 것입니다.
// ... import { ReactiveFormsModule } from '@angular/forms'; // ... @NgModule({ // ... imports: [ // ... ReactiveFormsModule ] }) export class EmployeesModule { }
이제 app.module.ts
파일에 다음 두 모듈을 추가해 보겠습니다.
// ... import { EmployeesModule } from './employees/employees.module'; import { CoreModule } from './core/core.module'; imports: [ // ... CoreModule, EmployeesModule ],
마지막으로 직원 페이지의 실제 구성 요소와 해당 모델, 서비스, 저장소 및 상태를 만들어 보겠습니다.
ng gc employees/components/EmployeesPage ng gc employees/components/EmployeesList ng gc employees/components/EmployeesForm
우리 모델의 경우 models/employee.ts
라는 파일이 필요합니다.
export interface Employee { id: string; name: string; location: string; hasDriverLicense: boolean; }
우리 서비스는 employees/services/employee.firestore.ts
라는 파일에 있을 것입니다. 이 서비스는 이전에 생성된 일반 FirestoreService<T>
를 확장하고 Firestore 컬렉션의 basePath
를 설정합니다.
import { Injectable } from '@angular/core'; import { FirestoreService } from 'src/app/core/services/firestore.service'; import { Employee } from '../models/employee'; @Injectable({ providedIn: 'root' }) export class EmployeeFirestore extends FirestoreService<Employee> { protected basePath: string = 'employees'; }
그런 다음 employees/states/employees-page.ts
파일을 생성합니다. 이것은 직원 페이지의 상태로 사용됩니다.
import { Employee } from '../models/employee'; export interface EmployeesPage { loading: boolean; employees: Employee[]; formStatus: string; }
상태는 페이지에 로딩 메시지를 표시할지 여부를 결정하는 loading
값, employees
자체 및 양식의 상태를 처리하기 위한 formStatus
변수(예: Saving
또는 Saved
)를 갖습니다.
employees/services/employees-page.store.ts
에 파일이 필요합니다. 여기서 우리는 이전에 생성된 StoreService<T>
를 확장할 것입니다. 디버깅할 때 식별하는 데 사용할 저장소 이름을 설정합니다.
이 서비스는 직원 페이지의 상태를 초기화하고 유지합니다. 생성자는 페이지의 초기 상태로 super()
를 호출합니다. 이 경우 loading=true
와 빈 직원 배열로 상태를 초기화합니다.
import { EmployeesPage } from '../states/employees-page'; import { StoreService } from 'src/app/core/services/store.service'; import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class EmployeesPageStore extends StoreService<EmployeesPage> { protected store: string = 'employees-page'; constructor() { super({ loading: true, employees: [], }) } }
이제 EmployeeFirestore
와 EmployeesPageStore
를 통합하기 위해 EmployeesService
를 만들어 보겠습니다.
ng gs employees/services/Employees
이 서비스에 EmployeeFirestore
및 EmployeesPageStore
를 주입하고 있습니다. 즉, EmployeesService
는 Firestore 및 저장소에 대한 호출을 포함하고 조정하여 상태를 업데이트합니다. 이렇게 하면 구성 요소가 호출할 단일 API를 만드는 데 도움이 됩니다.
import { EmployeesPageStore } from './employees-page.store'; import { EmployeeFirestore } from './employee.firestore'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { Employee } from '../models/employee'; import { tap, map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class EmployeesService { constructor( private firestore: EmployeeFirestore, private store: EmployeesPageStore ) { this.firestore.collection$().pipe( tap(employees => { this.store.patch({ loading: false, employees, }, `employees collection subscription`) }) ).subscribe() } get employees$(): Observable<Employee[]> { return this.store.state$.pipe(map(state => state.loading ? [] : state.employees)) } get loading$(): Observable<boolean> { return this.store.state$.pipe(map(state => state.loading)) } get noResults$(): Observable<boolean> { return this.store.state$.pipe( map(state => { return !state.loading && state.employees && state.employees.length === 0 }) ) } get formStatus$(): Observable<string> { return this.store.state$.pipe(map(state => state.formStatus)) } create(employee: Employee) { this.store.patch({ loading: true, employees: [], formStatus: 'Saving...' }, "employee create") return this.firestore.create(employee).then(_ => { this.store.patch({ formStatus: 'Saved!' }, "employee create SUCCESS") setTimeout(() => this.store.patch({ formStatus: '' }, "employee create timeout reset formStatus"), 2000) }).catch(err => { this.store.patch({ loading: false, formStatus: 'An error ocurred' }, "employee create ERROR") }) } delete(id: string): any { this.store.patch({ loading: true, employees: [] }, "employee delete") return this.firestore.delete(id).catch(err => { this.store.patch({ loading: false, formStatus: 'An error ocurred' }, "employee delete ERROR") }) } }
서비스가 어떻게 작동하는지 살펴보겠습니다.

생성자에서 Firestore 직원 컬렉션을 구독합니다. Firestore가 컬렉션에서 데이터를 내보내는 즉시 loading=false
로 설정하고 Firestore의 반환된 컬렉션으로 employees
을 업데이트하여 저장소를 업데이트합니다. EmployeeFirestore
를 주입했기 때문에 Firestore에서 반환된 개체는 Employee
형식으로 지정되어 더 많은 IntelliSense 기능을 사용할 수 있습니다.
이 구독은 앱이 활성화되어 있는 동안 활성화되어 모든 변경 사항을 수신하고 Firestore가 데이터를 스트리밍할 때마다 스토어를 업데이트합니다.
this.firestore.collection$().pipe( tap(employees => { this.store.patch({ loading: false, employees, }, `employees collection subscription`) }) ).subscribe()
employees$()
및 loading$()
함수는 나중에 구성 요소에서 사용하려는 상태를 선택합니다. employees$()
는 상태가 로드될 때 빈 배열을 반환합니다. 이렇게 하면 보기에 적절한 메시지를 표시할 수 있습니다.
get employees$(): Observable<Employee[]> { return this.store.state$.pipe(map(state => state.loading ? [] : state.employees)) } get loading$(): Observable<boolean> { return this.store.state$.pipe(map(state => state.loading)) }
자, 이제 모든 서비스가 준비되었으며 뷰 구성 요소를 빌드할 수 있습니다. 하지만 그렇게 하기 전에 빠른 복습이 도움이 될 수 있습니다.
RxJ Observable과 async
파이프
Observable을 사용하면 구독자가 데이터 방출을 스트림으로 수신할 수 있습니다. 이것은 async
파이프와 함께 매우 강력할 수 있습니다.
async
파이프는 Observable을 구독하고 새 데이터가 방출될 때 뷰를 업데이트합니다. 더 중요한 것은 구성 요소가 파괴되면 자동으로 구독을 취소하여 메모리 누수로부터 우리를 보호한다는 것입니다.
공식 문서에서 일반적으로 Observables 및 RxJs 라이브러리에 대해 자세히 읽을 수 있습니다.
보기 구성 요소 만들기
employees/components/employees-page/employees-page.component.html
에 다음 코드를 삽입합니다.
<div class="container"> <div class="row"> <div class="col-12 mb-3"> <h4> Employees </h4> </div> </div> <div class="row"> <div class="col-6"> <app-employees-list></app-employees-list> </div> <div class="col-6"> <app-employees-form></app-employees-form> </div> </div> </div>
마찬가지로, employees/components/employees-list/employees-list.component.html
은 위에서 언급한 async
파이프 기술을 사용하여 이것을 갖게 됩니다.
<div *ngIf="loading$ | async"> Loading... </div> <div *ngIf="noResults$ | async"> No results </div> <div class="card bg-light mb-3" *ngFor="let employee of employees$ | async"> <div class="card-header">{{employee.location}}</div> <div class="card-body"> <h5 class="card-title">{{employee.name}}</h5> <p class="card-text">{{employee.hasDriverLicense ? 'Can drive': ''}}</p> <button (click)="delete(employee)" class="btn btn-danger">Delete</button> </div> </div>
그러나 이 경우 구성 요소에 대한 일부 TypeScript 코드도 필요합니다. employees/components/employees-list/employees-list.component.ts
에는 다음이 필요합니다.
import { Employee } from '../../models/employee'; import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { EmployeesService } from '../../services/employees.service'; @Component({ selector: 'app-employees-list', templateUrl: './employees-list.component.html', styleUrls: ['./employees-list.component.scss'] }) export class EmployeesListComponent implements OnInit { loading$: Observable<boolean>; employees$: Observable<Employee[]>; noResults$: Observable<boolean>; constructor( private employees: EmployeesService ) {} ngOnInit() { this.loading$ = this.employees.loading$; this.noResults$ = this.employees.noResults$; this.employees$ = this.employees.employees$; } delete(employee: Employee) { this.employees.delete(employee.id); } }
이제 브라우저로 이동하면 다음과 같습니다.
콘솔에는 다음과 같은 출력이 표시됩니다.
이를 보면 Firestore가 employees
컬렉션을 빈 값으로 스트리밍하고 직원 employees-page
저장소가 패치되어 loading
를 true
에서 false
로 설정했음을 알 수 있습니다.
자, Firestore에 새 직원을 추가하는 양식을 작성해 보겠습니다.
직원 양식
employees/components/employees-form/employees-form.component.html
에 다음 코드를 추가합니다.
<form [formGroup]="form" (ngSubmit)="submit()"> <div class="form-group"> <label for="name">Name</label> <input type="string" class="form-control" formControlName="name" [class.is-invalid]="isInvalid('name')"> <div class="invalid-feedback"> Please enter a Name. </div> </div> <div class="form-group"> <select class="custom-select" formControlName="location" [class.is-invalid]="isInvalid('location')"> <option value="" selected>Choose location</option> <option *ngFor="let loc of locations" [ngValue]="loc">{{loc}}</option> </select> <div class="invalid-feedback"> Please select a Location. </div> </div> <div class="form-group form-check"> <input type="checkbox" class="form-check-input" formControlName="hasDriverLicense"> <label class="form-check-label" for="hasDriverLicense">Has driver license</label> </div> <button [disabled]="form.invalid" type="submit" class="btn btn-primary d-inline">Add</button> <span class="ml-2">{{ status$ | async }}</span> </form>
해당 TypeScript 코드는 employees/components/employees-form/employees-form.component.ts
.
import { EmployeesService } from './../../services/employees.service'; import { AngularFirestore } from '@angular/fire/firestore'; import { Component, OnInit } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; import { Observable } from 'rxjs'; @Component({ selector: 'app-employees-form', templateUrl: './employees-form.component.html', styleUrls: ['./employees-form.component.scss'] }) export class EmployeesFormComponent implements OnInit { form: FormGroup = new FormGroup({ name: new FormControl('', Validators.required), location: new FormControl('', Validators.required), hasDriverLicense: new FormControl(false) }); locations = [ 'Rosario', 'Buenos Aires', 'Bariloche' ] status$: Observable < string > ; constructor( private employees: EmployeesService ) {} ngOnInit() { this.status$ = this.employees.formStatus$; } isInvalid(name) { return this.form.controls[name].invalid && (this.form.controls[name].dirty || this.form.controls[name].touched) } async submit() { this.form.disable() await this.employees.create({ ...this.form.value }) this.form.reset() this.form.enable() } }
이 양식은 EmployeesService
의 create()
메서드를 호출합니다. 현재 페이지는 다음과 같습니다.
새 직원을 추가하면 어떻게 되는지 살펴보겠습니다.
새 직원 추가
새 직원을 추가한 후 콘솔에 다음과 같은 get 출력이 표시됩니다.
이것은 새 직원을 추가할 때 트리거되는 모든 이벤트입니다. 자세히 살펴보겠습니다.
create()
를 호출할 때 다음 코드를 실행하여 loading=true
, formStatus='Saving...'
및 employees
배열을 공백으로 설정합니다(위 이미지에서 (1)
).
this.store.patch({ loading: true, employees: [], formStatus: 'Saving...' }, "employee create") return this.firestore.create(employee).then(_ => { this.store.patch({ formStatus: 'Saved!' }, "employee create SUCCESS") setTimeout(() => this.store.patch({ formStatus: '' }, "employee create timeout reset formStatus"), 2000) }).catch(err => { this.store.patch({ loading: false, formStatus: 'An error ocurred' }, "employee create ERROR") })
다음으로 기본 Firestore 서비스를 호출하여 (4)
를 기록하는 직원을 생성합니다. 약속 콜백에서 우리는 formStatus='Saved!'
를 설정했습니다. 및 로그 (5)
. 마지막으로, formStatus
를 다시 빈 상태로 설정하기 위해 시간 초과를 설정했습니다. logging (6)
.
로그 이벤트 (2)
및 (3)
은 직원 컬렉션에 대한 Firestore 구독에 의해 트리거되는 이벤트입니다. EmployeesService
가 인스턴스화되면 컬렉션을 구독하고 변경 사항이 발생할 때마다 컬렉션을 받습니다.
이것은 employees
배열을 Firestore에서 오는 직원으로 설정하여 loading=false
인 저장소에 새 상태를 설정합니다.
로그 그룹을 확장하면 모든 이벤트 및 저장소 업데이트에 대한 자세한 데이터가 이전 값과 다음 값과 함께 표시되므로 디버깅에 유용합니다.
새 직원을 추가한 후의 페이지는 다음과 같습니다.
요약 구성 요소 추가
이제 페이지에 일부 요약 데이터를 표시하려고 한다고 가정해 보겠습니다. 총 직원 수, 운전 기사 수, Rosario 출신 수를 원한다고 가정해 보겠습니다.
employees/states/employees-page.ts
의 페이지 상태 모델에 새 상태 속성을 추가하는 것으로 시작하겠습니다.
// ... export interface EmployeesPage { loading: boolean; employees: Employee[]; formStatus: string; totalEmployees: number; totalDrivers: number; totalRosarioEmployees: number; }
그리고 employees/services/emplyees-page.store.ts
의 스토어에서 초기화합니다.
// ... constructor() { super({ loading: true, employees: [], totalDrivers: 0, totalEmployees: 0, totalRosarioEmployees: 0 }) } // ...
다음으로 새 속성의 값을 계산하고 EmployeesService
에 해당 선택기를 추가합니다.
// ... this.firestore.collection$().pipe( tap(employees => { this.store.patch({ loading: false, employees, totalEmployees: employees.length, totalDrivers: employees.filter(employee => employee.hasDriverLicense).length, totalRosarioEmployees: employees.filter(employee => employee.location === 'Rosario').length, }, `employees collection subscription`) }) ).subscribe() // ... get totalEmployees$(): Observable < number > { return this.store.state$.pipe(map(state => state.totalEmployees)) } get totalDrivers$(): Observable < number > { return this.store.state$.pipe(map(state => state.totalDrivers)) } get totalRosarioEmployees$(): Observable < number > { return this.store.state$.pipe(map(state => state.totalRosarioEmployees)) } // ...
이제 요약 구성 요소를 생성해 보겠습니다.
ng gc employees/components/EmployeesSummary
우리는 이것을 employees/components/employees-summary/employees-summary.html
에 넣을 것입니다:
<p> <span class="font-weight-bold">Total:</span> {{total$ | async}} <br> <span class="font-weight-bold">Drivers:</span> {{drivers$ | async}} <br> <span class="font-weight-bold">Rosario:</span> {{rosario$ | async}} <br> </p>
그리고 employees/components/employees-summary/employees-summary.ts
:
import { Component, OnInit } from '@angular/core'; import { EmployeesService } from '../../services/employees.service'; import { Observable } from 'rxjs'; @Component({ selector: 'app-employees-summary', templateUrl: './employees-summary.component.html', styleUrls: ['./employees-summary.component.scss'] }) export class EmployeesSummaryComponent implements OnInit { total$: Observable < number > ; drivers$: Observable < number > ; rosario$: Observable < number > ; constructor( private employees: EmployeesService ) {} ngOnInit() { this.total$ = this.employees.totalEmployees$; this.drivers$ = this.employees.totalDrivers$; this.rosario$ = this.employees.totalRosarioEmployees$; } }
그런 다음 구성 요소를 employees/employees-page/employees-page.component.html
추가합니다.
// ... <div class="col-12 mb-3"> <h4> Employees </h4> <app-employees-summary></app-employees-summary> </div> // ...
결과는 다음과 같습니다.
콘솔에는 다음이 있습니다.
직원 서비스는 각 배출에서 총 totalEmployees
, totalDrivers
및 totalRosarioEmployees
를 계산하고 상태를 업데이트합니다.
이 자습서의 전체 코드는 GitHub에서 사용할 수 있으며 라이브 데모도 있습니다.
Observable을 사용하여 Angular 앱 상태 관리… 확인하십시오!
이 튜토리얼에서는 Firebase 백엔드를 사용하여 Angular 앱에서 상태를 관리하는 간단한 접근 방식을 다루었습니다.
이 접근 방식은 Observable을 사용하는 Angular 지침과 잘 맞습니다. 또한 앱 상태에 대한 모든 업데이트에 대한 추적을 제공하여 디버깅을 용이하게 합니다.
일반 스토어 서비스를 사용하여 Firebase 기능을 사용하지 않는 앱의 상태를 관리하거나 앱의 데이터만 관리하거나 다른 API에서 가져온 데이터를 관리할 수도 있습니다.
그러나 이것을 무분별하게 적용하기 전에 고려해야 할 한 가지 점은 EmployeesService
가 생성자에서 Firestore를 구독하고 앱이 활성화되어 있는 동안 계속 수신 대기한다는 것입니다. 이는 앱의 여러 페이지에서 직원 목록을 사용하여 페이지 사이를 탐색할 때 Firestore에서 데이터를 가져오는 것을 방지하는 데 유용할 수 있습니다.
그러나 초기 값을 한 번만 가져온 다음 Firebase에서 데이터 다시 로드를 수동으로 트리거해야 하는 경우와 같은 다른 시나리오에서는 이것이 최선의 옵션이 아닐 수 있습니다. 결론은 더 나은 구현 방법을 선택하기 위해 앱의 요구 사항을 이해하는 것이 항상 중요하다는 것입니다.