이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실행할 수 있습니다.
- Github에서 소스코드 보기: https://github.com/a-mean-blogger/tour-of-heroes-api/tree/704b511112d351f88a9f34403b105b4499f23509
** 이 강의는 node js와 Express로 기초적인 사이트를 제작할 수 있는 분들을 대상으로 합니다. MEAN Stack, Node JS 첫걸음의 강의를 먼저 보신 후 진행하시기 바랍니다.
이전 강의에서는 REST API를 테스트하는 방법에 대해 알아보았습니다. 이번에는 Node JS를 사용해서 REST API를 직접 만들어 봅시다.
만들 API는 Angular의 공식 튜토리얼인 Tour of Heroes(https://angular.io/tutorial)에서 사용되는 API입니다. 튜토리얼에서는 진짜 REST API를 사용하지 않고 일종의 가상 API를 사용하는데요, 이 가상 API대신에 사용할 수 있는 API를 node.js와 express를 사용하여 만들어 봅시다.
Angular 튜토리얼에 사용되는 API를 만들지만 REST API이기 때문에 Angular뿐만 아니라 React, vue 등 무슨 프레임워크를 front-end로 쓰든지 상관없이 사용할 수 있습니다.
Tour of Heroes 사이트에는 영웅(hero)의 리스트를 보여주고(index), 하나의 hero의 정보를 보여주고(show), hero를 생성하고(create), hero의 정보를 수정하고(update), hero를 삭제(destroy)하는 5가지 API들을 사용합니다.
코딩을 시작하기 전에 API의 스펙(Specifications)을 먼저 살펴봅시다. 스펙에는 각각 API의 URL(endpoint라고도 부릅니다), API에 전달될 request의 구조, 해당 request에 대한 response 등의 정보가 담겨있습니다.
실제 개발현장에서는 API를 제작하기 전에 API 개발자, API 오너, API 이용자(API를 사용하는 다른 개발자들)이 함께 스펙을 작성하고, API 개발자가 스펙을 바탕으로 API를 제작하는 것이 이상적입니다. 이 강의를 위한 Tour of Heroes의 Hero APIs 스펙은 튜토리얼의 가상 API의 행동을 보고 제가 작성하였습니다.
{ "success": true, "data": [ { "_id": "5984f2a0d8afc1040c004ee9", "id": 1, "name": "My Hero 1", "__v": 0 }, { "_id": "5984f2c6d8afc1040c004eea", "id": 2, "name": "My Hero 2", "__v": 0 }, { "_id": "5984f2cdd8afc1040c004eec", "id": 3, "name": "Your Hero 1", "__v": 0 } ] }
{ "success": true, "data": [ { "_id": "5984f2a0d8afc1040c004ee9", "id": 1, "name": "My Hero 1", "__v": 0 }, { "_id": "5984f2c6d8afc1040c004eea", "id": 2, "name": "My Hero 2", "__v": 0 } ] }
{ "success": true, "data": { "_id": "5984f2c6d8afc1040c004eea", "id": 2, "name": "My Hero 2", "__v": 0 } }
{name:String}
을 받아 새로운 hero를 생성합니다. hero의 id는 현재 마지막 hero id의 다음 번호입니다. 새로생성된 hero를 리턴합니다.
{ "name": "Your Hero 2" }
{ "success": true, "data": { "__v": 0, "id": 4, "name": "Your Hero 2", "_id": "5984f935ed01e00d54553d36" } }
{id:Number,name:String}
를 받아 id가 {id}인 hero를 수정합니다.
{ "id": 4, "name": "My Hero 3" }
{ "success": true }
{ "success": true }
프로젝트 생성에 앞서, 해당 프로젝트에서 사용할 DB를 생성하고 환경변수에 등록해 줍시다.
프로젝트 폴더를 생성합니다. 해당 폴더에서 command line(cmd, git bash 등)에 npm init을 입력하여 node.js 프로젝트를 생성합니다.
$ npm init
프로젝트에 필요한 package들도 설치해줍니다.
$ npm install express mongoose body-parser --save
이번 강의에서 필요한 package는 express, mongoose, body-parser입니다. 각각의 package에 대해 잠깐 복습을 해보면,
프로젝트 생성과정과 위 각각의 package들이 이해가 잘 안되시는 분들은 반드시 MEAN Stack/개발 환경 구축부터 Node JS 첫걸음/주소록 만들기까지 다시 한번 읽어 주시기 바랍니다.
웹사이트를 만들 땐 routes 폴더가 있었는데 API는 api 폴더가 그 역할을 합니다. API와 일반사이트를 섞어 만들때 구별하기 쉽게 하기 위해서 이렇게 다른 폴더이름을 사용합니다. 웹사이트를 만들 때 사용했던 views나 public등의 폴더도 API 앱에는 없는 것을 알 수 있습니다.
// index.js var express = require('express'); var mongoose = require('mongoose'); var bodyParser = require('body-parser'); var app = express(); // DB setting mongoose.set('useNewUrlParser', true); mongoose.set('useFindAndModify', false); mongoose.set('useCreateIndex', true); mongoose.set('useUnifiedTopology', true); mongoose.connect(process.env.MONGO_DB); var db = mongoose.connection; db.once('open', function(){ console.log('DB connected'); }); db.on('error', function(err){ console.log('DB ERROR : ', err); }); // Other settings app.use(bodyParser.json()); app.use(bodyParser.urlencoded({extended: true})); app.use(function (req, res, next) { // 1 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')); // Port setting var port = 3000; app.listen(port, function(){ console.log('server on! http://localhost:'+port); });
1. 보안상의 이유로 서버는 기본적으로 같은 서버가 아닌 다른 곳에서 오는 요청(request)들을 차단합니다. 클라이언트의 브라우저에서 오는 요청도 다른 곳으로 간주합니다. 하지만 REST API를 브라우저에서 사용하려면 이 요청들을 허가해야 하는데, 이것을 HTTP 접근 제어 혹은 CORS(Cross-origin resource sharing, 출처가 다른 곳끼리 자원 공유)라고 합니다.
Node JS 첫걸음/주소록 만들기의 최종 코드와 비교해서 view에 관련된 부분이 모두 제거되고 CORS가 추가되었습니다.
// models/hero.js var mongoose = require('mongoose'); var heroSchema = mongoose.Schema({ id: { type:Number, required:true, }, name: { type:String, required:true, unique:true, }, }); var Hero = mongoose.model('hero', heroSchema); module.exports = Hero;
Tour of Heroes에서 사용되는 hero의 schema입니다. 숫자로 된 id와 문자열인 name을 가지고 있습니다.
// api/heroes.js var express = require('express'); var router = express.Router(); var Hero = require('../models/hero'); // Index router.get('/', function(req, res, next){ var query = {}; if(req.query.name) query.name = {$regex:req.query.name, $options:'i'}; // 1 Hero.find(query) .sort({id: 1}) .exec(function(err, heroes){ if(err) { res.status(500); res.json({success:false, message:err}); } else { res.json({success:true, data:heroes}); } }); } ); // Show router.get('/:id', function(req, res, next){ Hero.findOne({id:req.params.id}) .exec(function(err, hero){ if(err) { res.status(500); res.json({success:false, message:err}); } else if(!hero){ res.json({success:false, message:'hero not found'}); } else { res.json({success:true, data:hero}); } }); } ); // Create router.post('/', function(req, res, next){ Hero.findOne({}) .sort({id: -1}) .exec(function(err, hero){ if(err) { res.status(500); return res.json({success:false, message:err}); } else { res.locals.lastId = hero?hero.id:0; next(); } }); }, function(req, res, next){ var newHero = new Hero(req.body); newHero.id = res.locals.lastId + 1; newHero.save(function(err, hero){ if(err) { res.status(500); res.json({success:false, message:err}); } else { res.json({success:true, data:hero}); } }); } ); // Update router.put('/:id', function(req, res, next){ Hero.findOneAndUpdate({id:req.params.id}, req.body) .exec(function(err, hero){ if(err) { res.status(500); res.json({success:false, message:err}); } else if(!hero){ res.json({success:false, message:'hero not found'}); } else { res.json({success:true}); } }); } ); // Destroy router.delete('/:id', function(req, res, next){ Hero.findOneAndRemove({id:req.params.id}) .exec(function(err, hero){ if(err) { res.status(500); res.json({success:false, message:err}); } else if(!hero){ res.json({success:false, message:'hero not found'}); } else { res.json({success:true}); } }); } ); module.exports = router;
모든 route이 JSON을 return합니다! 사실 웹사이트와 API의 차이점은 html을 return하지 않고 데이터를 JSON형태로 바로 리턴한다는 것 뿐입니다. 데이터를 가공해서 사람이 보기 쉽게 하는 것이 웹사이트, DB의 데이터를 그대로 돌려주는 것이 REST API입니다.
1. DB상의 hero를 검색하기 위해 regex를 사용합니다. $options:i
는 대소문자 구별을 하지 않는 regex 옵션입니다. 이 부분을 제외하면 이전까지의 강의와 비교해서 특별한 것이 없는 코드입니다. 만약 이해가 안되시는 부분이 있다면 댓글 남겨주세요.
Node JS API/API 테스트 프로그램(Postman)설치및 간단 사용법에서 살펴봤던 포스트맨(Postman)을 사용해서 API를 테스트 해봅시다. 해당 강의에서는 POST용 API가 없어서 연습을 제대로 하지 못했는데, 이번에는 제대로 해봅시다.
nodemon을 사용해서 서버를 실행합시다
nodemon
Postman을 실행하고 hero를 만들어 봅시다. Method는 POST를 선택하고 url에 http://localhost:3000/api/heroes를 입력합니다. Body를 눌러 raw를 선택하고 우측 dropdown 메뉴에서 JSON을 선택합니다. 아래 텍스트 입력창에 다음과 같이 입력합니다.
{ "name": "My Hero 1" }
이후 send를 눌러서 "success": true
가 보인다면 성공입니다.
포스트맨상에서 보이는 모습입니다. My Hero 2까지 만들고 다음으로 Hero Index API를 테스트해봅시다.
새 탭을 열고 [GET] http://localhost:3000/api/heroes 에 request를 보내면, 아래와 같이 hero 목록을 볼 수 있습니다.
나머지 API들도 스펙에 맞게 잘 작동하는지 테스트해 봅시다.
강의 본문에서 언급하였지만 사실 웹사이트와 API의 개발에서의 차이는 data를 어떠한 형식으로 제공하는지의 차이밖에 없습니다. MEAN stack에서 실제 사이트 개발은 Angular로 하는데 이 블로그에서 Node JS/Express 강의들(주소록 만들기, 게시판 만들기)에 상당한 분량이 들어 간 것도 이 때문입니다.
이 API가 Angular에서 어떻게 활용되는지가 궁금하신 분들은 Angular 카테고리의 글들을 읽어보세요.
긴 글 읽으시느라 수고하셨고 질문있으면 댓글로 남겨주세요.
댓글
이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기