1. 소개
최종 업데이트: 2020년 9월 11일
빌드할 항목
이 Codelab에서는 Angular와 Firebase를 사용하여 웹 간반 보드를 빌드해보겠습니다. 최종 앱에는 밀린 작업, 진행 중, 완료라는 작업 카테고리 3개가 만들어집니다. 작업을 만들고 삭제하며 드래그 앤 드롭을 사용하여 한 카테고리에서 다른 카테고리로 작업을 전송할 수 있게 됩니다.
Angular를 사용하여 사용자 인터페이스를 개발하고 Firestore를 영구 스토리지로 사용하겠습니다. Codelab을 마치면 Angular CLI를 사용하여 Firebase 호스팅에 앱을 배포하게 됩니다.
학습할 내용
- Angular Material과 CDK를 사용하는 방법
- Angular 앱에 Firebase 통합을 추가하는 방법
- Firestore에 영구 데이터를 유지하는 방법
- 단일 명령어로 Angular CLI를 사용하여 Firebase 호스팅에 앱을 배포하는 방법
필요한 항목
이 Codelab에서는 개발자에게 Google 계정이 있고 Angular와 Angular CLI를 기본적으로 이해하고 있다고 가정합니다.
이제 시작해 볼까요?
2. 새 프로젝트 만들기
먼저 다음과 같이 새 Angular 작업공간을 만듭니다.
ng new kanban-fire
? Would you like to add Angular routing? No
? Which stylesheet format would you like to use? CSS
몇 분 정도 걸릴 수 있습니다. Angular CLI는 프로젝트 구조를 만들고 모든 종속 항목을 설치합니다. 설치 프로세스가 완료되면 kanban-fire
디렉터리로 이동하여 Angular CLI의 개발 서버를 시작합니다.
ng serve
http://localhost:4200을 열면 다음과 같은 출력이 표시됩니다.
편집기에서 src/app/app.component.html
을 열고 전체 내용을 삭제합니다. http://localhost:4200으로 다시 돌아가면 빈 페이지가 표시됩니다.
3. 머티리얼 및 CDK 추가
Angular는 머티리얼 디자인을 준수하는 사용자 인터페이스 구성요소의 구현과 함께 제공되며 @angular/material
패키지의 일부입니다. @angular/material
의 종속 항목 중 하나는 구성요소 개발 키트(CDK)입니다. CDK는 a11y 유틸리티, 드래그 앤 드롭, 오버레이와 같은 프리미티브를 제공합니다. Google에서는 @angular/cdk
패키지로 CDK를 배포합니다.
앱에 머티리얼을 추가하려면 다음을 실행하세요.
ng add @angular/material
이 명령어는 개발자가 전역 머티리얼 서체 스타일을 사용하고 Angular Material의 브라우저 애니메이션을 설정하려는 경우 테마를 선택하라고 요청합니다. '인디고/핑크'를 선택하여 이 Codelab과 동일한 결과를 얻고 마지막 두 질문에 '예'라고 답변합니다.
ng add
명령어는 @angular/material
및 그 종속 항목을 설치하고 AppModule
에서 BrowserAnimationsModule
을 가져옵니다. 다음 단계에서는 이 모듈에서 제공하는 구성요소를 사용할 수 있습니다.
먼저 툴바와 아이콘을 AppComponent
에 추가해보겠습니다. app.component.html
을 열고 다음 마크업을 추가합니다.
src/app/app.component.html
<mat-toolbar color="primary">
<mat-icon>local_fire_department</mat-icon>
<span>Kanban Fire</span>
</mat-toolbar>
여기서는 머티리얼 디자인 테마의 기본 색상을 사용하는 툴바를 추가한 다음 그 안에 local_fire_depeartment
아이콘을 'Kanban Fire' 라벨 옆에 사용합니다. 지금 콘솔을 살펴보면 Angular에서 몇 가지 오류가 발생하는 것을 확인할 수 있습니다. 이 문제를 해결하려면 AppModule
에 다음 가져오기를 추가해야 합니다.
src/app/app.module.ts
...
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatToolbarModule,
MatIconModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Angular Material 툴바와 아이콘을 사용하므로 AppModule
에서 상응하는 모듈을 가져와야 합니다.
이제 화면이 다음과 같이 표시됩니다.
HTML 4줄과 가져오기 2개만 사용했는데도 나쁘지 않습니다.
4. 작업 시각화
다음 단계로 간반 보드에서 작업을 시각화하는 데 사용할 수 있는 구성요소를 만들어보겠습니다.
src/app
디렉터리로 이동하여 다음 CLI 명령어를 실행합니다.
ng generate component task
이 명령어는 TaskComponent
를 생성하고 AppModule
에 선언을 추가합니다. task
디렉터리 내부에 task.ts
라는 파일을 만듭니다. 이 파일을 간반 보드에서 작업 인터페이스를 정의하는 데 사용해보겠습니다. 각 작업에는 모두 문자열 유형인 id
, title
, description
필드가 선택사항으로 있습니다.
src/app/task/task.ts
export interface Task {
id?: string;
title: string;
description: string;
}
이제 task.component.ts
를 업데이트하겠습니다. TaskComponent
가 Task
유형의 객체를 입력으로 허용하고 'edit
' 출력을 내보낼 수 있어야 합니다.
src/app/task/task.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Task } from './task';
@Component({
selector: 'app-task',
templateUrl: './task.component.html',
styleUrls: ['./task.component.css']
})
export class TaskComponent {
@Input() task: Task | null = null;
@Output() edit = new EventEmitter<Task>();
}
TaskComponent
의 템플릿을 수정합니다. task.component.html
을 열고 콘텐츠를 다음 HTML로 바꿉니다.
src/app/task/task.component.html
<mat-card class="item" *ngIf="task" (dblclick)="edit.emit(task)">
<h2>{{ task.title }}</h2>
<p>
{{ task.description }}
</p>
</mat-card>
이제 콘솔에 오류가 표시됩니다.
'mat-card' is not a known element:
1. If 'mat-card' is an Angular component, then verify that it is part of this module.
2. If 'mat-card' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.ng
위 템플릿에서는 @angular/material
의 mat-card
구성요소를 사용하고 있지만 앱에서 이에 상응하는 모듈을 가져오지 않았습니다. 위와 같은 오류를 해결하려면 AppModule
에서 MatCardModule
을 가져와야 합니다.
src/app/app.module.ts
...
import { MatCardModule } from '@angular/material/card';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatCardModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
이제 AppComponent
에서 몇 가지 작업을 만들고 TaskComponent
를 사용하여 이를 시각화합니다.
AppComponent
에서 todo
라는 배열을 정의하고 그 안에 작업 2개를 추가합니다.
src/app/app.component.ts
...
import { Task } from './task/task';
@Component(...)
export class AppComponent {
todo: Task[] = [
{
title: 'Buy milk',
description: 'Go to the store and buy milk'
},
{
title: 'Create a Kanban app',
description: 'Using Firebase and Angular create a Kanban app!'
}
];
}
이제 app.component.html
하단에 다음 *ngFor
지시어를 추가합니다.
src/app/app.component.html
<app-task *ngFor="let task of todo" [task]="task"></app-task>
브라우저를 열면 다음과 같이 표시됩니다.
5. 작업 드래그 앤 드롭 구현
이제 재미있는 부분을 할 차례입니다. 작업이 배치될 수 있는 각기 다른 상태의 구획 3개를 만들고 Angular CDK를 사용하여 드래그 앤 드롭 기능을 구현합니다.
app.component.html
에서 상단에 *ngFor
지시어가 있는 app-task
구성요소를 삭제하고 다음으로 바꿉니다.
src/app/app.component.html
<div class="container-wrapper">
<div class="container">
<h2>Backlog</h2>
<mat-card
cdkDropList
id="todo"
#todoList="cdkDropList"
[cdkDropListData]="todo"
[cdkDropListConnectedTo]="[doneList, inProgressList]"
(cdkDropListDropped)="drop($event)"
class="list">
<p class="empty-label" *ngIf="todo.length === 0">Empty list</p>
<app-task (edit)="editTask('todo', $event)" *ngFor="let task of todo" cdkDrag [task]="task"></app-task>
</mat-card>
</div>
<div class="container">
<h2>In progress</h2>
<mat-card
cdkDropList
id="inProgress"
#inProgressList="cdkDropList"
[cdkDropListData]="inProgress"
[cdkDropListConnectedTo]="[todoList, doneList]"
(cdkDropListDropped)="drop($event)"
class="list">
<p class="empty-label" *ngIf="inProgress.length === 0">Empty list</p>
<app-task (edit)="editTask('inProgress', $event)" *ngFor="let task of inProgress" cdkDrag [task]="task"></app-task>
</mat-card>
</div>
<div class="container">
<h2>Done</h2>
<mat-card
cdkDropList
id="done"
#doneList="cdkDropList"
[cdkDropListData]="done"
[cdkDropListConnectedTo]="[todoList, inProgressList]"
(cdkDropListDropped)="drop($event)"
class="list">
<p class="empty-label" *ngIf="done.length === 0">Empty list</p>
<app-task (edit)="editTask('done', $event)" *ngFor="let task of done" cdkDrag [task]="task"></app-task>
</mat-card>
</div>
</div>
많은 작업이 진행되고 있습니다. 이 스니펫의 개별 부분을 단계별로 살펴보겠습니다. 다음은 템플릿의 최상위 구조입니다.
src/app/app.component.html
...
<div class="container-wrapper">
<div class="container">
<h2>Backlog</h2>
...
</div>
<div class="container">
<h2>In progress</h2>
...
</div>
<div class="container">
<h2>Done</h2>
...
</div>
</div>
여기에서는 세 구획을 모두 래핑하는 div
를 만듭니다. 클래스 이름은 'container-wrapper
'입니다. 각 구획에는 클래스 이름 'container
'와 h2
태그 내부의 제목이 있습니다.
이제 첫 번째 구획의 구조를 살펴보겠습니다.
src/app/app.component.html
...
<div class="container">
<h2>Backlog</h2>
<mat-card
cdkDropList
id="todo"
#todoList="cdkDropList"
[cdkDropListData]="todo"
[cdkDropListConnectedTo]="[doneList, inProgressList]"
(cdkDropListDropped)="drop($event)"
class="list"
>
<p class="empty-label" *ngIf="todo.length === 0">Empty list</p>
<app-task (edit)="editTask('todo', $event)" *ngFor="let task of todo" cdkDrag [task]="task"></app-task>
</mat-card>
</div>
...
먼저 cdkDropList
지시어를 사용하는 mat-card
로 구획을 정의합니다. mat-card
를 사용하는 이유는 이 구성요소가 제공하는 스타일 때문입니다. 나중에 cdkDropList
를 통해 요소 내부에 작업을 드롭할 수 있습니다. 다음 두 가지 입력도 설정합니다.
cdkDropListData
: 데이터 배열을 지정할 수 있는 드롭다운 목록의 입력입니다.cdkDropListConnectedTo
: 현재cdkDropList
가 연결된 다른cdkDropList
참조입니다. 이 입력을 설정하면 항목을 드롭할 수 있는 다른 목록을 지정합니다.
또한 cdkDropListDropped
출력을 사용하여 드롭 이벤트를 처리해야 합니다. cdkDropList
에서 이 출력을 내보내면 AppComponent
내에서 선언된 drop
메서드를 호출하고 현재 이벤트를 인수로 전달합니다.
이 컨테이너의 식별자로 사용할 id
를 지정하고 스타일을 설정할 class
이름도 지정합니다. 이제 mat-card
의 하위 콘텐츠를 살펴보겠습니다. 다음과 같은 두 가지 요소가 있습니다.
- 단락:
todo
목록에 항목이 없을 때 '빈 목록' 텍스트를 표시하는 데 사용합니다. app-task
구성요소: 여기서는 목록 이름과$event
객체를 사용하여editTask
메서드를 호출해 처음에 선언한edit
출력을 처리합니다. 이렇게 하면 올바른 목록에서 수정된 작업을 바꾸는 데 도움이 됩니다. 그런 다음 위와 마찬가지로todo
목록 작업을 반복하고task
입력을 전달합니다. 하지만 이번에는cdkDrag
지시어도 추가합니다. 이렇게 하면 개별 작업이 드래그 가능해집니다.
이 모든 작업을 실행하려면 app.module.ts
를 업데이트하고 DragDropModule
의 가져오기를 포함해야 합니다.
src/app/app.module.ts
...
import { DragDropModule } from '@angular/cdk/drag-drop';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
DragDropModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
또한 inProgress
및 done
배열과 editTask
및 drop
메서드도 선언해야 합니다.
src/app/app.component.ts
...
import { CdkDragDrop, transferArrayItem } from '@angular/cdk/drag-drop';
@Component(...)
export class AppComponent {
todo: Task[] = [...];
inProgress: Task[] = [];
done: Task[] = [];
editTask(list: string, task: Task): void {}
drop(event: CdkDragDrop<Task[]|null>): void {
if (event.previousContainer === event.container) {
return;
}
if (!event.container.data || !event.previousContainer.data) {
return;
}
transferArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex
);
}
}
drop
메서드에서 먼저 작업이 발생한 것과 동일한 목록에 드롭하는지 확인합니다. 제대로 되고 있다면 즉시 돌아갑니다. 동일한 목록에 드롭하지 않으면 현재 작업을 대상 구획으로 전송합니다.
결과가 다음과 같이 표시됩니다.
이제 두 목록 간에 항목을 전송할 수 있습니다.
6. 새 작업 만들기
이제 새 작업을 생성하는 기능을 구현해보겠습니다. 이를 위해 AppComponent
템플릿을 업데이트합니다.
src/app/app.component.html
<mat-toolbar color="primary">
...
</mat-toolbar>
<div class="content-wrapper">
<button (click)="newTask()" mat-button>
<mat-icon>add</mat-icon> Add Task
</button>
<div class="container-wrapper">
<div class="container">
...
</div>
</div>
container-wrapper
주위에 최상위 div
요소를 만들고 '작업 추가' 라벨 옆에 'add
' 머티리얼 아이콘이 있는 버튼을 추가합니다. 구획 목록 위에 버튼을 배치하려면 추가 래퍼가 필요한데, 이는 나중에 Flexbox를 사용하여 나란히 배치하겠습니다. 이 버튼은 머티리얼 버튼 구성요소를 사용하므로 AppModule
에서 상응하는 모듈을 가져와야 합니다.
src/app/app.module.ts
...
import { MatButtonModule } from '@angular/material/button';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatButtonModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
이제 AppComponent
에서 작업을 추가하는 기능을 구현해보겠습니다. 머티리얼 대화상자를 사용합니다. 대화상자에는 제목 필드와 설명 필드가 포함된 양식이 있습니다. 사용자가 '작업 추가' 버튼을 클릭하면 대화상자가 열리고 사용자가 양식을 제출하면 새로 생성된 작업이 todo
목록에 추가됩니다.
AppComponent
에서 이 기능의 대략적인 구현을 살펴보겠습니다.
src/app/app.component.ts
...
import { MatDialog } from '@angular/material/dialog';
@Component(...)
export class AppComponent {
...
constructor(private dialog: MatDialog) {}
newTask(): void {
const dialogRef = this.dialog.open(TaskDialogComponent, {
width: '270px',
data: {
task: {},
},
});
dialogRef
.afterClosed()
.subscribe((result: TaskDialogResult|undefined) => {
if (!result) {
return;
}
this.todo.push(result.task);
});
}
}
MatDialog
클래스를 삽입하는 생성자를 선언합니다. newTask
내에서 다음 작업을 실행합니다.
- 잠시 후 정의할
TaskDialogComponent
를 사용하여 새 대화상자를 엽니다. - 대화상자의 너비가
270px.
이 되도록 지정합니다. - 대화상자에 빈 작업을 데이터로 전달합니다.
TaskDialogComponent
에서 이 데이터 객체 참조를 가져올 수 있습니다. - 닫기 이벤트를 구독하고
result
객체의 작업을todo
배열에 추가합니다.
제대로 작동하도록 하려면 먼저 AppModule
에서 MatDialogModule
을 가져와야 합니다.
src/app/app.module.ts
...
import { MatDialogModule } from '@angular/material/dialog';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatDialogModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
이제 TaskDialogComponent
를 만들어보겠습니다. src/app
디렉터리로 이동하여 다음을 실행합니다.
ng generate component task-dialog
기능을 구현하려면 먼저 src/app/task-dialog/task-dialog.component.html
을 열고 콘텐츠를 다음으로 바꿉니다.
src/app/task-dialog/task-dialog.component.html
<mat-form-field>
<mat-label>Title</mat-label>
<input matInput cdkFocusInitial [(ngModel)]="data.task.title" />
</mat-form-field>
<mat-form-field>
<mat-label>Description</mat-label>
<textarea matInput [(ngModel)]="data.task.description"></textarea>
</mat-form-field>
<div mat-dialog-actions>
<button mat-button [mat-dialog-close]="{ task: data.task }">OK</button>
<button mat-button (click)="cancel()">Cancel</button>
</div>
위 템플릿에서 title
필드와 description
필드가 포함된 양식을 만듭니다. 사용자가 대화상자를 열 때 cdkFocusInput
지시어를 사용하여 자동으로 title
입력에 포커스를 둡니다.
템플릿 내에서 구성요소의 data
속성을 어떻게 참조하는지 확인합니다. 이는 AppComponent
에서 dialog
의 open
메서드에 전달하는 data
와 동일합니다. 사용자가 해당 필드의 콘텐츠를 변경할 때 작업의 제목과 설명을 업데이트하기 위해 ngModel
과 양방향 데이터 결합을 사용합니다.
사용자가 확인 버튼을 클릭하면 결과 { task: data.task }
가 자동으로 반환되며 이는 위 템플릿의 양식 필드를 사용하여 변경한 작업입니다.
이제 구성요소의 컨트롤러를 구현해보겠습니다.
src/app/task-dialog/task-dialog.component.ts
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Task } from '../task/task';
@Component({
selector: 'app-task-dialog',
templateUrl: './task-dialog.component.html',
styleUrls: ['./task-dialog.component.css'],
})
export class TaskDialogComponent {
private backupTask: Partial<Task> = { ...this.data.task };
constructor(
public dialogRef: MatDialogRef<TaskDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: TaskDialogData
) {}
cancel(): void {
this.data.task.title = this.backupTask.title;
this.data.task.description = this.backupTask.description;
this.dialogRef.close(this.data);
}
}
TaskDialogComponent
에서 대화상자 참조를 삽입하므로 대화상자를 닫아도 되며 MAT_DIALOG_DATA
토큰과 연결된 제공자의 값도 삽입합니다. 이는 위 AppComponent
에서 open 메서드에 전달한 데이터 객체입니다. 데이터 객체와 함께 전달한 작업의 사본인 비공개 속성 backupTask
도 선언합니다.
사용자가 취소 버튼을 눌렀을 때 this.data.task
속성이 변경되었다면 이를 복원하고 대화상자를 닫아 this.data
를 결과로 전달합니다.
참조했으나 아직 선언하지 않은 두 가지 유형이 있습니다. TaskDialogData
와 TaskDialogResult
입니다. src/app/task-dialog/task-dialog.component.ts
내부에서 파일 하단에 다음 선언을 추가합니다.
src/app/task-dialog/task-dialog.component.ts
...
export interface TaskDialogData {
task: Partial<Task>;
enableDelete: boolean;
}
export interface TaskDialogResult {
task: Task;
delete?: boolean;
}
기능을 준비하기 전에 마지막으로 해야 할 작업은 AppModule
에서 몇 가지 모듈을 가져오는 것입니다.
src/app/app.module.ts
...
import { MatInputModule } from '@angular/material/input';
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatInputModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
이제 '작업 추가' 버튼을 클릭하면 다음과 같은 사용자 인터페이스가 표시됩니다.
7. 앱 스타일 개선
애플리케이션을 시각적으로 멋지게 만들기 위해 스타일을 약간 조정하여 레이아웃을 개선합니다. 구획을 서로 나란히 배치하려고 합니다. '작업 추가' 버튼과 빈 목록 라벨도 약간 조정합니다.
src/app/app.component.css
를 열고 하단에 다음 스타일을 추가합니다.
src/app/app.component.css
mat-toolbar {
margin-bottom: 20px;
}
mat-toolbar > span {
margin-left: 10px;
}
.content-wrapper {
max-width: 1400px;
margin: auto;
}
.container-wrapper {
display: flex;
justify-content: space-around;
}
.container {
width: 400px;
margin: 0 25px 25px 0;
}
.list {
border: solid 1px #ccc;
min-height: 60px;
border-radius: 4px;
}
app-new-task {
margin-bottom: 30px;
}
.empty-label {
font-size: 2em;
padding-top: 10px;
text-align: center;
opacity: 0.2;
}
위 스니펫에서는 툴바와 라벨의 레이아웃을 조정합니다. 또한 너비를 1400px
로, 여백을 auto
로 설정하여 콘텐츠가 가로로 정렬되도록 합니다. 그런 다음 flexbox를 사용하여 구획을 나란히 배치하고, 마지막으로 작업 및 빈 목록을 시각화하는 방법을 조정합니다.
앱이 새로고침되면 다음과 같은 사용자 인터페이스가 표시됩니다.
앱 스타일을 크게 개선했지만 작업을 이동할 때 여전히 성가신 문제가 있습니다.
'우유 구매' 작업을 드래그하기 시작하면 같은 작업에 카드가 두 개 표시됩니다. 하나는 현재 드래그하는 카드고 다른 하나는 구획에 있는 카드입니다. Angular CDK는 이 문제를 해결하는 데 사용할 수 있는 CSS 클래스 이름을 제공합니다.
src/app/app.component.css
하단에 다음 스타일 재정의를 추가합니다.
src/app/app.component.css
.cdk-drag-animating {
transition: transform 250ms;
}
.cdk-drag-placeholder {
opacity: 0;
}
요소를 드래그하는 동안 Angular CDK의 드래그 앤 드롭은 요소를 클론하여 원본을 드롭할 위치에 삽입합니다. 이 요소가 표시되지 않도록 하기 위해 CDK가 자리표시자에 추가할 cdk-drag-placeholder
클래스에 불투명도 속성을 설정합니다.
추가로, 요소를 드롭하면 CDK에서 cdk-drag-animating
클래스를 추가합니다. 요소를 직접 스냅하는 대신 부드러운 애니메이션을 표시하려면 재생 시간을 250ms
로 설정하여 전환을 정의합니다.
작업 스타일도 약간 조정하려고 합니다. task.component.css
에서 호스트 요소의 디스플레이를 block
으로 지정하고 여백을 설정해보겠습니다.
src/app/task/task.component.css
:host {
display: block;
}
.item {
margin-bottom: 10px;
cursor: pointer;
}
8. 기존 작업 수정 및 삭제
기존 작업을 수정하고 삭제하기 위해 이미 구현한 기능 대부분을 재사용하겠습니다. 사용자가 작업을 더블클릭하면 TaskDialogComponent
가 열리고 양식의 두 필드가 작업의 title
과 description
으로 채워집니다.
TaskDialogComponent
에 삭제 버튼도 추가하겠습니다. 사용자가 이 버튼을 클릭하면 삭제 명령이 전달되고 이 명령은 AppComponent
에서 끝납니다.
TaskDialogComponent
에서 변경해야 하는 유일한 사항은 템플릿에 있습니다.
src/app/task-dialog/task-dialog.component.html
<mat-form-field>
...
</mat-form-field>
<div mat-dialog-actions>
...
<button
*ngIf="data.enableDelete"
mat-fab
color="primary"
aria-label="Delete"
[mat-dialog-close]="{ task: data.task, delete: true }">
<mat-icon>delete</mat-icon>
</button>
</div>
이 버튼에는 삭제 머티리얼 아이콘이 표시됩니다. 사용자가 클릭할 때 대화상자를 닫고 객체 리터럴 { task: data.task, delete: true }
를 결과로 전달해보겠습니다. 또한 mat-fab
를 사용하여 버튼을 원형으로 만들고 색상은 기본으로 설정하여 대화상자 데이터에 삭제 기능이 사용 설정된 경우에만 버튼을 표시하겠습니다.
수정 및 삭제 기능의 나머지 구현은 AppComponent
에 있습니다. editTask
메서드를 다음으로 바꿉니다.
src/app/app.component.ts
@Component({ ... })
export class AppComponent {
...
editTask(list: 'done' | 'todo' | 'inProgress', task: Task): void {
const dialogRef = this.dialog.open(TaskDialogComponent, {
width: '270px',
data: {
task,
enableDelete: true,
},
});
dialogRef.afterClosed().subscribe((result: TaskDialogResult|undefined) => {
if (!result) {
return;
}
const dataList = this[list];
const taskIndex = dataList.indexOf(task);
if (result.delete) {
dataList.splice(taskIndex, 1);
} else {
dataList[taskIndex] = task;
}
});
}
...
}
editTask
메서드의 인수를 살펴보겠습니다.
- 개별 구획과 연결된 속성에 상응하는 값을 포함하는 문자열 리터럴 합집합 유형인
'done' | 'todo' | 'inProgress',
유형 목록 - 수정하려는 현재 작업
먼저 메서드 본문에서 TaskDialogComponent
의 인스턴스를 엽니다. 수정할 작업을 지정하는 객체 리터럴을 data
로 전달하고 enableDelete
속성을 true
로 설정하여 양식에서 수정 버튼도 사용 설정합니다.
대화상자에서 결과를 받으면 다음 두 가지 시나리오를 처리합니다.
delete
플래그가true
로 설정된 경우(즉, 사용자가 삭제 버튼을 누른 경우) 상응하는 목록에서 작업이 삭제됩니다.- 또는 주어진 색인의 작업을 대화상자 결과에서 얻은 작업으로 대체합니다.
9. 새 Firebase 프로젝트 만들기
이제 새 Firebase 프로젝트를 만들어보겠습니다.
- Firebase Console로 이동합니다.
- 'KanbanFire'라는 이름으로 새 프로젝트를 만듭니다.
10. 프로젝트에 Firebase 추가
이 섹션에서는 프로젝트를 Firebase와 통합합니다. Firebase팀은 두 기술 간의 통합을 제공하는 @angular/fire
패키지를 제공합니다. 앱에 Firebase 지원을 추가하려면 작업공간의 루트 디렉터리를 열고 다음을 실행합니다.
ng add @angular/fire
이 명령어를 실행하면 @angular/fire
패키지가 설치되고 몇 가지 질문이 표시됩니다. 터미널에 다음과 같이 표시됩니다.
그동안 Firebase 계정으로 인증할 수 있도록 설치 시 브라우저 창이 열립니다. 마지막으로 Firebase 프로젝트를 선택하라는 메시지가 표시되고 디스크에 파일이 생성됩니다.
이제 Firestore 데이터베이스를 만들어야 합니다. 'Cloud Firestore'에서 '데이터베이스 만들기'를 클릭합니다.
그런 다음 테스트 모드에서 데이터베이스를 만듭니다.
마지막으로 리전을 선택합니다.
이제 Firebase 구성을 환경에 추가하기만 하면 됩니다. Firebase Console에서 프로젝트 구성을 찾을 수 있습니다.
- 프로젝트 개요 옆에 있는 톱니바퀴 아이콘을 클릭합니다.
- 프로젝트 설정을 선택합니다.
'내 앱'에서 '웹 앱'을 선택합니다.
이제 애플리케이션을 등록하고 'Firebase 호스팅'을 사용 설정합니다.
'앱 등록'을 클릭하면 구성을 src/environments/environment.ts
에 복사할 수 있습니다.
최종적으로 구성 파일은 다음과 같이 표시됩니다.
src/environments/environment.ts
export const environment = {
production: false,
firebase: {
apiKey: '<your-key>',
authDomain: '<your-project-authdomain>',
databaseURL: '<your-database-URL>',
projectId: '<your-project-id>',
storageBucket: '<your-storage-bucket>',
messagingSenderId: '<your-messaging-sender-id>'
}
};
11. Firestore로 데이터 이동
Firebase SDK를 설정했으므로 이제 @angular/fire
를 사용하여 데이터를 Firestore로 이동합니다. 먼저 AppModule
에서 필요한 모듈을 가져옵니다.
src/app/app.module.ts
...
import { environment } from 'src/environments/environment';
import { AngularFireModule } from '@angular/fire';
import { AngularFirestoreModule } from '@angular/fire/firestore';
@NgModule({
declarations: [AppComponent, TaskDialogComponent, TaskComponent],
imports: [
...
AngularFireModule.initializeApp(environment.firebase),
AngularFirestoreModule
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Firestore를 사용할 것이므로 AppComponent
의 생성자에 AngularFirestore
를 삽입해야 합니다.
src/app/app.component.ts
...
import { AngularFirestore } from '@angular/fire/firestore';
@Component({...})
export class AppComponent {
...
constructor(private dialog: MatDialog, private store: AngularFirestore) {}
...
}
그런 다음 구획 배열을 초기화하는 방법을 업데이트합니다.
src/app/app.component.ts
...
@Component({...})
export class AppComponent {
todo = this.store.collection('todo').valueChanges({ idField: 'id' }) as Observable<Task[]>;
inProgress = this.store.collection('inProgress').valueChanges({ idField: 'id' }) as Observable<Task[]>;
done = this.store.collection('done').valueChanges({ idField: 'id' }) as Observable<Task[]>;
...
}
여기서는 AngularFirestore
를 사용하여 컬렉션의 콘텐츠를 데이터베이스에서 직접 가져옵니다. valueChanges
는 배열 대신 Observable을 반환하고 이 컬렉션의 문서 ID 필드는 Task
인터페이스에서 사용하는 이름과 일치하도록 id
로 호출되어야 한다고 지정합니다. valueChanges
가 반환하는 Observable은 변경될 때마다 작업 컬렉션을 내보냅니다.
배열 대신 Observable을 사용하고 있으므로 작업을 추가, 삭제, 수정하는 방법과 구획 간에 작업을 이동하는 기능을 업데이트해야 합니다. 메모리 내 배열을 변경하는 대신 Firebase SDK를 사용하여 데이터베이스의 데이터를 업데이트합니다.
먼저 재정렬이 어떻게 표시되는지 살펴보겠습니다. src/app/app.component.ts
의 drop
메서드를 다음으로 바꿉니다.
src/app/app.component.ts
drop(event: CdkDragDrop<Task[]>): void {
if (event.previousContainer === event.container) {
return;
}
const item = event.previousContainer.data[event.previousIndex];
this.store.firestore.runTransaction(() => {
const promise = Promise.all([
this.store.collection(event.previousContainer.id).doc(item.id).delete(),
this.store.collection(event.container.id).add(item),
]);
return promise;
});
transferArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex
);
}
위 스니펫에서는 새 코드가 강조표시되어 있습니다. 현재 구획에서 대상 구획으로 작업을 이동하려면 첫 번째 컬렉션에서 작업을 삭제하고 두 번째 컬렉션에 추가합니다. 하나처럼 보이도록 하려는 작업 2개를 실행하므로(즉, 작업을 원자성으로 만듦) Firestore 트랜잭션에서 실행합니다.
이제 Firestore를 사용하도록 editTask
메서드를 업데이트합니다. 닫기 대화상자 핸들러 내에서 다음 코드 줄을 변경해야 합니다.
src/app/app.component.ts
...
dialogRef.afterClosed().subscribe((result: TaskDialogResult|undefined) => {
if (!result) {
return;
}
if (result.delete) {
this.store.collection(list).doc(task.id).delete();
} else {
this.store.collection(list).doc(task.id).update(task);
}
});
...
Firestore SDK를 사용하여 조작하는 작업에 상응하는 대상 문서에 액세스하여 삭제 또는 업데이트합니다.
마지막으로 새 작업을 만드는 메서드를 업데이트해야 합니다. this.todo.push('task')
를 this.store.collection('todo').add(result.task)
로 바꿉니다.
이제 컬렉션이 배열이 아니라 Observable입니다. 이를 시각화하려면 AppComponent
템플릿을 업데이트해야 합니다. todo
, inProgress
, done
속성의 모든 액세스를 각각 todo | async
, inProgress | async
, done | async
로 교체하면 됩니다.
비동기 파이프는 컬렉션과 연결된 Observable을 자동으로 구독합니다. Observable에서 새 값을 내보내면 Angular는 변경 감지를 자동으로 실행하여 내보낸 배열을 처리합니다.
예를 들어 todo
구획에서 변경해야 하는 사항을 살펴보겠습니다.
src/app/app.component.html
<mat-card
cdkDropList
id="todo"
#todoList="cdkDropList"
[cdkDropListData]="todo | async"
[cdkDropListConnectedTo]="[doneList, inProgressList]"
(cdkDropListDropped)="drop($event)"
class="list">
<p class="empty-label" *ngIf="(todo | async)?.length === 0">Empty list</p>
<app-task (edit)="editTask('todo', $event)" *ngFor="let task of todo | async" cdkDrag [task]="task"></app-task>
</mat-card>
데이터를 cdkDropList
지시어에 전달하면 비동기 파이프가 적용됩니다. *ngIf
지시어 내부와 동일하지만 todo | async
가 null
또는 undefined
가 아닌 경우 런타임 오류가 발생하지 않도록 length
속성에 액세스할 때 체이닝(Angular에서는 안전한 탐색 연산자라고도 함)도 선택적으로 사용합니다.
이제 사용자 인터페이스에서 새 작업을 만들고 Firestore를 열면 다음과 같이 표시됩니다.
12. 낙관적 업데이트 개선
애플리케이션에서 현재 낙관적 업데이트를 실행하고 있습니다. Firestore에 정보 소스가 있지만 동시에 작업의 로컬 사본이 있습니다. 컬렉션과 연결된 Observable을 내보내면 작업 배열이 반환됩니다. 사용자 작업이 상태를 변경하면 먼저 로컬 값을 업데이트하고 변경사항을 Firestore에 전파합니다.
한 구획에서 다른 구획으로 작업을 이동하면 각 구획의 작업을 나타내는 배열의 로컬 인스턴스에서 작동하는 transferArrayItem,
이 호출됩니다. Firebase SDK는 이러한 배열을 변경 불가능으로 처리합니다. 즉, 다음에 Angular가 변경 감지를 실행할 때 새 인스턴스를 가져와 작업을 전송하기 전에 이전 상태를 렌더링합니다.
동시에 Firestore 업데이트가 트리거되고 Firebase SDK는 올바른 값으로 업데이트를 트리거하므로 사용자 인터페이스가 몇 밀리초 후에 올바른 상태가 됩니다. 따라서 방금 전송한 작업이 첫 번째 목록에서 다음 목록으로 이동합니다. 아래 GIF에서 확인할 수 있습니다.
이 문제를 해결하는 올바른 방법은 애플리케이션마다 다르지만 모든 경우에 데이터가 업데이트될 때까지 일관된 상태가 유지되도록 해야 합니다.
valueChanges
에서 수신하는 원래 관찰자를 래핑하는 BehaviorSubject
를 활용할 수 있습니다. 내부적으로 BehaviorSubject
는 transferArrayItem
의 업데이트를 유지하는 변경 가능한 배열을 유지합니다.
수정사항을 구현하려면 AppComponent
를 업데이트하기만 하면 됩니다.
src/app/app.component.ts
...
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { BehaviorSubject } from 'rxjs';
const getObservable = (collection: AngularFirestoreCollection<Task>) => {
const subject = new BehaviorSubject<Task[]>([]);
collection.valueChanges({ idField: 'id' }).subscribe((val: Task[]) => {
subject.next(val);
});
return subject;
};
@Component(...)
export class AppComponent {
todo = getObservable(this.store.collection('todo')) as Observable<Task[]>;
inProgress = getObservable(this.store.collection('inProgress')) as Observable<Task[]>;
done = getObservable(this.store.collection('done')) as Observable<Task[]>;
...
}
위 스니펫에서는 컬렉션과 연결된 Observable이 발생될 때마다 값을 내보내는 BehaviorSubject
를 만들기만 합니다.
모두 예상대로 작동합니다. BehaviorSubject
가 변경 감지 호출에서 배열을 재사용하고 Firestore에서 새 값을 가져올 때만 업데이트되기 때문입니다.
13. 애플리케이션 배포
앱을 배포하려면 다음을 실행하기만 하면 됩니다.
ng deploy
이 명령어는 다음 작업을 실행합니다.
- 컴파일 시간 최적화를 적용하여 프로덕션 구성으로 앱을 빌드합니다.
- Firebase 호스팅에 앱을 배포합니다.
- 결과를 미리 볼 수 있도록 URL을 출력합니다.
14. 마무리 단계
수고하셨습니다. Angular와 Firebase를 사용하여 간반 보드를 빌드했습니다.
다양한 작업의 상태를 나타내는 열이 3개 있는 사용자 인터페이스를 만들었습니다. Angular CDK를 사용하여 열 간에 작업의 드래그 앤 드롭을 구현했습니다. 그런 다음, Angular Material을 사용하여 새 작업을 만들고 기존 작업을 수정하는 양식을 만들었습니다. @angular/fire
사용 방법을 알아보고 모든 애플리케이션 상태를 Firestore로 옮겼습니다. 마지막으로 애플리케이션을 Firebase 호스팅에 배포했습니다.
다음 단계
테스트 구성을 사용하여 애플리케이션을 배포했습니다. 앱을 프로덕션으로 배포하기 전에 올바른 권한을 설정했는지 확인하세요. 여기에서 방법을 알아볼 수 있습니다.
현재 Google에서는 특정 구획에서 개별 작업의 순서를 유지하지 않습니다. 이를 구현하려면 작업 문서의 순서 필드를 사용하고 이 필드를 기반으로 하여 정렬하면 됩니다.
또한 단일 사용자 전용 간반 보드를 만들었으며, 이는 앱을 여는 사용자에게 제공되는 단일 간반 보드가 생겼다는 뜻입니다. 앱의 여러 사용자를 위한 별도의 보드를 구현하려면 데이터베이스 구조를 변경해야 합니다. 여기에서 Firestore의 권장사항을 알아보세요.