6 분 소요


REST API 라우팅하기

  • 지난 시간 코드
// @ts-check

/* eslint-disable no-console */

const express = require('express')
const fs = require('fs')

const app = express()

const PORT = 5000

app.use('/', async (req, res, next) => {
  console.log('Middleware 1')

  const fileContent = await fs.promises.readFile('.gitignore')
  const requestedAt = new Date()

  // @ts-ignore
  req.requestedAt = requestedAt
  // @ts-ignore
  req.fileContent = fileContent
  next()
})

app.use((req, res) => {
  console.log('Middleware 2')
  // @ts-ignore
  res.send(`Requested at ${req.requestedAt}, ${req.fileContent}`)
})

app.listen(PORT, () => {
  console.log(`The Express server is listening at port: ${PORT}`)
})


지난 시간에는 루트 경로로 들어오는 라우팅만 동작하도록 처리를 했었는데 이제 이것들이 어떤 주소로든 들어와도 처리되도록 할 수 있다.


라우팅(Routing)이란?

라우팅이란 네트워크에서 사용하는 용어로 어떤 네트워크가 있을 때 이 안에서 통신되는 데이터를 보낼 경로를 선택해내는 과정을 말한다. 이는 HTTP 메소드(GET, POST 등…)를 어떤 걸 사용하느냐에 따라 달라질 수 있는데 해당 기능을 하는 함수를 바로 사용할 수도 있다.

app.get('')
app.post('')


간단한 예로 get과 post 메소드를 사용해 보자.

...

app.get('/', (req, res) => {
  res.send('Root - GET')
})

app.post('/', (req, res) => {
  res.send('Root - POST')
})

app.listen(PORT, () => {
  console.log(`The Express server is listening at port: ${PORT}`)
})

일단 루트 경로에 대해 처리를 하도록 했고 각각 HTTP 메소드가 GET일 때와 POST일 때 response로 string을 반환해 보았다.

httpie나 postman을 통해 확인해 보면 각각의 메소드로 요청을 보냈을 때 알맞은 문구가 뜨는 것을 확인할 수 있을 것이다.


path

path 즉, 경로에는 여러가지 방식으로 작성할 수가 있는데 그 종류에 대해서 한 번 알아보자.


  • 문자열 패턴

    • app.get('/ab?cd', function(req, res) {
        res.send('ab?cd');
      });
      
      • 문자열 패턴을 기반으로 하는 라우트 경로의 모습이며 위의 라우트 경로는 acd 및 abcd와 일치한다.
      • 물음표 전에 온 문자에 대해서는 있을 수도 있고 없을 수도 있다는 의미.

```js
app.get('/ab+cd', function(req, res) {
  res.send('ab+cd');
});
```

- 위의 라우트 경로는 abcd, abbcd 및 abbbcd 등과 일치한다.
- \+ 전에 온 문자에 대해서는 해당 문자가 여러번(**한 번 이상**) 올 수 있다는 의미.
  • app.get('/ab*cd', function(req, res) {
      res.send('ab*cd');
    });
    
    • 위의 라우트 경로는 abcd, abxcd, abRABDOMcd 및 ab123cd 등과 일치한다.
    • * 의 의미는 해당 부분에는 종류와 길이에 상관없이 어떤 문자열이든 올 수 있다는 의미.
  • app.get('/ab(cd)?e', function(req, res) {
     res.send('ab(cd)?e');
    });
    
    • 위의 라우트 경로는 /abe 및 /abcde 와 일치한다.
    • 괄호를 묶으면 그룹으로 생각하면 된다.(+, ? 와 같은 기호가 한 번에 적용됨)
  • 정규식 기반(/something/) -> /로 감싼 형태

    • 정규식으로 표현할 때는 따옴표를 넣지 않는다!

    • app.get(/a/, function(req, res) {
        res.send('/a/');
      });
      
      • 위의 라우트 경로는 라우트 이름에 “a”가 포함된 모든 항목과 일치한다.
      • 반드시 a는 포함해야 한다는 의미.
    • app.get(/.*fly$/, function(req, res) {
        res.send('/.*fly$/');
      });
      
      • 위의 라우트 경로는 butterfly 및 dragonfly와 일치하지만 butterflyman 및 dragonfly man 등과는 일치하지 않는다.
      • ~~로 끝나야 한다는 $를 사용하여 사용할 수 있음
    • app.get(/^\/abcd$/, (req, res) => {
        res.send('/^\/abcd$/')
      })
      
      • 위의 정규표현식대로 라우팅 경로를 지정하면 딱 저 문자열(/abcd)과 정확히 일치하는 uri만을 받도록 할 수 있다.
  • 문자열 - 여러 uri 지정

    • app.get(['/abc', '/xyz'], (req, res) => {
        res.send('/^\/abcd$/')
      })
      
      • 위처럼 여러 uri를 받고 싶을 때는 배열에 여러 uri를 넣어 표현할 수도 있다.
      • 물론 배열의 원소로 정규표현식을 사용하는 것도 가능하다.


prefix

app.get('/users', (req, res) => {
  res.send('User list')
})

app.get('/users:id', (req, res) => {
  res.send('User info with ID')
})

app.post('/users', (req, res) => {
  // Register user
})

위 코드는 총 세 개의 API를 구현해 본 것으로 위에서부터 각각 User list를 GET하는 API, 특정 id를 가진 user의 정보를 GET 하는 API, 새로운 user를 등록(POST)하는 API이다.

이 때는 모두 /users 라는 경로로 들어가야 하기 때문에 prefix가 동일하다고 볼 수 있고 이처럼 prefix를 공유하는 경우에 Router 기능을 사용할 수 있다.

router 역시 일종의 미들웨어이다.


위 코드를 바꾼 코드는 다음과 같이 될 것이다. (Router를 적용한 코드)

const userRouter = express.Router()

userRouter.get('/', (req, res) => {
  res.send('User list')
})

userRouter.get('/:id', (req, res) => {
  res.send('User info with ID')
})

userRouter.post('/', (req, res) => {
  // Register user logic
  res.send('User registered')
})

그러면 이 userRouter라는 라우터는 /users 라는 prefix에만 반응을 하도록 해 주어야 하기 때문에 다음과 같은 구문이 추가 되어야 한다.

app.use('/users', userRouter)


path

  • path variable: :
    • 지정한다.
    • 특정한 값을 지목해서 처리하는 방식
    • ex) userId = 1, videoId = 123 과 같은 데이터를 원할 때
    • 경로에 존재하는 내용이 없을 시
    • 404 Error 발생
      • resource를 식별해야 하는 경우에 적합
  • query-string: ?
    • 일종의 필터링
    • 필터링을 활용한 처리
      • ex) 활성화 상태인 동영상을 원할 때
    • 데이터가 없는 경우
    • 빈 리스트가 나옴. => 추가적인 예외 처리 필요
      • 정렬, 필터링을 해야 하는 경우에 적합


path variable을 통해 :id 를 path uri에 지정하지 않아도 123과 같은 숫자만 적어도 처리가 될 수 있다.

무슨 말이냐 하면 다음과 같은 코드를 한 번 생각해 보자.

const USERS = {
  15: {
    nickname: 'foo',
  },
}

userRouter.param('id', (req, res, next, value) => {
  console.log('id parameter', value)
  // @ts-ignore
  req.user = USERS[value]
  next()
})

userRouter.get('/:id', (req, res) => {
  console.log('userRouter get ID')
  res.send(req.user)
})

userRouter.post('/', (req, res) => {
  // Register user
})

app.use('/users', userRouter)

USERS라는 유저 정보가 저장되는 객체가 있다고 생각하고 request uri에 :id 에 해당하는 것이 있다고 판단이 되면 userRouter.param에는 ‘id’라는 인자를 받는 것을 대기하고 있으므로 해당 구문에 걸리게 되고,

이 때 걸린 id라는 값은 value라는 parameter로 받아서 제대로 받아왔는지 server console에 이 값을 찍어주고 해당 id에 해당하는 정보가 USERS객체에 존재하면 이 값을 req.user로 넘겨주어 다음 미들웨어에서 서버 콘솔에 ID를 받았다는 문구를 출력하고 client 측으로 req.user 값을 send한다.

이 때 param 메소드에서 next()를 사용한 이유는 res.send와 같이 response를 해 주어 다음 미들웨어로 넘겨주는 일련의 과정이 생략되고 단지 req.user에 값만 넘겨주었기 때문에 따로 next()를 넣어주어 정삭적으로 다음 미들웨어로 넘어갈 수 있도록 한 것이다.


// server
id parameter
userRouter get ID


// client
{
    "nickname": "foo"
}


재밌는 사실은 client가 send를 통해 받은 응답의 타입이 원래는 string 객체로 넘겨주었는데 req.user를 넘겨주니까 content-type을 보면 application/json으로 되어있는 것을 확인할 수 있고 이는 express가 자동으로 json 형식으로 바꾸어 준 것으로 보아 express의 기능이 좋다는 것을 다시 한 번 느낄 수 있을 것이다.


그렇다면 이제 id와 nickname을 클라이언트로부터 받아서 server측에 추가(POST) 해 주는 기능을 추가해 보도록 하겠다.


그렇다면 우리가 가장 먼저 기대할 수 있는 사실은 req.body의 형식이 다음과 같다는 것을 알 수 있다.

req.body = {"nickname": "bar"}


다음과 같이 코드를 먼저 구현해 보고 이를 토대로 user를 추가해 보도록 하겠다.

userRouter.post('/:id/nickname', (req, res) => {
  // req.body = {"nickname": "bar"}
  const { user } = req
  const { nickname } = req.body
  // @ts-ignore
  user.nickname = nickname

  res.send(`User nickname updated: ${nickname}`)
})
http POST localhost:5000/users/15/nickname nickname=bar

위와 같이 접속을 하여 추가를 하려고 하면 오류가 나는 것을 볼 수 있는데


Cannot destructure property 'nickname' of 'req.body' as it is undefined.

req.body가 undefined인데 거기서 nickname이라는 property를 가져오려고 해서 오류가 난 것이다.

이를 해결하기 위해서는 body parser라는 모듈을 통해 해결할 수 있다.


Why ‘body parser?’

지금 express 자체에는 어떤 요청이 오던지 간에 body를 parsing하고 있지는 않다.

지금 상황은 application/json 형태로 POST 요청이 온 것인데 이것에 대해서 어떠한 일관적 반응을 하도록 되어 있지는 않는다는 뜻이다. 그런데 body parser를 사용하여 이 문제를 해결할 수 있기에 해당 모듈을 사용하는 것이다.


body parser 모듈을 다운받기 위해서는 다음과 같은 명령어를 입력하면 된다.

  • 현재 실행 중인 서버는 Ctrl-C를 눌러서 잠시 종료 시킨 뒤에 설치하도록 한다.
npm install body-parser


설치가 완료되었으면 const bodyParser = require('body-parser')를 통해 body parser를 가져오는 부분을 추가해 주도록 한다.


그리고 이 bodyParser를 적용하기 위해서는 미들웨어를 또 끼우면 된다. 즉, app.use(bodyParser.json()) 구문을 추가하면 된다.


그래서 이번에 다음과 같이 POST 요청을 하게 되면 올바르게 수정사항이 변경되도록 할 수 있다.

http POST localhost:5000/users/15/nickname nickname=bar

이를 확인해 보기 위해선 다음과 같이 입력한다.

http localhost:5000/users/15

그러면 15번에 해당하는 유저의 정보가 json 형태로 응답을 받게 되고 해당 객체 안의 nickname 부분이 ‘foo’ 에서 ‘bar’로 바뀐 것을 볼 수 있다.


+ 그러나 최근 express에서 body parser를 제공하여 body parser 모듈을 사용하지 않고 다음과 같이 하면 body가 parsing되어 코드가 정상적으로 실행되도록 기능이 추가되었다.

app.use(express.json())

댓글남기기