Angular CLI 사이트에 REST API 서버 추가하기

소스코드

이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실행할 수 있습니다.

이 게시물의 소스코드는 Tour of Heroes / HTTP에서 이어집니다.

tour-of-heroes.git 을 clone 한 적이 있는 경우: 터미널에서 해당 폴더로 이동 후 아래 명령어들을 붙여넣기합니다. 폴더 내 모든 코드가 이 게시물의 코드로 교체됩니다. 이를 원치 않으시면 이 방법을 선택하지 마세요.

git reset --hard
git pull
git reset --hard 5febff1
git reset --soft 58820f3
npm install
atom .

tour-of-heroes.git 을 clone 한 적이 없는 경우: 터미널에서 코드를 다운 받을 폴더로 이동한 후 아래 명령어들을 붙여넣기하여 tour-of-heroes.git 을 clone 합니다.

git clone https://github.com/a-mean-blogger/tour-of-heroes.git
cd tour-of-heroes
git reset --hard 5febff1
git reset --soft 58820f3
npm install
atom .

- Github에서 소스코드 보기: https://github.com/a-mean-blogger/tour-of-heroes/tree/5febff197d61a2970ca102b2534f6d70aa21706d


Angular 2/Tour of Heroes에서 Angular로 사이트를 만드는 방법을, Node JS API/기본 REST API 만들기에서 API를 만드는 방법을 알아보았습니다. 이처럼 두 개의 분리된 프로젝트형식은 프론트 엔드 개발자와 백엔드 개발자가 나누어져 있을때 유용한 방식입니다. 만약 혼자서 전체를 개발하는 경우에는 두개로 나누어져있으면 오히려 번거로울 수도 있습니다. 

이번 포스팅에서는 Angular 2/Tour of Heroes의 코드를 베이스로 하고 여기에 Node JS API/기본 REST API 만들기의 코드를 더해서 하나의 서버로 Angular 사이트와 Node JS REST API를 실행하는 방법에 대해 알아보겠습니다

이번 강좌는 단편강좌/Angular CLI 사이트 Heroku(헤로쿠)로 인터넷에 올리기와도 연관성이 많습니다. Angular CLI 사이트를 헤로쿠에 올리려면 서버를 만들어 주어야 하기 때문이죠. 위 강좌와 중복되는 부분도 있지만 포스팅의 독립성을 위해서 그 부분들도 이 포스팅에서 다시 한번 설명하였습니다.

Angular CLI 프로젝트는 개발(development) 단계에서 ng server 명령어를 통해 사이트 실행합니다. 이 명령어는 컴퓨터의 임의의 위치에 Angular 파일들을 생성하고 임시 서버(기본설정: localhost:4200)를 만들어 그 파일들을 접근가능하게 하고, 코드에 수정이 있으면 실시간으로 서버의 파일을 수정하여 사이트에 반영하는 역할을 합니다. 사이트를 운영(production) 단계에서는 사이트의 성능을 떨어트리기 때문에 사용하지 말아야 합니다.

Angular CLI 프로젝트는 아래 명령어를 통해서 production용 사이트 파일들을 얻을 수 있습니다.

$ ng build --prod

이 명령어를 실행하면 프로젝트_폴더/dist 위치에 index.html을 비롯한 angular 관련 static 파일들이 생성됩니다. Angular 사이트는 static 파일들로만 구성되어 있고 그 파일들을 서버에 올려 index.html에 접근가능하게 하면 Angular 사이트가 바로 작동합니다. 

Node JS API/기본 REST API 만들기 이미 Node JS 서버코드가 있으므로 Angular 2/Tour of Heroes 프로젝트로 이 코드를 옮기고 프로젝트_폴더/dist의 index.html를 접근하게 하면 두개의 프로젝트를 하나의 서버에서 작동하게 할 수 있습니다.

물론 위 이야기는 production 단계에서의 이야기입니다. 개발중에는 Angular 코드가 변화면 그때그때 새로 사이트를 업데이트해주는 기능이 있는 것이 편합니다. 즉 개발중에는 두개의 서버로, production 단계에서는 하나의 서버로 만드는 것이 이 포스팅의 첫번째 목표입니다.

개발(development) 환경

  • REST API 서버: 서버(localhost:3000)에서 구동
  • Angular 사이트: ng server를 통해 다른 서버(localhost:4200)에서 작동. REST API 서버(localhost:3000)로 API 요청을 보냄 

운영(production) 환경

  • REST API 서버: 서버(localhost:3000)에서 구동
  • Angular 사이트: ng build --prod를 통해 Angular 사이트 static 파일들이 REST API 서버에 올려짐. 동일한 서버로 API 요청을 보냄

두번째 목표는 Angular 사이트가 현재 개발환경에 맞게 각각의 주소로 API 요청을 하게 하는 것입니다.

Package 설치

현재 Angular 2/Tour of Heroes 코드는 서버 코드에서 필요한 package들이 설치되어 있지 않습니다. 아래 명령어를 입력하여 서버 코드에서 사용되는 pakcage들을 설치합시다

$ npm install express mongoose body-parser --save

폴더 구조

두 개의 프로젝트가 합쳐지면서 파일들이 너무 많아져서 새로추가된 파일(초록색)과 수정된 파일(주황색)만 표시했습니다.


Angular 2/Tour of Heroes 강의에서는 살펴보지 않았던 environments 폴더의 파일들이 수정됩니다. 이 environments의 파일들로 부터 Angular의 실행환경(development, production 등)에 따라 다른 변수값을 가져올 수 있습니다.

코드

server 폴더의 다른 파일들은 Node JS API/기본 REST API 만들기의 파일을 그대로 가져왔기 때문에 따로 살펴보지 않고 server.js파일만 살펴보겠습니다.

// server/server.js
var express = require('express');
var app     = express();
var path    = require('path');
var mongoose   = require('mongoose');
var bodyParser = require('body-parser');

// Database
mongoose.Promise = global.Promise;
mongoose.connect(process.env.MONGO_DB, {useMongoClient: true});
var db = mongoose.connection;
db.once('open', function () {
   console.log('DB connected!');
});
db.on('error', function (err) {
  console.log('DB ERROR:', err);
});

// Middlewares
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(function (req, res, next) {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'content-type');
  next();
});

// API
app.use('/api/heroes', require('./api/heroes')); //*

// Angular
app.use(express.static(path.resolve(__dirname, '../dist'))); //1
app.get('*', function (req, res) { //2
  var indexFile = path.resolve(__dirname,'../dist/index.html');
  res.sendFile(indexFile);
});

// Server
var port = 3000;
app.listen(port, function(){
  console.log('listening on port:' + port);
});

server.js파일도 // Angular 부분을 제외하면 기존의 파일과 동일합니다.

1. 프로젝트_폴더/dist를 static 파일 위치로 만듭니다. 그 위치에 angular 파일들이 생성되기 때문입니다.

2. 모든 요청을 Angular의 index.html파일로 전달합니다. API에 관련된 요청은 //*에서 먼저 처리되기 때문에 API 요청을 제외한 모든 route 이 Angular로 전달됩니다. 그렇기 때문에 이 코드는 API route 코드(//*) 밑에 있어야 합니다.

서버에 관련된 코드는 server폴더 안에 있는 것들 뿐이고 다음은 Angular에 관련된 코드들입니다.

// src/app/app.module.ts

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule }   from '@angular/forms';
import { HttpModule }    from '@angular/http';

import { AppComponent }        from './app.component';
import { DashboardComponent }  from './dashboard.component';
import { HeroDetailComponent } from './hero-detail.component';
import { HeroesComponent }     from './heroes.component';
import { HeroService }         from './hero.service';
import { HeroSearchComponent } from './hero-search.component';

import { AppRoutingModule }    from './app-routing.module';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    DashboardComponent,
    HeroDetailComponent,
    HeroesComponent,
    HeroSearchComponent
  ],
  providers: [ HeroService ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

app. module.ts에서 추가된 부분은 없고, 이전 강의에서 사용됬던 (In Memory Web Api Module) 부분을 삭제하였습니다. 진짜 API를 사용하기 때문에 더이상 가짜 API를 사용할 필요가 없죠. 

environments 폴더의 파일들을 살펴봅시다.

// src/environments/environment.prod.ts

export const environment = {
  production: true,
  apiBaseUrl:"api",
};

// src/environments/environment.ts

export const environment = {
  production: false,
  apiBaseUrl:"http://localhost:3000/api",
};

 ng build --prod를 실행하면 Angular 사이트는 environment.prod.ts의 값을, ng server를 실행하면 environment.ts의 값을 읽어오게 됩니다. 개발환경에서는 서버가 두개이므로 API의 절대주소를, 운영환경에서는 서버가 하나이므로 API의 상대주소를 적습니다. 개발환경에 따라 Angular사이트에 달라지는 값이 있다면 여기에 적어주면 됩니다.

// src/app/hero.service.ts

import { environment } from '../environments/environment'; //1
import { Injectable }    from '@angular/core';
import { Headers, Http } from '@angular/http';

//...

@Injectable()
export class HeroService {
  private apiBaseUrl = environment.apiBaseUrl; //2

  private headers = new Headers({'Content-Type': 'application/json'});
  private heroesUrl = `${this.apiBaseUrl}/heroes`;  // URL to web api //3

  constructor(private http: Http) { }

  // ...
}

 1. environment 오브젝트를 가져옵니다. Angular 실행환경(production, development)에 따라 사이트가 알아서 environment.ts 혹은 environment.prod.ts에서 값을 읽어옵니다.

2. apiBaseUrl을 가져옵니다.

3. apiBaseUrl을 이용해서 heroes API의 url을 만듭니다.

// src/app/hero-search.service.ts

import { environment } from '../environments/environment'; //1
import { Injectable } from '@angular/core';
import { Http }       from '@angular/http';

// ...

@Injectable()
export class HeroSearchService {
  private apiBaseUrl = environment.apiBaseUrl; //2

  constructor(private http: Http) {}

  search(term: string): Observable<Hero[]> {
    return this.http
               .get(`${this.apiBaseUrl}/heroes/?name=${term}`) //3
               .map(response => response.json().data as Hero[]);
  }
}

search service 에서도 비슷한 방법으로 처리됩니다. 형태가 동일하지 않은건 원래 tour of heroes 예제가 그렇습니다.

이것으로 Angular 사이트에 관련된 코드도 모두 수정하였습니다. 마지막으로 package.json을 수정합시다.

// package.json

{
  "name": "tour-of-heroes",
  "version": "0.0.0",
  "license": "MIT",
  "scripts": {
    "ng": "ng",
    "start": "node ./server/server.js", //1
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "local": "ng serve | nodemon" //2
  },
  // ...
}

1. production 환경에서는 npm start 명령을 통해 사이트를 실행할 수 있습니다. npm start는 package.json의 start 스크립트를 실행시킵니다. 이 부분을 node 명령어로 서버를 실행시키게 합니다. production 환경에서 ng server로 Angular 사이트를 실행시키지 않은 것과 같은 이유로 production 환경에서는 nodemon으로 사이트를 실행시키지 말아야 합니다.

2. local script는 사용자 정의 스크립트입니다(즉 이름을 원하는대로 바꿔도 됩니다) 사용자 정의 스크립트는

$ npm run 스크립트_이름

를 통해 실행시킬 수 있습니다. nodemon과 ng server를 동시에 실행하게 됩니다.

실행 결과

개발(development) 환경

아래 명령어를 입력합니다.

$ npm run local

local script에 의해 nodemon과 ng server가 동시에 실행되며 사이트는 ng server에 의해서 localhost:4200 에서 접근할 수 있습니다. 서버 파일에 수정이 있는 경우에는 nodemon에 의해 REST API 서버가 재실행되고, Angular 파일에 수정이 있는 경우에는 ng server에 의해 Angular사이트가 재조립됩니다. 

운영(production)  환경

아래 명령어를 입력하여 Angular 사이트 static 파일들을 생성합니다.

ng build --prod

Angular 사이트 static 파일들이 생성되고 나면 아래 명령어로 서버를 실행시킵니다.

$ npm start

마치며..

프로젝트를 하나로 합치면 헤로쿠(heroku)를 통해 프로젝트를 인터넷에 올리기도 훨씬 간편합니다. 다만 프로젝트가 node js와 Angular를 모두 가지고 있으므로 단편강좌/Node js 사이트 Heroku(헤로쿠)로 인터넷에 올리기와 단편강좌/Angular CLI 사이트 Heroku(헤로쿠)로 인터넷에 올리기의 변화가 모두 적용되어야 합니다. 설명은 따로 하지 않고 소스코드는 여기에서 볼 수 있습니다. 프로젝트 폴더에서 아래 명령어를 사용하면 헤로쿠용 코드로 변환됩니다만 기존의 코드들이 모두 사라지므로 주의하세요

$ git reset --hard 5573fd4

댓글

유문상 2017.11.14
강의 잘 보고 있습니다! 이게 최초의 Angular부터 Mongo까지 왔다갔다 하는 MEAN 완성본 이로군요..! 이 원리를 그대로 해봤던 서버단에서 게시판 만들기와 angular로 게시판 만들기 두 강좌에 적용하면 MEAN한 게시판이 완성 되겠죠?^^
I
Ian H 2017.11.15
@유문상,
네 맞습니다. Angular의 게시판 코드는 직접 작성하셔야 하지만 기존의 강의 내용을 활용하면 만드실 수 있습니다.
H
Hyunsung Kim 2017.12.02
ng serve로 angular 실행하면 컴파일 이상없이 되고 페이지도 작동 잘 되는데 build만 하면 에러가 나네요. ERROR in ./src/main.ts Module not found: Error: Can't resolve './$$_gendir/app/app.module.ngfactory' in 폴더 @ ./src/main.ts 4:0-74 @ multi ./src/main.ts
I
Ian H 2017.12.03
@Hyunsung Kim,
ng serve시에는 view에 오류가 있어도 검사하지 않고 컴파일됩니다. 몇몇 타입스크립트 오류들은 자바스크립트로 컴파일되고 나면 더이상 오류가 아니게 되어 발견할 수 없는 경우도 있습니다. 예를 들어 component 에 존재하지 않는 항목을 view에서 표시하려고 하면 타입스크립트상으로는 오류이지만 자바스크립트는 없는 항목을 호출하는 것이 오류가 아니기 때문에 결국 발견하지 못하게 됩니다.(자바스크립트에서는 var obj = {a:1}; 후에 console.log(obj.b)를 해도 오류가 아닌거 처럼요) 
ng build --prod를하면 aot라고 해서 ts파일과 view파일간의 오류들도 모두 확인하게 됩니다.  오류를 찾아서 수정하거나 ng build --prod --aot=false 로 aot를 꺼주면 됩니다.
비밀s 2018.01.31
안녕하세요. 제가 초보다 보니 초보적인 질문일 수 있습니다만.. "Angular 2/Tour of Heroes에서 Angular로 사이트를 만드는 방법을, Node JS API/기본 REST API 만들기에서 API를 만드는 방법을 알아보았습니다. 이처럼 두 개의 분리된 프로젝트형식은 프론트 엔드 개발자와 백엔드 개발자가 나누어져 있을때 유용한 방식입니다. 만약 혼자서 전체를 개발하는 경우에는 두개로 나누어져있으면 오히려 번거로울 수도 있습니다." 라는 문단에서요. 만약 혼자서 전체를 개발하는경우, Angular만가지고 db연동을 할 수 있나요? 프론트엔드와 백엔드를 나누지않고 하는 방법이 있는것인지 궁금합니다.
I
Ian H 2018.02.01
@비밀s,
안녕하세요^^ 일단 질문에 답을 드리자면 Angular만 가지고 db연동을 할 수 없습니다.
Angular는 프론트엔드 프레임워크입니다. Angular는 사이트 접속자(client)의 브라우저에서 실행되고 서버의 DB에 직접 접근할 수 없습니다. 
위 단편강의는 하나의 프로젝트로 서버 API 프로젝트와 Angular 프로젝트를 묶고, 동시에 실행할 수 있는 방법이고, 결국 Angular는 서버 API를 호출하여 DB와 연결하게 됩니다.
댓글쓰기

이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기

UP