[TS]NodeJS, Express의 요청, 응답, 에러 핸들링
서론
REST API를 구축하고 클라이언트 단에서 어떤 요청을 보내면 그에 맞는 응답을 백엔드에서 해줍니다. 일반적으로 JSON 형태의 응답을 하고 받은 JSON 데이터를 클라이언트에서 이용하거나 또는 화면에 보여주게 됩니다.
지금까지 Express를 통하여 REST API를 많이 구축해 보았지만 사실 정말 제대로 깔끔하게 그리고 그 용도에 맞게 응답을 해준적이 없는 것 같습니다. 개발을 독학해 오면서 튜토리얼들을 정말 많이 봐왔는데 ExpressJS에서의 대부분의 응답(Response)은 아래와 같은 형식이었습니다.
router.get('/someroute', (req, res) => {
res.json({message: 'hello'})
};
대부분이 이러했고 만약에 요청이 적절하지 않아서 에러가 있다면
if(req.body.content === 'INVALID"){
res.json({ message: "INVALID REQUEST!!!" })
}else{
res.json({ message: "VALID"})
}
};
이러한 식으로 req를 분석해서 응답되는 json의 메시지를 조정하곤 했습니다.
실제로 NodeJS로 REST API를 작성하는 대부분의 튜토리얼들이 위와 같은 방식으로 코드 작성을 해와서 저 역시 적당히 저렇게 코드를 작성해왔습니다.
다른 프레임워크들은
최근 스프링 부트를 경험삼아 한 번 배워봤는데, 지금까지 작성해온 Express와 다른 점을 많이 볼 수 있었습니다. 물론 Express는 라이브러리이고 스프링은 거대한 프레임워크이기 때문에 누가 봐도 다르지만
Best Practice라고 할 수 있는 부분을 많이 볼 수 있었습니다. Entity, Repository, Controller, Service 등으로 클래스, 메소드를 전부 나누어 MVC 구조를 따르고 대부분의 코드 작성에 있어서 틀이 존재했습니다.
Express는 라이브러리이기 때문에 가장 처음부터 하나하나 조립해 나가는 느낌이라면 스프링은 이미 정해져 있는 툴의 사용 방법을 익히는 느낌을 받았습니다. 하지만 스프링의 작성 방식은 위에 말씀드렸던 대로 Best Practice, 수 많은 시간과 뛰어난 개발자들에 의해 어느 정도 가장 좋은 방법, 좋은 구조가 정해져 있었고 이러한 구조를 ExpressJS에 적용하니 제 백엔드 코드가 전보다 훨씬 깔끔해지고 견고해진 것을 볼 수 있었습니다.
그래서
조금 딴 소리를 했지만 여러 튜토리얼 영상과 깃허브의 다른 개발자들의 코드 그리고 다른 백엔드 프레임워크를 통해 배운 백엔드에서 요청과 응답의 처리에 대해 적어 볼까 합니다. 실제로 구글링을 해보니 저와 같은 많은 초보 개발자들(국내, 해외)이 요청, 응답을 대충 처리한 코드들을 확인할 수 있었고(저역시…) 튜토리얼만으로 NodeJS, Express를 경험해 왔다면 res.json 또는 res.send 정도를 적당히 사용해 응답을 했을텐데 지금까지 배운 저만의 Best Practice에 대해 적어볼까 합니다.
아래 내용은 지금까지 제가 배워온 내용을 토대로 작성하였으며 절대 정답이 아닙니다.
요청
클라이언트 단의 요청 코드는 아래와 같습니다.
import axios from "axios"const sendRequest = async (username: string, password: string) => {
try{
const res = await axios.post(`${server}/api`, {username, password})
// do something with res
console.log(res.data.message);
}catch(err){
if(err) console.log(err)
}
}
서버의 /api에 username과 password를 보내고 res라는 이름의 응답을 받습니다.
만약 err가 생긴다면 err를 콘솔에 출력합니다.
응답
자 그러면 Express를 통하여 위 요청에 응답을 해봅시다.
import * as express from "express"
const router = express.Router()router.post("/api", (req, res)=> {
const { username, password } = req.body
if(username.valid() && password.valid()){
res.json({ message: "OK!"})
}else{
res.json({ message: "Not OK..."})
}
})
응답 코드는 위와 같습니다. valid() 메소드는 username과 password가 적절한지 확인하고 boolean을 리턴한다고 가정합니다.
위와 같은 응답을 하면 클라이언트 단에서
console.log(res.data.message);
를 통해
res.json({ message: "OK!"}) 또는 res.json({ message: "Not OK..."})
안의 message를 출력할 수 있을 것입니다. 하지만 우리가 사용하는 HTTP 통신에는 상태코드가 존재합니다.
위에서 수많은 HTTP 상태 코드를 확인할 수 있습니다.
만약 요청에서 들어온 username과 password가 유효하지 않다면 406 코드를 통해서 응답을 해보겠습니다.
import * as express from "express"
const router = express.Router()router.post("/api", (req, res)=> {
const { username, password } = req.body
if(username.valid() && password.valid()){
res.json({ message: "OK!"})
}else{
res.status(406)
}
})
status 메소드를 통해 406 응답을 할 수 있습니다. 만약 username.valid() 또는 password.valid()가 false를 반환한다면 클라이언트 단에서 406에러를 확인할 수 있습니다. 하지만 여기서 우리는 유저네임에 오류가 있는건지 비밀번호에 오류가 있는건지 알려주고 싶다고 가정합시다.
router.post("/api", (req, res)=> {
const { username, password } = req.body
if(!username.valid()){
res.status(406)
}else if(!password.valid()){
res.status(406)
}else{
res.json({ message: "OK!"})
}
})
else if를 통해 나누면 이렇게 valid하지 않은 경우엔 406 응답을 하게 만들 수 있을 것입니다.
하지만 406응답만으로는 어떤 부분이 틀렸는지 알 수가 없습니다. 따라서 우리는 메시지도 함께 전해주기로 합니다.
router.post("/api", (req, res)=> {
const { username, password } = req.body
if(!username.valid()){
res.status(406).json({message: "Username invalid"})
}else if(!password.valid()){
res.status(406).json({message: "Password invalid"})
}else{
res.json({ message: "OK!"})
}
})
Express의 공식 문서에서는 위와 같이 에러를 처리하도록 말하고 있습니다. 위 방법은 Postman을 사용하면 message를 전달받을 수 있었지만 React에서는(크롬 브라우저) 406 상태 코드는 전달받을 수 있었지만 res.json 내의 message는 받을 수 없었습니다.
상태 코드와 메시지를 동시에 전달받을 수 있는 방법을 하루종일 구글링 해보았지만 찾을 수 없었습니다.제 생각에 200번대의 응답이 아닌 다른 응답을 받으면 클라이언트에서 더 이상의 응답 메소드를 받지 않거나 아니면 제가 올바른 방법을 사용하고 있지 않고 있었던것 같습니다.
Try, Catch를 이용한 더 좋은 방법
상태 코드와 에러 메시지의 내용을 동시에 전달해줄 방법을 찾던 중 스프링에서 사용하는 방법이 있었습니다.(아마 다른 백엔드 프레임워크, 라이브러리에서도 사용하는 방법일 것입니다.)
그건 바로 Try, Catch의 Error를 이용하는 것입니다.
스프링에서는 exception이라는 패키지를 만들어 각 어플리케이션 마다 생길 수 있는 에러를 개발자들이 각자 커스텀하여 처리하도록 하고 있습니다. 저는 이러한 방식을 Express에서 적용해 보았습니다.
먼저 CustomError.ts라는 파일을 작성합니다.
export class CustomError extends Error {
constructor(message) {
super(message); // 여기에 더 message 이외에 더 많은 종류의 응답을 추가할 수 있습니다!
}
}
Error가 message 요소를 생성자를 통해 만들고 super를 통해 메시지를 받을 수 있도록 합니다.
그리고 try, catch를 사용하고 try문에서 요청에 에러가 있다면 throw를 통해 예외(exception)를 발생시켜서 에러를 처리하도록 합니다.
CustomError를 import해주고 위처럼 에러를 처리합니다. 만약 요청에 오류가 있다면 throw를 통해 CustomError내에 메시지를 지정해 줄 수 있게 됩니다.
else if 또는 else를 사용하지 않고 조건문 코드 작성을 했고 406 상태를 보내는 에러도 catch안에 한줄로 넣어버려서 굉장히 코드가 깔끔해졌습니다.
catch문에서 406과 함께 json을 전달하면 클라이언트 단에서도 catch문을 작성하여
catch(err){
if(err) {console.log(err.response)}
}
//클라이언트 단
err.response를 통하여 message의 내용을 전달 받을 수 있었습니다. 위와 같은 방법이 전보다 훨씬 코드를 보기도 깔끔하고 작성하기도 좋아보입니다.(이유를 찾을 수 없는 에러도 없고 상태 코드도 정상적으로 전달되고…)
결론
res.status에서 200번대의 응답이 아닌 다른 HTTP 상태 코드를 반환한다면 뒤의 json을 전달하지 않는 에러를 찾다가 다른 백엔드 프레임워크인 스프링을 통해 오류를 해결한 내용에 대해 적어보았습니다. postman에서는 응답이 잘되는데 왜 react에서는 못받는지 아직 이유를 잘 모르겠습니다. 하지만 그 와중에 보다 더 좋은 방법을 찾게 되었습니다.
res.status(...).json(...)
을 통해 응답하는 방법은 Express 공식 문서에도 적혀있는데 그냥 사용하면 클라이언트에 상태 코드만을 전달했고 catch문 안에서 사용하면 클라이언트 단에서도 catch 문의 err를 통해 메시지를 전달받을 수 있었습니다. 왜 안됐는지 정확한 이유를 알게 되면 이 글을 다시 업데이트하겠지만 사실 try, catch문을 사용하는 것이 더 좋은 방법이기 때문에 위 방법(try,catch를 이용한 방법)을 계속 사용해가면 되겠습니다.