Angular + MySQL + Node.js CRUD Application 구현하기
Angular, MySQL, Node.js를 이용하여 CRUD Application을 구현하는 방법에 대해 알아보겠습니다.
구현 환경은 다음의 버전을 기준으로 구성하였습니다.
- Node.js v12.13.1
- MySQL v5.7.3
- Angular v11.2.4
구현에 앞서 다음 포스팅을 참고하여 서버를 먼저 구성해주어야 합니다.
1. 프로젝트 생성 & 디렉터리 구조 설정
1.1. Angular 프로젝트 생성
Angular CLI를 이용하여 Angular 프로젝트를 생성해줍니다.
$ ng new angular-frontend
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? CSS
1.2. 디렉터리 구조 설정
프로젝트를 생성한 이후엔 다음과 같이 src 하위의 디렉터리 구조를 설정해줍니다. 파일은 TypeScript(.ts)를 베이스로 하였습니다.
src
├── app
│ ├── app.component.css
│ ├── app.component.html
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── app-routing.module.ts
│ └── tutorial
│ ├── tutorial-add
│ │ ├── tutorial-add.component.html
│ │ └── tutorial-add.component.ts
│ ├── tutorial-detail
│ │ ├── tutorial-detail.component.html
│ │ └── tutorial-detail.component.ts
│ ├── tutorial-list
│ │ ├── tutorial-list.component.html
│ │ └── tutorial-list.component.ts
│ ├── tutorial.module.ts
│ ├── tutorial.service.ts
│ └── tutorial.value.ts
├── ...
└── index.html
1.3. index.html 설정
index.html에는 application 실행시 호출할 root component(AppComponent)를 추가해주고 bootstrap 사용을 위해 cdn을 추가해줍니다.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>angular-study</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="//unpkg.com/bootstrap@4/dist/css/bootstrap.min.css">
</head>
<body>
<app-root></app-root>
</body>
</html>
2. Module 설정
2.1. AppModule 설정
src/app.module.ts 파일을 다음과 같이 작성해줍니다. bootstrap에는 브라우저가 index.html 파일을 읽어서 application을 실행할 때 사용할 component를 작성해줍니다.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
루트 모듈인 AppModule에 추가한 외부 모듈의 역할은 다음과 같습니다.
- BrowserModule
브라우저에서 앱이 동작할 때 필요한 인프라를 제공하는 모듈 - AppRoutingModule
라우팅 관련 디렉티브 및 모듈 - FormsModule
폼 관련 디렉티브 및 모듈 - HttpClientModule
HTTP 통신 관련 모듈
2.2. AppRoutingModule 설정
라우팅을 위해 src/app-routing.module.ts 파일을 다음과 같이 작성해줍니다. AppRoutingModule에서는 RouterModule에 forRoot()로 라우트 정보를 설정해줍니다. routes 설정 정보에는 TutorialModule을 loadChildren(lazy loading)으로 import 해줍니다.
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { TutorialModule } from './tutorial/tutorial.module';
const routes: Routes = [
{
path: '',
loadChildren: () => TutorialModule
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
2.3. TutorialModule 설정
AppRoutingModule에서 import한 TutorialModule 설정을 위해 src/app/tutorial/tutorial.module.ts 파일을 다음과 같이 작성해줍니다. 각각의 url path 별로 component를 나누어 설정해줍니다. TutorialModule은 lazy loading으로 import 되었기 때문에 forChild()를 이용하여 라우팅을 설정해줍니다.
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { TutorialListComponent } from 'src/app/tutorial/tutorial-list/tutorial-list.component';
import { TutorialDetailComponent } from 'src/app/tutorial/tutorial-detail/tutorial-detail.component';
import { TutorialAddComponent } from 'src/app/tutorial/tutorial-add/tutorial-add.component';
import { TutorialService } from 'src/app/tutorial/tutorial.service';
@NgModule({
imports: [
FormsModule,
ReactiveFormsModule,
CommonModule,
RouterModule.forChild([
{
path: '',
redirectTo: 'tutorial/list',
pathMatch: 'full'
},
{
path: 'tutorial/list',
component: TutorialListComponent
},
{
path: 'tutorial/add',
component: TutorialAddComponent
},
{
path: 'tutorial/:id',
component: TutorialDetailComponent
},
])
],
declarations: [
TutorialListComponent,
TutorialAddComponent
TutorialDetailComponent
],
providers: [
TutorialService
]
})
export class TutorialModule {}
3. Model 설정
application에서 사용할 데이터의 처리를 위해 Model을 만들어줍니다. src/app/tutorial/tutorial.model.ts 파일을 다음과 같이 작성해줍니다. 모델의 클래스명은 Tutorial로 지정하였습니다.
export class Tutorial {
id?: string;
title?: string;
description?: string;
published?: boolean;
}
4. Service 설정
application에서 HTTP 요청을 위해 사용할 Service를 만들어줍니다. src/app/tutorial/tutorial.service.ts 파일을 다음과 같이 작성해줍니다. CRUD 기능에 따라 메서드를 나눠주고 HTTP request를 위해 Angular의 HttpClient를 사용해줍니다.
서비스에는 @Injectable() 데코레이터를 사용하여 같은 모듈내의 component에서 호출할 수 있도록 해줍니다.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Tutorial } from 'src/app/tutorial/tutorial.value';
const baseUrl = 'http://localhost:8080/api/tutorial';
@Injectable()
export class TutorialService {
constructor(private http: HttpClient) {}
// Retrieve all tutorials
public getTutorialList(): Observable<Tutorial[]> {
return this.http.get<Tutorial[]>(baseUrl);
}
// Retrieve tutorial by id
public getTutorial(id: string): Observable<Tutorial> {
return this.http.get(`${baseUrl}/${id}`);
}
// Search tutorial by keyword
public searchTutorial(keyword: string): Observable<Tutorial[]> {
return this.http.get<Tutorial[]>(`${baseUrl}?keyword=${keyword}`);
}
// Create tutorial
public createTutorial(data: Tutorial): Observable<any> {
return this.http.post(baseUrl, data);
}
// Update tutorial by id
public updateTutorial(id: string, data: Tutorial): Observable<any> {
return this.http.put(`${baseUrl}/${id}`, data);
}
// Delete tutorial by id
public deleteTutorial(id: string): Observable<any> {
return this.http.delete(`${baseUrl}/${id}`);
}
}
5. Component 설정
5.1. TutorialListComponent 설정
tutorial 목록 조회를 위한 component 입니다. src/app/tutorial/tutorial-list/tutorial-list.component.ts 파일을 다음과 같이 작성해줍니다.
import { Component, OnInit } from '@angular/core';
import { Tutorial } from 'src/app/tutorial/tutorial.value';
import { TutorialService } from 'src/app/tutorial/tutorial.service';
@Component({
selector: 'app-tutorial-list',
templateUrl: './tutorial-list.component.html',
styleUrls: []
})
export class TutorialListComponent implements OnInit {
public tutorialList: Tutorial[] = [];
public tutorial: Tutorial = new Tutorial();
public keyword = '';
constructor(private tutorialService: TutorialService) {}
ngOnInit(): void {
this.retrieveTutorialList();
}
// Retrieve all tutorials
public retrieveTutorialList(): void {
this.tutorialService.getTutorialList()
.subscribe(response => {
if (response) {
this.tutorialList = response;
} else {
console.log(response);
}
});
}
// Search tutorial by keyword
public searchTutorial(): void {
this.tutorialService.searchTutorial(this.keyword)
.subscribe(response => {
if (response) {
this.tutorialList = response;
} else {
console.log(response);
}
});
}
}
view 템플릿 구성을 위해 src/app/tutorial/tutorial-list/tutorial-list.component.html 파일을 다음과 같이 작성해줍니다.
<div class="list row">
<div class="col-md-8">
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="Please enter search keyword" [(ngModel)]="keyword"/>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" (click)="searchTutorial()">Search</button>
</div>
</div>
</div>
<div class="col-md-8">
<h4>Tutorial List</h4>
<ul class="list-group">
<li class="list-group-item"
*ngFor="let tutorial of tutorialList;"
routerLink="/tutorial/{{tutorial.id}}">
{{tutorial.title}}
</li>
</ul>
</div>
</div>
5.2. TutorialDetailComponent 설정
단일 tutorial의 조회, 수정, 삭제를 위한 component 입니다. src/app/tutorial/tutorial-detail/tutorial-detail.component.ts 파일을 다음과 같이 작성해줍니다.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Tutorial } from 'src/app/tutorial/tutorial.value';
import { TutorialService } from 'src/app/tutorial/tutorial.service';
@Component({
selector: 'app-tutorial-detail',
templateUrl: './tutorial-detail.component.html',
styleUrls: []
})
export class TutorialDetailComponent implements OnInit {
public tutorial: Tutorial = new Tutorial();
constructor(private tutorialService: TutorialService,
private route: ActivatedRoute,
private router: Router) {}
ngOnInit(): void {
this.getTutorial(this.route.snapshot.params.id);
}
// Retrieve tutorial by id
public getTutorial(id: string): void {
this.tutorialService.getTutorial(id)
.subscribe(response => {
if (response) {
this.tutorial = response;
} else {
console.log(response);
}
});
}
// Update tutorial by id
public updateTutorial(): void {
if (!this.tutorial.id) {
return;
}
this.tutorialService.updateTutorial(this.tutorial.id, this.tutorial)
.subscribe(response => {
if (response) {
this.router.navigate(['/tutorial/list']);
} else {
console.log(response);
}
});
}
// Delete tutorial by id
public deleteTutorial(): void {
if (!this.tutorial.id) {
return;
}
this.tutorialService.deleteTutorial(this.tutorial.id)
.subscribe(response => {
if (response) {
this.router.navigate(['/tutorial/list']);
} else {
console.log(response);
}
});
}
// Update tutorial status
public updatePublished(status: boolean): void {
if (!this.tutorial.id) {
return;
}
this.tutorialService.updateTutorial(this.tutorial.id, this.tutorial)
.subscribe(response => {
if (response) {
this.tutorial.published = status;
} else {
console.log(response);
}
});
}
}
view 템플릿 구성을 위해 src/app/tutorial/tutorial-detail/tutorial-detail.component.html 파일을 다음과 같이 작성해줍니다.
<div class="col-md-8">
<div *ngIf="tutorial.id" class="edit-form">
<h4>Tutorial Detail</h4>
<form>
<div class="form-group">
<label for="title">Title</label>
<input type="text" name="title" class="form-control" id="title" [(ngModel)]="tutorial.title"/>
</div>
<div class="form-group">
<label for="description">Description</label>
<input type="text" name="description" class="form-control" id="description" [(ngModel)]="tutorial.description"/>
</div>
<div class="form-group">
<label>Status</label>
<p><strong>{{tutorial.published ? 'Published' : 'Pending'}}</strong></p>
</div>
</form>
<button class="btn btn-primary mr-2" *ngIf="tutorial.published" (click)="updatePublished(false)">UnPublish</button>
<button class="btn btn-primary mr-2" *ngIf="!tutorial.published" (click)="updatePublished(true)">Publish</button>
<button class="btn btn-success mr-2" (click)="updateTutorial()">Update</button>
<button class="btn btn-danger mr-2" (click)="deleteTutorial()">Delete</button>
</div>
</div>
5.3. TutorialAddComponent 설정
tutorial 추가를 위한 component 입니다. src/app/tutorial/tutorial-add/tutorial-add.component.ts 파일을 다음과 같이 작성해줍니다.
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Tutorial } from 'src/app/tutorial/tutorial.value';
import { TutorialService } from 'src/app/tutorial/tutorial.service';
@Component({
selector: 'app-tutorial-add',
templateUrl: './tutorial-add.component.html',
styleUrls: []
})
export class TutorialAddComponent implements OnInit {
public tutorial: Tutorial = new Tutorial();
constructor(private tutorialService: TutorialService,
private router: Router) {}
ngOnInit(): void {
}
// Create tutorial
public createTutorial(): void {
this.tutorialService.createTutorial(this.tutorial)
.subscribe(response => {
if (response) {
this.router.navigate(['/tutorial/list']);
} else {
console.log(response);
}
});
}
}
view 템플릿 구성을 위해 src/app/tutorial/tutorial-add/tutorial-add.component.html 파일을 다음과 같이 작성해줍니다.
<div class="col-md-8">
<div class="submit-form">
<div>
<h4>Tutorial Add</h4>
<div class="form-group">
<label for="title">Title</label>
<input type="text" name="title" class="form-control" id="title" required [(ngModel)]="tutorial.title"/>
</div>
<div class="form-group">
<label for="description">Description</label>
<input name="description" class="form-control" id="description" required [(ngModel)]="tutorial.description"/>
</div>
<button class="btn btn-success" (click)="createTutorial()">Submit</button>
</div>
</div>
</div>
6. 실행 및 테스트
6.1. MySQL Server 실행
앞서 생성한 MySQL Server 실행을 위해 mysql-server.ts 파일을 Node.js로 실행해줍니다.
$ node mysql-server.ts
서버가 정상적으로 실행되면 다음과 같이 메시지가 출력됩니다.
브라우저에서 http://localhost:8080 url로 접속하면 다음과 같이 화면에 메시지가 출력되는 것을 확인할 수 있습니다.
6.2. Angular Application 실행
npm으로 패키지 설치 후에 application을 실행해줍니다.
$ npm install
$ npm start
브라우저에서 http://localhost:4200 url로 접속하면 다음과 같이 화면이 나타나는 것을 확인할 수 있습니다.
6.3. API 테스트
브라우저에서 기능 동작을 확인해보면서 구성한 API를 테스트 해줍니다.
6.3.1. Tutorial 생성 (POST)
6.3.2. Tutorial 조회 (전체) (GET)
6.3.3. Tutorial 조회 (단일) (GET)
6.3.4. Tutorial 조회 (검색) (GET)
6.3.5. Tutorial 수정 (PUT)
6.3.6. Tutorial 삭제 (DELETE)
이상으로 Angular, MySQL, Node.js를 이용하여 CRUD Application을 구현하는 방법에 대해 알아봤습니다.
※ Reference
- bezkoder.com, Angular 11 CRUD Application example with Web API, bezkoder.com/angular-11-crud-app/
- bezkoder.com, Angular 11 + Node.js Express + MySQL example: CRUD Application, bezkoder.com/angular-11-node-js-express-mysql/
- js2prince.tistory.com, Angular Component, js2prince.tistory.com/entry/Angular-Component
- steadev.tistory.com, [Angular] Lazy-loading, steadev.tistory.com/49
- skout90.github.io, 04. Angular 모듈, skout90.github.io/2017/07/12/Angular/4.%20Angular%20%EB%AA%A8%EB%93%88/#
- skout90.github.io, 07. Angular 라우터, skout90.github.io/2017/07/12/Angular/7.%20Angular%20%EB%9D%BC%EC%9A%B0%ED%84%B0/