🌳목표
익스프레스와 유사한 응답 객체인 Response 모듈을 만듭니다.
Response 모듈
앞으로 웹 개발을 할 때 API는 자주 사용 될 것입니다. 단일 페이지 어플리케이션에서는 거의 모든 데이터를 API 형태로 다루기 때문이죠.
이 때 서버는 JSON 형식으로 데이터를 응답해야 하는데 꽤 자주 사용되는 코드가 될 듯 싶습니다.
뿐만아니라 상태 코드를 포함한 헤더 설정도 모든 엔드 포인트마다 사용될 것 같구요.
이러한 응답 처리를 위한 "Response" 라는 모듈을 만들어 볼 거에요.
시리즈 초반에 소개했던 http.createServer() 메소드를 기억하나요? 이것의 콜백함수가 인자로 받는 응답 객체 res를 확장해서 만들어 보겠습니다.
요구사항 확인
테스트 코드가 있는 브랜치로 체크아웃 합니다.
$ git checkout -f response/spec
src/Response.spec.js 코드에 요구사항을 기록한 테스트 코드가 있습니다.
it("status 메소드를 노출한다", () => {
res.should.have.property("status")
should(typeof res.status).be.equal("function")
})
it("set 메소드를 노출한다", () => {
res.should.have.property("set")
should(typeof res.set).be.equal("function")
should(res.set.length).be.equal(2)
})
it("send 메소드를 노출한다", () => {
res.should.have.property("send")
should(typeof res.send).be.equal("function")
})
it("json 메소드를 노출한다", () => {
res.should.have.property("json")
should(typeof res.json).be.equal("function")
})
함수 유무만 판단하는 간단한 테스트 케이스인데요, 각 함수의 역할은 다음과 같아요.
status(code)
: 상태 코드를 설정set(key, value)
: 헤더 값을 key/value로 설정send(text)
: 문자 응답json(object)
: 제이슨 응답
🐤실습 - Response 모듈을 만들어 보세요
res 객체를 이용해 Response 모듈을 구현해 보세요. 위에서 설명한 4개 메소드를 모두 구현해야 합니다.
힌트: res 객체를 확장해서 만들기
🐤풀이
다 만들수 있었나요? 그럼 같이 풀어 보겠습니다. 👨🏻🏫
먼저 src 폴더에 Response.js 파일을 만듭니다. 다섯 부분으로 나눠 코드를 입력할게요
const Response = res => {
if (!res) throw Error("res is required")
return res
}
module.exports = Response
Response 모듈은 기존의 res 객체를 인자로 받습니다. 없을 경우 즉시 에러를 던져 프로그램을 종료하구요.
res를 확장한 뒤 마지막에 반환해 줍니다. (아직 확장 코드는 없습니다)
그럼 메소드를 하나씩 추가해 보죠.
res.status =
res.status ||
(code => {
// 기존 객체에 안전하게 추가
res.statusCode = code
return res // 함수 체이닝을 위해
})
상태값을 설정하는 status() 메소드를 구현합니다.
res는 이미 사용하고 있는 객체이기 때문에 기존 키를 덮어 쓰지 않기 위해 ||
연산을 사용했습니다.
statusCode를 설정한 후 res를 그대로 반환했습니다. 이것은 편리하게 사용하도록 하기 위한 함수 체이닝 기법입니다.
res.set =
res.set ||
((key, value) => {
res.setHeader(key, value)
return res // 함수 체이닝을 위해
})
키/값을 인자로 받아 헤더에 세팅하는 set() 메소드 입니다. setHeader() 메소드로 헤더값을 설정하고, 역시 함수 체이닝을 위해 res를 반환합니다.
res.send =
res.send ||
(text => {
if (!res.getHeader("Content-Type")) {
res.setHeader("Content-Type", "text/plain")
}
res.end(text)
})
문자열을 text로 받아 end() 메소드로 응답하는 send() 메소드입니다. 만약 getHeader()로 조회한 뒤 설정된 Content-Type 헤더가 없다면 "text/plain"으로 기본값을 설정합니다.
res.json =
res.json ||
(data => {
res.setHeader("Content-Type", "application/json")
res.end(JSON.stringify(data))
})
json() 메소드는 헤더와 바디를 제이슨 형식으로 응답합니다.
🐤실습 - Response 모듈을 Application에서 사용해 보세요
이렇게 구현한 Response 모듈을 Application에서 사용해야 웹 서버가 제대로 동작할 것입니다. 마치 Middleware 모듈을 Application에서 활용한 것 처럼 말이죠.
Application 코드를 수정하여 Response 메소드를 활용할 수 있도록 개선해 보세요. 코드를 작성하지 못하신 분은 브랜치 이동후 진행하시기 바랍니다.
$ git checkout -f response/methods
힌트: Middleware.run(req, res) 부분 변경, route/index.js, api/posts.js 변경
🐤풀이
같이 풀어 볼까요?
src/Application.js에서 req, res 객체를 사용한 부분이 어디일까요? 네, 바로 미들웨어 구동 메소드인 run() 부분이죠. 미들웨어 함수에 주입하기 위해 여기서부터 req, res를 인자로 전달하는 구조입니다.
이 분을 우리가 만든 Response 객체로 교체하겠습니다.
const Response = require('./Response')
const Application = () => {
const _middleware = Middleware();
const _server = http.createServer((req, res) => {
_middleware.run(req, Response(res)) // Response 객체로 교체
})
// ...
비교적 간단하지요?
그럼 각 미들웨어 함수에서는 기존의 res 메소드 뿐만 아니라 Response 객체의 메소드까지 사용할 수 있게 되었습니다.
현재까지 라우트 컨트롤러가 2개죠?
- index.listPosts()
- apiPost.index()
이걸 새로 제작한 메소드를 이용해 더 심플한 코드로 개선해 보겠습니다.
index.listPosts()는 routers/index.js에 정의 되어 있죠.
const listPosts = () => (req, res, next) => {
fs.readFile(`${publicPath}/index.html`, (err, data) => {
if (err) throw err
res.status(200).set("Content-Type", "text/html").send(data)
})
}
상태코드를 설정하는 status()와 헤더를 설정하는 set(), 그리고 응답하는 send() 메소드를 사용했습니다. 특히 함수 체이닝을 지원하기 때문에 코드를 한 줄로 작성한 부분을 눈여겨 보시기 바랍니다.
apiPost.index()는 api/posts.js에 있습니다.
const index = () => (req, res, next) => {
res.status(200).json(posts)
}
json 응답은 이렇게 간단히 설정할수 있습니다.
코드를 더 단순하게 만들고 싶다면 자주 사용하는 200 상태 코드는 기본 인자값으로 설정하는 방법도 있겠네요.
방금 만들었던 Response 모듈이 익스프레스JS의 세 번째 모듈 되시겠습니다! (초록색 부분)
정리
- 응답 처리를 개선하기위해 Response 모듈을 만들었습니다.
- stauts(), set(), send(), json() 메소드를 추가로 지원합니다.