[Node.js, Javascript] Todo App 개발 – (2) REST API

restful api

이번에는 express.js를 이용하여 REST API를 구성해보도록 하겠습니다. front-end 강의에서 node.js, express.js, REST API 등의 back-end 기술을 다루는 이유는, front-end 개발자를 하더라도 서버에 대한 최소한의 이해가 필요하기 때문입니다.

front-end 개발자는 react.js, vue.js, angular.js 등을 이용하여 SPA(Single Page Application)를 만듭니다. SPA는 RESTful API와 연동되어 사용됩니다. REST API는 일종의 Databus라고 생각하면 쉽습니다.

내용 구성은 다음과 같습니다.

  1. REST API
  2. 프로젝트 구성
  3. FileSytem을 이용한 DB 설계
  4. REST API 제작

굉장히 다룰 내용이 많지만, 필요한 것들만 짚고 넘어가도록 하겠습니다. RESTful API 제작은, 다음에 언급할 Front-end Framework를 사용하기 위함입니다.


1. REST API

REST(Representational State Transfer)는 자원을 명시하여 주고받는 행위를 말한다.

REST에는 get, post, put, delete 등의 행위가 있습니다.

  1. get : 데이터를 가져온다. (read)
  2. post : 데이터를 등록한다. (write)
  3. put : 데이터를 수정한다. (update)
  4. delete : 데이터를 삭제한다. (delete)

즉, CRUD(Create, Read, Update, Delete) 를, get, post, put, delete 라는 키워드를 사용하여 자원을 관리한다는게 REST의 개념입니다.

그리고 REST API란, 이러한 REST와 관련된 행위를 API로 구현한 것입니다.

REST를 사용하는 목적은 3가지 입니다.

  1. 애플리케이션 분리 및 통합
  2. 다양한 플랫폼의 등장에 따른 지원
  3. 서비스 자원에 대한 아키텍쳐가 깔끔하고 쉬움

예를 들어, 네이티브 앱(안드로이드, IOS)에서는 데이터베이스 사용이 불가능합니다. 그래서 웹으로 REST API를 구성하고, 네이티브 앱에서 REST API와 통신하여 자원을 관리합니다.

또한 client-side에서는 완벽하게 UI만 담당하고(SPA), server-side에서는 완벽하게 UI에 지원하는 자원만 관리하도록 할 때 REST API를 이용합니다.

REST의 구성 요소에는 자원(URI), 행위(Method), 표현(JSON) 등이 있습니다.

이 외에 자세한 설명은 다음 링크를 참고해주세요
https://gmlwjd9405.github.io/2018/09/21/rest-and-restful.html


2. 프로젝트 구성

먼저 프로젝트 구성을 해보도록 하겠습니다.
프로젝트 구성 이전에, node와 express에 대한 이해가 필요합니다.
node.js란? / node.js와 express.js

> mkdir todoapp
> cd todoapp
> npm init
todoapp

추가로, express와 nodemon을 설치합니다.

> npm install express
> npm install -g nodemon

nodemon으로 project혹은 js파일을 실행시킬 때, 변화를 자동으로 감지하여 수정이 있으면 다시 재실행 해주는 패키지입니다. 매우 유용하니 꼭 설치합시다.

이렇게 npm으로 project를 구성하게 되면 저절로 node_modules과 package.json이 생성됩니다. 추가로 프로젝트 진행을 위해 다음과 같이 파일과 폴더를 구성하여 봅시다.

  • node_modules(자동 생성)
  • data
    • data.json
    • db.js
  • public
    • js
      • app.js
    • index.html
  • index.js
  • package.json (자동 생성)


3. File System을 이용한 DB 설계

Database를 이용하여 설계하면 좋지만, DB를 따로 다루기에 지금 당장은 시간이 부족해서 간단히 File System을 통하여 자원을 관리하도록 해보겠습니다. 코드는 굉장히 간단합니다. 하지만.. 간단하기에 관리가 어렵습니다. 어쨋든 코드를 살펴봅시다.

/data/db.js

const fs = require('fs')

// data.json만 사용할 것입니다.
const path = __dirname + '/data.json'
const getData = _ => new Promise((resolve, reject) => {
  fs.readFile(path, 'utf-8', (err, data) => {
    err ? reject(err) : resolve(JSON.parse(data || null))
  })
})
const setData = data => new Promise((resolve, reject) => {
  fs.writeFile(path, JSON.stringify(data), 'utf-8', err => {
    err ? reject(err) : resolve()
  })
})

module.exports = {getData, setData}

Promise를 사용하였습니다. Promise에 기억나지 않는다면 다음 링크의 내용을 보고 이해하고 오도록 합시다. 참고로 이번 프로젝트 코드에 굉장히 빈번하게 등장하기 때문에 꼭 이해하고 넘어가야 합니다.
[javascript] Promise, async, await


/data/data.json

{
    "folder": [],
    "task": []
}

folder와 task라는 자원을 사용할 것입니다. 간단하게 배열로 구성합시다.


4. REST API 제작

드디어! express.js로 REST API를 만들어봅시다.

/index.js

// 외부 모듈 import
const express = require('express')
const db = require('./data/db.js')
const app = express()

// middle ware 등록
app.use(express.json())  // request body를 사용하기 위함
app.use(express.static('public')) // static file을 사용하기 위함
app.listen(3000)

아직 Routing은 하지 않았습니다. 방금 제작한 /data/db.js를 불러와서 사용할 것이며, 추후에 실제로 html과 연동을 하기 위해서 static 폴더를 등록합니다.

express.json()을 등록하면 req.body에 있는 내용을 사용할 수 있게 됩니다.


GET : /api/folder

// 외부 모듈 import
// ...내용생략..

// routing
app.route('/api/folder')
  .get(async (req, res) => {
    const result = {success: true}
    try {
      const json = await db.getData()
      result.data = json.folder
    } catch (err) {
      result.success = false
      result.err = err
    }
    res.json(result)
  })

// middle ware 등록
// ...내용생량...

RESTAPI는 성공을 하든, 실패를 하든 JSON 형태로 결과를 반환해야 합니다. 성공 여부는 success를 통하여 전달하고, success가 true면 data를 가져다 사용할 수 있게 하며, success가 false면 err를 확인할 수 있도록 데이터를 전달합니다.

이렇게 코드를 작성한 후에 커맨드 라인에서 다음과 같이 서버를 실행해봅시다.

> nodemon index.js
nodemon

위에 언급했듯이, nodemon을 사용할 경우 index.js의 변화를 감지하여 수정 시 자동으로 재실행 해줍니다. 서버가 정상적으로 실행 되었다면,
http://127.0.0.1:3000/api/folder
로 접근하여 결과를 확인해봅시다.

/api/folder result

success: true => 코드 진행 상의 오류는 없다는 뜻입니다. data: [] 로 나온 이유는 현재 data.json의 내용이 비어있기 때문입니다. 그럼 data.json을 수정 후에 다시 결과를 확인해볼까요?

{
    "folder": [
      {"name": "첫번째 폴더입니다."}
    ],
    "task": []
}
 api folder result 2

네, 이렇게 웹 페이지상에 data.json의 내용을 읽어와 뿌려주는 것을 확인할 수 있습니다. 이번에는 post method에 대한 코드를 작성해봅시다.


post : /api/folder

// 외부 모듈 import
// ...내용생략..

// routing
app.route('/api/folder')
  .get(/*생략*/)
  .post(async (req, res) => {
    const result = {success: true}
    const folder = req.body.folder // 입력 받은 폴더 정보
    try {
      const json = await db.getData() // 데이터 읽어오기
      json.folder = folder  // 데이터 수정
      await db.setData(json) // 데이터 반영
    } catch (err) {
      result.success = false
      result.err = err
    }
    res.json(result) // 결과 출력
  })

// middle ware 등록
// ...내용생략...

test를 위해 postman이라는 프로그램을 설치해주세요. postman 설치
postman은 rest api를 테스트하기 위한 도구입니다.

postman-test1
  1. 먼저 method를 post로 지정
  2. 주소는 http://127.0.0.1:3000/api/folder
  3. body tab 선택
  4. raw 선택
  5. json 입력
  6. send 버튼 클릭

입력할 json은 이렇게 입력해주세요.

{
  "folder": [
      {"name": "첫번째 folder"},
      {"name": "두번째 folder"},
      {"name": "세번째 folder"}
  ]
}

순으로 진행하면 됩니다. json형태의 data를 넘기기 위해선 raw형태의 json text 전송해야 합니다. 결과를 확인해볼까요?

success: true로 결과가 출력된다면 정상입니다. 그럼 다시 정상적으로 데이터를 불러오는지 확인해봅시다.

http://127.0.0.1:3000/api/folder에서 확인하거나, postman에서 method를 get로 바꿔준 후 그대로 send하여봅시다.

post man test 2

네, 이렇게 success: true와 함께 변경된 data를 확인할 수 있습니다. 나머지도 입력해볼까요?


GET, POST, PUT : /api/task/:parent

// 외부 모듈 import
// ...내용생략..

// routing
app.route('/api/folder')
  .get(/* 생략 */)
  .post(/* 생략 */)

// task
app.route('/api/task/:parent')
  .get(async (req, res) => {
    const result = {success: true}
    const parent = req.params.parent
    try {
      const json = await db.getData()
      list = []
      json.task.forEach((v, idx) => { // 모든 task data를 가져옵니다.
        if (v.parent === parent) { // 주소의 parent와 일치하면
          v.idx = idx // idx도 같이 지정해주고
          list.push(v) // list에 push해줍니다.
        }
      })
      result.data = list // 검열된 data를 결과로 반환합니다.
    } catch (err) {
      result.success = false
      result.err = err
    }
    res.json(result)
  })
  .post(async (req, res) => {
    const result = {success: true}
    const task = req.body.task
    const parent = req.params.parent
    try {
      const json = await db.getData() // 데이터를 가져온 후
      task.parent = parent // 부모값을 지정하여 추가한다.
      json.task.push(task) // task를 새로 추가하고
      await db.setData(json) // 업데이트 합니다.
    } catch (err) {
      result.success = false
      result.err = err
    }
    res.json(result)
  })
  .put(async (req, res) => {
    const result = {success: true}
    const task = req.body.task
    const idx = req.params.parent
    try {
      const json = await db.getData()
      json.task[idx] = task // 지정한 task를 수정 후
      await db.setData(json) // 업데이트 합니다.
    } catch (err) {
      result.success = false
      result.err = err
    }
    res.json(result)
  })

// middle ware 등록
// ...내용생략...

예외처리 코드 때문에 양은 많아보이지만, 실제로는 별거 없습니다. 실제로 sql을 사용하게 되면 더욱 깔끔한 코드가 됩니다만.. 단순히 JSON을 이용한 filesystem에서 원하는 data를 정확히 가져오기란 쉽지 않습니다. 반복문을 돌려서 검열을 한 후에 가져와야 하기 때문입니다.

Postman을 통해 다음과 같이 테스트해봅시다.

POST http://127.0.0.1:3000/api/task/0

body 입력

{
  "task": {
    "name": "첫번째 task",
    "state": false
  }
}

result

{"success": true}


GET http://127.0.0.1:3000/api/task/0 (GET은 그냥 링크로 확인해도 무방합니다)
결과는 다음과 같이 출력될 것입니다.

{
    "success": true,
    "data": [
        {
            "name": "첫번째 task",
            "state": false,
            "parent": "0",
            "idx": 0
        }
    ]
}


PUT http://127.0.0.1:3000/api/task/0

body 입력

{
  "task": {
    "name": "첫번째 task 수정",
    "state": true,
    "parent": "0",
    "idx": 0
  }
}

result

{"success": true}


GET http://127.0.0.1:3000/api/task/0 (GET은 그냥 링크로 확인해도 무방합니다)
결과는 다음과 같이 출력될 것입니다.

{
    "success": true,
    "data": [
        {
            "name": "첫번째 task 수정",
            "state": true,
            "parent": "0",
            "idx": 0
        }
    ]
}


최종 결과물 코드

// 외부 모듈 import
const express = require('express')
const db = require('./data/db.js')
const app = express()

// middle ware 등록
app.use(express.json())  // request body를 사용하기 위함
app.use(express.static('public')) // static file을 사용하기 위함


// index page
app.get('/', (req, res) => {
  res.render('index') // index.html render
})

// folder
app.route('/api/folder')
  .get(async (req, res) => {
    const result = {success: true}
    try {
      const json = await db.getData()
      result.data = json.folder
    } catch (err) {
      result.success = false
      result.err = err
    }
    res.json(result)
  })
  .post(async (req, res) => {
    const result = {success: true}
    const folder = req.body.folder
    try {
      const json = await db.getData()
      json.folder = folder
      await db.setData(json)
    } catch (err) {
      result.success = false
      result.err = err
    }
    res.json(result)
  })

// task
app.route('/api/task/:parent')
  .get(async (req, res) => {
    const result = {success: true}
    const parent = req.params.parent
    try {
      const json = await db.getData()
      list = []
      json.task.forEach((v, idx) => {
        if (v.parent === parent) {
          v.idx = idx
          list.push(v)
        }
      })
      result.data = list
    } catch (err) {
      result.success = false
      result.err = err
    }
    res.json(result)
  })
  .post(async (req, res) => {
    const result = {success: true}
    const task = req.body.task
    const parent = req.params.parent
    try {
      const json = await db.getData()
      task.parent = parent
      json.task.push(task)
      await db.setData(json)
    } catch (err) {
      result.success = false
      result.err = err
    }
    res.json(result)
  })
  .put(async (req, res) => {
    const result = {success: true}
    const task = req.body.task
    const idx = req.params.parent
    try {
      const json = await db.getData()
      json.task[idx] = task
      await db.setData(json)
    } catch (err) {
      result.success = false
      result.err = err
    }
    res.json(result)
  })


app.listen(3000)

이렇게 간단하게 rest api를 제작해봤습니다. 다음 포스트는 rest api와 spa를 연동하는 방법에 대해 알아보도록 하겠습니다.


이어지는 포스트