[Node.js코드랩] 14. 요청 객체

🌳목표

Response 모듈을 만든 것처럼 익스프레스와 유사한 요청 객체인 Request 모듈을 만듭니다.

쿼리스트링 요청

먼저 브랜치를 이동해 볼까요?

$ git checkout -f request/spec

서버를 재 구동하고 브라우져로 접속해 보세요.

크롬 개발자 도구로 보니 에러가 나오는데요. 이전과 다르게 /api/posts 요청에 쿼리문자열이 추가 되었습니다.

?limit=2&page=1

그런데 이게 404로 응답되는군요. 분명히 /api/posts 라우트를 등록했는데 말이죠.

아마도 서버에서 미들웨어 등록시 사용한 URL과 요청한 URL이 '정확히' 일치하지 않는것 같습니다. src/Middleware.js에 있는 코드를 확인해 볼까요?

if (nextMw._path) {
  const pathMatched = _req.url === nextMw._path // 바로 이 부분 !!!
  return pathMatched ? nextMw(_req, _res, next) : _run(i + 1)
}

등록한 경로(nextMw._path)와 요청한 전체 주소(_req.url)가 완벽히 일치하지 않군요. 그래서 등록한 미들웨어도 실행되지 않는 것이고요. 결국에 404를 응답하는 것입니다.

이 문제를 해결하려면 쿼리스트링을 제외한 정확한 경로을 뽑아서 비교하는 기능이 필요합니다.

Request 모듈

HTTP 에서 요청 URL은 다음 형식을 갖습니다.

Protocol + Domain + Path + QueryString + Body

이 중 우리가 사용할 것은 path, querystrig, body 부분입니다.

지금까지 경로 매칭에 사용한 req.url은 path와 querystring이 포함되어 있습니다. 이것이 문제의 원인이기도 하구요.

req가 아래와 같은 형식의 정보를 제공한다면 어떨까요?

req.path = "/api/posts"
req.query = {
  limit: "2",
  page: "1",
}

경로를 비교할때 req.url 대신에 req.path를 사용하면 정확한 비교가 되겠죠? 게다가 쿼리스트링에 접근할 때는 미리 파싱된 req.query 객체를 통해서 limit과 page 값에 접근할 수도 있을 것입니다.

이러한 req 객체를 확장하는 Request 모듈을 만들어 보겠습니다.

요구사항이 있는 테스트 코드를 살펴 볼까요? 네 부분으로 나눠 설명하겠습니다.

require('should');
const Request = require('./Request');

describe('Request', () => {
  it('생성 인자가 없으면 에러를 던진다', () => {
    should(() => Request()).throw()
  })

테스트 모듈을 임포트하였습니다. 첫 번째 테스트 케이스는 모듈 생성시 인자가 없으면 에러를 던지는 코드입니다. 확장할 req 인자가 반드시 필요하니까요.

  describe('반환 객체', () => {
    let req, path, qs

    beforeEach(() => {
      path = '/api/posts'
      qs = {
        limit: '2',
        page: '1'
      }
      const encodedQs = `limit=${qs.limit}&page=${qs.page}`
      req = Request({url: `${path}?${encodedQs}`})
    })

두 번째 테스트 케이스부터는 생성 인자가 올바로 들어간 이후의 테스트 입니다. 모듈 생성 코드는 각 테스트 케이스에서 중복으로 사용될 것이므로 beforeEach()에서 기술하였습니다.

기존 req 객체의 url 문자열을 기반으로 path와 querystring을 추출할 것이므로 url 문자열을 키로 가지고 있는 객체를 전달하였습니다.

it("path 속성을 노출한다", () => {
  req.should.have.property("path", path)
})

생성된 객체가 path 속성이 있는지 확인하는 코드입니다. path에는 querystring이 제거된 순수한 경로만 있어야 합니다.

    it('query 속성을 노출한다', () => {
      req.should.have.property('query')
      req.query.limit.should.be.equal(qs.limit)
      req.query.page.should.be.equal(qs.page)
    })
  })
})

query 객체가 있는지 확인합니다. 이 객체에 limit과 page 값이 올바로 할당되어 있는지도 체크하는 코드입니다.

🐤실습 - Request 모듈을 구현해 보세요

테스트 코드에서 요구하는 것처럼 기존 req객체에 path와 query 속성을 추가하는 Request 모듈을 만들어 보세요.

힌트: 문자열을 나눌 때는 String.prorotype.split()

🐤풀이

이 실습에서는 split() 스트링 메소드만 잘 사용해도 쉽게 해결할 수 있습니다. 세 부분으로 나누어 풀어볼게요.

const Request = req => {
  if (!req) throw Error("req is required")

  // req 확장 코드

  return req
}

module.exports = Request

생성인자 req가 없으면 에러를 던져 버립니다. 그리고 확장된 req 객체를 다시 반환합니다.

const partials = req.url.split("?")

const path = partials[0] || "/"
req.path = req.path || path

if (!partials[1] || !partials[1].trim()) return req

split() 메소드는 특정 캐릭터 기준으로 문자를 쪼겐 뒤 배열에 담아 반환합니다. req.url을 "?"로 분리한 뒤 partials 배열로 담습니다.

이렇게 분리된 partials의 첫 번째 요소에는 경로 정보가 들어가게 될 것입니다. 그렇지 않을 경우엔 기본 경로 "/"를 할당하구요. 이 path을 req.path에 저장해서 req 객체를 확장합니다.

분리된 문자열의 나머지 부분이 없을 경우엔 바로 req 객체를 리턴합니다.

const qs = partials[1].split("&").reduce((obj, p) => {
  const frag = p.split("=")
  obj[frag[0]] = frag[1]
  return obj
}, {})

req.query = qs

쿼리스트링은 "&" 문자를 기준으로 "key=value" 형식으로 구성되어 있습니다. 그래서 먼저 "&"로 문자를 분리한 것이죠.

이렇게 반환된 배열을 reduce 함수로 돌리면서 obj에 결과를 담습니다. 배열의 요소(p)는 "key=value" 형태의 문자열일 테니깐 "=" 문자 기준으로 한 번 더 분리합니다. 그리고 나면 frag[0]에는 키가 frag[1]에는 밸류가 들어가게 되겠지요.

이렇게 해서 완성된 qs 객체를 req.query로 할당하여 req 객체를 확장하였습니다.

Request 모듈을 Application에서 사용

Reponse를 Application에서 사용한 것처럼 Request도 그렇게 해야합니다. src/Application.js 코드중 req 객체를 사용하는 부분을 볼까요?

// ...
const Request = require('./Request');

const Application = () => {
  const _server = http.createServer((req, res) => {
    _middleware.run(Request(req), Response(res)) // Request 객체로 교체
  });

기존의 req 객체를 Request 객체로 확장하였습니다.

Request 모듈을 Middleware에서 사용

초반에 언급한 문제의 원인을 다시 떠올려 볼까요? 등록했던 API인데 404응답을 받은 문제였지요? 그것은 바로 등록한 경로와 요청 주소가 정확히 일치하지 않기 때문이었습니다.

이 부분의 코드를 담고 있는 src/Middleware.js 코드를 개선해 보지요.

if (nextMw._path) {
  const pathMatched = _req.path === nextMw._path // 경로만 체크
  return pathMatched ? nextMw(_req, _res, next) : _run(i + 1)
}

이전에는 쿼리문자열이 포함된 _req.url을 비교하는 코드였습니다. 이제는 경로만 저장된 _req.path를 비교해서 요청정보와 정확한 비교를 할 수 있게 되었습니다.

서버를 다시 실행하여 결과를 확인해 볼까요?

포스트 목록이 나오는 것을 보니 API가 잘 응답되는 것 같네요.

크롬 개발자 도구를 보면 성공을 의미하는 200 상태코드가 응답되었습니다.

하지만 요청 주소의 쿼리문자열을 유심히 살펴 보세요.

?limit=2&page=1

포스트 갯수를 최대 2개로 한정하고 1페이지를 요청했으니깐 처음 2개 포스트만 나오는게 맞을 겁니다. 그런데 지금은 포스트 3개가 출력되었죠.

🐤실습 - Request 모듈을 이용해 이 문제를 해결해 보세요

/api/posts API에서 페이지네이션 로직을 추가로 구현하세요. 코드를 작성하지 않았다면 아래 브랜치로 이동하시구요.

$ git checkout -f request/props

🐤풀이

간단히 풀어 보죠.

포스트 조회 API가 있는 routes/api/posts.js 파일을 수정합니다.

const index = () => (req, res, next) => {
  const limit = req.query.limit * 1 || 2
  const page = req.query.page * 1 || 1

  const begin = (page - 1) * limit
  const end = begin + limit

  res.json(posts.slice(begin, end))
}

req.query에 담겨있는 쿼리문자열 정보는 모두 문자열로 저장되어 있습니다. 이걸 모두 숫자형으로 변환(*1) 했습니다. 만약 없을 경우에는 limit은 2, page는 1로 각 각 기본값을 할당했구요

그리고나서 posts 배열에서 요청한 데이터 범위를 찾기 위해 begin과 end를 계산합니다.

마지막으로 구간의 정보를 추출한 배열을 제이슨으로 응답합니다.

서버를 재실행하고 확인해 볼까요?

3개 포스트를 출력한 이전과 달리 요청한 2개 포스트만 출력되는 것을 확인했습니다.

Request 모듈까지 해서 익스프레스JS 섹션을 모두 채웠습니다.

정리

  • 요청 정보에 쉽게 접근하게 위한 Request 모듈을 만들었습니다.
  • req.path, req.query

목차 바로가기