1. Express Middleware
error handling 전에, Express에서 middleware를 어떻게 돌리는지부터 알아보자. (기본 중의 기본이라.. 먼저 정리했어야하지 않나 싶은 내용이긴 하다)
express의 미들웨어란, 클라이언트에서 요청이 들어오고 서버에서 응답이 나갈 때까지 req와 res 객체를 대상으로 진행되는 동작들의 집합이다. request -> middleware1 -> middlware2 -> ... -> middlewareN -> response 의 과정을 거친다고 보면 된다.
일반적으로 미들웨어는 request object, response object, next function을 인자로 가지는 funtion이다. request는 말 그대로 유저의 요청 오브젝트, response는 서버가 보내는 응답 오브젝트, 그리고 next()는 현재 미들웨어 다음에 존재하는 미들웨어를 call하는 함수이다.
middleware 코드가 어떻게 동작하는지 살펴보자!
Using Express middleware
Using middleware Express is a routing and middleware web framework that has minimal functionality of its own: An Express application is essentially a series of middleware function calls. Middleware functions are functions that have access to the request ob
expressjs.com
공식 문서에 있는 코드를 그대로 가져왔다. express app을 이용한 가장 간단한 미들웨어 함수 코드다.
const express = require('express')
const app = express()
app.use((req, res, next) => {
console.log('Time:', Date.now())
next()
})
app.use(<middleware function>)의 형태를 띄고있다. 앞서 설명한대로 미들웨어 함수는 req 객체, res 객체, next 함수를 인자로 가지고 있다. app.use를 사용했으므로 저 안의 미들웨어는 유저의 "every single request"마다 실행될 것이다. 유저가 해당 서버의 페이지를 새로고침하거나 어떤 링크를 누를 때마다 콘솔에 현재 시간이 찍힌다는 의미이다. 위 코드의 미들웨어는 콘솔에 현재 시간을 찍고 next()를 호출한다. 이 다음 미들웨어가 실행된다는 말이다.
이번엔 어떤 path를 받는 미들웨어 코드의 예시이다.
app.use('/user/:id', (req, res, next) => {
console.log('Request Type:', req.method)
next()
})
"<사이트의 url>/user/<특정 id숫자값>"이 서버 요청으로 들어오면, 위의 미들웨어가 실행된다. 역시 app.use이므로 요청이 어떤 method임에 상관 없이 요청이 들어올 때마다 실행된다. 만약 유저가 get요청을 했으면 "Request Type: GET", post 요청을 했다면 "Request Type: POST"가 콘솔에 찍힐 것이다. 여기서 역시 next()를 호출했으므로 req type을 콘솔에 찍고 다음 미들웨어가 실행되게 된다.
위 코드 바로 밑에 아래 코드를 추가했다.
app.get('/user/:id', (req, res, next) => {
res.send('USER')
})
res.send는 'USER'라는 plain text를 유저에게 응답으로 보내는 부분이다. 사용자에게 res 객체 전송을 완료했으므로 Req->Res cycle이 끝났다고 볼 수 있다.
req type을 찍는 미들웨어가 next()를 호출하면 사용자에게 'USER'응답을 전송하는 위 미들웨어가 실행된다. next()가 다음 middleware를 호출한다는 것은 바로 이 의미이다.
같은 path에 관한 연속된 middleware들은 굳이 코드를 나누지 않고 이렇게 한꺼번에 쓸 수 있다. use() 함수의 인자를 middleware들로 계속 추가하는 것이다.
app.use('/user/:id', (req, res, next) => {
console.log('Request URL:', req.originalUrl)
next()
}, (req, res, next) => {
console.log('Request Type:', req.method)
next()
})
app.use는 클라이언트가 보내는 모든 req에 대해 method 상관 없이 실행된다. app.METHOD_NAME으로 특정 method에 관한 처리를 할 수 있다.
app.get('/user/:id', (req, res, next) => {
console.log('ID:', req.params.id)
next()
}, (req, res, next) => {
res.send('User Info')
})
method가 get이든 put이든 delete이든 post이든 상관없이 실행됐던 app.use와 다르게, app.get은 클라이언트의 요청이 get일 때만 실행된다. req.params.id로 path의 :id부분을 콘솔에 찍은 뒤, 'User Info'라는 텍스트를 응답으로 전송하는 코드다.
express가 추가로 제공하는 기능 중, middleware를 skip할 수 있는 기능이 있다. next 함수에 특정 인자를 추가하는 것이다.
app.get('/user/:id', (req, res, next) => {
// if the user ID is 0, skip to the next route
if (req.params.id === '0') next('route')
// otherwise pass the control to the next middleware function in this stack
else next()
}, (req, res, next) => {
// send a regular response
res.send('regular')
})
// handler for the /user/:id path, which sends a special response
app.get('/user/:id', (req, res, next) => {
res.send('special')
})
:id에서 id의 값이 0이면 'special'이라는 text가, 그렇지 않으면 'regular'이라는 text가 유저에게 응답으로 보내지는 시스템이다. id가 0이면 다음 미들웨어를 스킵하는 next('route')코드가 실행되어서 res.send('regular')가 아닌 res.send('special')로 가기 때문이다. 반대로 id가 0이 아니라면 next()코드가 실행되어 res.send('regular')에서 응답 처리가 끝나 res.send('special')코드는 실행되지 않는다.
보통 실제 프로젝트에서 app.METHOD_NAME을 이용해 미들웨어들을 집어넣을 땐 함수를 따로 만들어 넣게 된다.
function logOriginalUrl (req, res, next) {
console.log('Request URL:', req.originalUrl)
next()
}
function logMethod (req, res, next) {
console.log('Request Type:', req.method)
next()
}
const logStuff = [logOriginalUrl, logMethod]
app.get('/user/:id', logStuff, (req, res, next) => {
res.send('User Info')
})
logStuff 대신 logOriginalUrl, logMethod를 직접 함수 인자로 나열해줘도 된다.
추가적으로 다룰 내용이 있긴 하지만 express를 이용한 일반적인 프로젝트를 진행할 때 알고 있어야할 middleware 관련 지식은 이정도면 충분하다.
2. Error Handling
express에서 error handling은 error handling을 위한 custom middleware를 설정해주고(사실 이건 선택사항이긴 하다. express엔 built-in error handler가 존재하기 때문) next()함수에 string 'route'를 제외한 인자를 집어넣는 것이다. next('route')는 앞서 설명했듯 다음 미들웨어를 스킵하는 역할을 하기 때문에.. error handling과는 관련 없다.
공식문서를 참고하며 어떻게 해야하는지 살펴보자. 들어가기 전에, 기본적으로 try-catch 블락이 어떤 것인지, throw error가 어떤 것인지 알고 있다고 가정한다.
Express error handling
Error Handling Error Handling refers to how Express catches and processes errors that occur both synchronously and asynchronously. Express comes with a default error handler so you don’t need to write your own to get started. Catching Errors It’s impor
expressjs.com
공식문서는 sync / async 함수에서의 error handling을 나눠 설명하고 있다.
sync 함수에서 에러 발생시 함수가 throw error를 하면 별다른 후속조치가 필요하지 않다. express가 알아서 에러를 받고
그러나 async 함수에서 에러 발생시, 에러를 캐치하여 next(err) 호출을 명시적으로 해줘야 한다는데.. express 5부터는 promise에 reject가 실행되면 자동으로 next()를 불러준다고 한다. 뭐지.. 왜.. 나눠 설명한거지....
app.get('/user/:id', async (req, res, next) => {
const user = await getUserById(req.params.id)
res.send(user)
})
가령 위와 같은 비동기 실행 함수가 존재하는 코드에서, getUserById 함수가 어떤 이유에서 에러가 나면(DB 서버와의 연결이 끊겼다거나, :id가 valid하지 않다거나...) 자동으로 express error handler가 작동한다는 것이다. 그렇지 않으면 위 미들웨어의 body 부분을 try 블락로 묶고, catch(err)을 한 뒤 next(err) 호출을 해줘야한다.
app.get('/', [
function (req, res, next) {
fs.writeFile('/inaccessible-path', 'data', next)
},
function (req, res) {
res.send('OK')
}
])
이건 그냥 가져와본 공식문서의 예시이다. node.js의 fs.writeFile()은 path, content, callback의 인자로 이뤄진 비동기 함수이다. 만약 path 파일에 content 내용을 적다 에러가 나면 callback 함수의 인자로 error object가 전달된다(없으면 전달안됨). 위 코드의 첫번째 미들웨어에서, 만약 error가 없다면 next()가 호출되고, error가 있다면 next(error);가 호출될 것이다. 전자는 얌전히 다음 미들웨어를, 후자는 next에 무언가 인자가 전달되었으므로 error handler를 실행할 것이다.
next 인자를 이용해서 async function의 error handling을 아래와 같이 할 수도 있다.
app.get('/', [
function (req, res, next) {
fs.readFile('/maybe-valid-file', 'utf-8', (err, data) => {
res.locals.data = data
next(err)
})
},
function (req, res) {
res.locals.data = res.locals.data.split(',')[1]
res.send(res.locals.data)
}
])
fs.readFile 실행 도중 error가 발생하면 err parameter가 설정되고, 그러면 next(err)의 인자가 존재하는 채로 넘어가 express의 error handler가 작동한다. 에러가 없으면 next()가 실행되어 data split 작업이 진행될 것이고.
만약에! next(err)를 진행하지 않고, 저 두 미들웨어를 하나로 합쳤다고 가정하면 readFile도중에 에러가 났을 때 express error handler는 작동하지 않은채 fs모듈이 던진 에러에 노드 프로그램 자체가 죽어버릴 것이다. 그러니 방금 설명했던 방법들으로 callback 인자를 이용해 async function의 에러핸들링을 할 수 있다.
이렇게 next(err)로 next에 'route' string을 제외한 인자를 넘기면 express의 built-in error handler가 작동하게 된다. err.statusCode에 있는 상태 코드가 res.statusCode로 들어가고 이에 따라 res.statusMessage가 생성된다. 만약 존재한다면, err.stack에는 상세한 에러 메세지가 위치하게 되고. 이렇게 생성된 에러코드와 메세지를 response로 보내는 일을 하는 것이 express의 default error 핸들러라고 보면 된다.
근데 만약 우리의 custom error handler를 만들고 싶다면? next(err)을 호출했을 때 err 오브젝트를 직접 다뤄서 다른 response를 보내고 싶다면 어떻게 해야할까? 미들웨어를 하나 작성하면 된다. 근데 인자가 4개인.
원래 보편적인 미들웨어의 인자는 req,res,next 3개로 정의된다고 설명한 바 있다. custom error handler 역시 미들웨어로 작성하는데, 맨 앞에 err 인자 하나가 더 붙는다. 예시 코드를 보자.
app.use((err, req, res, next) => {
console.error(err.stack)
res.status(500).send('Something broke!')
})
error handler 미들웨어는 err, req, res, next 4개의 인자가 존재한다. err는 이 전 미들웨어가 next(err)을 호출했을 때의 인자이다. next(err)가 호출되면 인자가 3개 이하인 다음 미들웨어들의 실행은 전부 skip된 채 위 핸들러가 실행된다. next(err)을 실행하면 콘솔에 err.stack의 내용이 찍히고 http 상태코드가 500으로 셋되고 서버로부터 'Something broke'라는 text를 응답으로 받게되는 것이다.
한가지 알아둬야 할 점은, 이 error handling middleware는 기본적으로 express 프로젝트를 돌리기 위해 실행되는 미들웨어(session, urlencoded, methodOverride 등)와 웹 동작에 필요한 route 밑에 넣어줘야 한다는 것이다.
또한 error handler 역시 여러개의 미들웨어로 돌릴 수 있다. app.use((err,req,res,next)=>{})형식의 미들웨어를 여러개 작성한 뒤 각각 순서에 맞춰 next(err)을 호출하면 되는 것이다. error handler middleware에서 명시적으로 응답을 보내주지 않는다면 꼭 next(err)을 호출해야한다는 것을 잊지말자. 에러 핸들러에서 응답을 보내지 않거나 next(err)로 에러를 다음 핸들러에게 넘기지 않으면 프로세스가 거기서 멈추고 garbage collector에게 삭제당한다..
3. Express Error
express에서 미들웨어, 에러 핸들러에 관한 전반적인 사항을 훑어봤으니 이제 프로젝트에 적용시켜보자.
웹 어플리케이션 실행 도중 에러가 날때마다 1.에러 코드(statusCode)와 2.에러 메세지(message)를 사용자에게 ejs파일로 띄울 것이다. 이를 위해 프로젝트의 utils/expressError.js에 위 두가지 내용을 담은 간단한 error 객체를 하나 만들었다.
// utils/expressError.js
class ExpressError extends Error {
constructor(message, statusCode) {
super();
this.message = message;
this.statusCode = statusCode;
}
}
module.exports = ExpressError;
ExpressError은 js의 Error class를 상속받은 class이다. 생성자로 message, statusCode를 받아 객체에 저장함을 알 수 있다. error handler가 돌아가려면 일단 node의 error 객체여야 하므로... 이렇게 상속을 받은 것이다.
이제 저번에 유저의 auth와 관련해 설명 생략했던 ExpressError에 대해 이해할 수 있다.
// controllers/user.js
const { userSchema } = require("../utils/validateSchema");
module.exports.validateUser = (req, res, next) => {
const { error } = userSchema.validate(req.body);
if (error) {
const message = error.details.map((el) => el.message).join(",");
throw new ExpressError(message, 400);
} else {
next();
}
};
userSchma는 schema validate 기능을 가진 객체이다. 에전에 model 설계 포스트에서 validate와 관련한 내용을 다룬 바 있다.
userSchema.validate()는 인자로 전달된 object가 설정한 옵션에 맞는지 확인하고, 만약 에러가 있다면 error key를 반환한다. 없으면 반환하지 않는다. const { error } 을 이용한 destructuring에서 validate에 성공하면 error에 null값이 할당될 것이고, ExpressError 객체가 throw된다. (에러 없으면 next()로 다음 미들웨어 실행) 앞서 express에서 throw error가 일어나면 별다른 후속조치 없이 error handler가 작동된다고 얘기한 바 있다.
app.js에 custom error handler를 다음과 같이 구성했다. app과 관련된 모든 동작 가장 밑에 존재하는 미들웨어이다.
// app.js
// error 받는 마지막 middleware
app.use((err, req, res, next) => {
if (!err.message) err.message = "something went wrong..";
if (!err.statusCode) err.statusCode = 500;
res.status(err.statusCode).render("error", { err });
});
message와 statusCode가 설정되어있지 않으면 서버 내부의 문제를 뜻하는 HTTP 500을 붙여서 views/error.ejs 파일을 렌더할 수 있도록 했다. 에러 메세지 렌더링을 위한 error.ejs파일은 알아서 구성해보도록 하자!
더하여, 에러 핸들러 바로 위에 유저가 invalid한 url path를 입력했을 때 에러를 던지는 과정 역시 추가했다.
// app.js
app.all("*", (req, res, next) => {
next(new ExpressError("page not found..TT", 404));
});
app.all("*")은 위에서 걸리지 않은 모든 path에 관하여 이 미들웨어를 받겠다는 뜻이다. 404 not found에러를 구현했다!
4. catchAsync
mongoose ORM을 이용해서 비동기적으로 DB와 통신을 하기 위해 async / await를 적극적으로 이용했다. 이때 DB와 통신 과정에서 에러가 일어났을 때 에러 핸들링을 하는 과정이 필요하다. await가 붙은 비동기 함수의 에러 핸들링은 try...catch블락으로 해당 코드 부분을 묶는 것이 전통적이다. 그러나 모든 async function에 대해 try...catch 블락을 구성하는 것은 비효율적이고 가독성 면에서 좋지도 않다.
그래서 async function 자체를 인자로 받아서 .then().catch()를 직접 구성하는 함수를 하나 짤 것이다. utils/catchAsync.js 파일에 아래 함수를 구성했다.
// utils/catchAsync.js
module.exports = (func) => {
return (req, res, next) => {
func(req, res, next).catch((e) => next(e));
};
};
func라는 함수를 인자로 받는 함수이고, func(req, res, next) 기능을 가지는 함수를 return하는 함수이다. 말로 설명하니 복잡하군
func은 async middleware function이다. 암시적으로 promise를 반환하며, DB서버와 통신을 하는 기능을 가져서 비동기 함수를 위한 에러 핸들링이 필요하다. 미들웨어이므로 func(req,res,next)로 동작하며, 말했듯 promise를 반환하므로 then().catch() 사용이 가능하다. 에러가 생기면 catch() 인자의 함수에 err 객체를 인자로 전달한다. 앞서 next에 어떤 인자를 주면 error handler가 작동된다고 설명한 바 있다. 에러가 생겨 .catch가 실행되면 next(e)로 인해 app.use((err,req,res,next)=>{})의 핸들러가 실행된다.
그러니까, 다시 한 번 정리하면 위 함수는 인자로 특정 async function을 주었을 때 그 특정 function 기능을 하면서 .catch()로 에러 핸들링이 되는 함수를 반환하는 것이다..... 최대한 말로 풀어서 설명해봤는데 나중의 내가 이걸 보고 이해할 수 있을런지 ㅜ
실제로는 아래와 같이 쓰인다. 앞선 포스트에서 설명을 생략한 catchAsync()부분이다.
// routes/user.js
const catchAsync = require("../utils/catchAsync");
router.post(
"/register",
userController.validateUser,
catchAsync(userController.register)
);
인자로 userController.register라는 미들웨어 함수를 주었다. register은 유저가 post로 보낸 회원가입 정보를 DB에 넣는 과정이 필요하다. 이 작업이 비동기적으로 이뤄지므로 register은 async function이다. catchAsync으로 묶어 register기능을 똑같이 하면서도 에러 e가 발생했을 때 catch하여 next(e)를 호출하는 기능을 추가한 것이다. 앞으로 모든 async function은 catchAsync으로 묶을 것이다.
다음엔 식단관리 앱의 꽃! 인 음식, Food와 관련한 CRUD 구현 과정을 포스트하겠다.
1. Express Middleware
error handling 전에, Express에서 middleware를 어떻게 돌리는지부터 알아보자. (기본 중의 기본이라.. 먼저 정리했어야하지 않나 싶은 내용이긴 하다)
express의 미들웨어란, 클라이언트에서 요청이 들어오고 서버에서 응답이 나갈 때까지 req와 res 객체를 대상으로 진행되는 동작들의 집합이다. request -> middleware1 -> middlware2 -> ... -> middlewareN -> response 의 과정을 거친다고 보면 된다.
일반적으로 미들웨어는 request object, response object, next function을 인자로 가지는 funtion이다. request는 말 그대로 유저의 요청 오브젝트, response는 서버가 보내는 응답 오브젝트, 그리고 next()는 현재 미들웨어 다음에 존재하는 미들웨어를 call하는 함수이다.
middleware 코드가 어떻게 동작하는지 살펴보자!
Using Express middleware
Using middleware Express is a routing and middleware web framework that has minimal functionality of its own: An Express application is essentially a series of middleware function calls. Middleware functions are functions that have access to the request ob
expressjs.com
공식 문서에 있는 코드를 그대로 가져왔다. express app을 이용한 가장 간단한 미들웨어 함수 코드다.
const express = require('express')
const app = express()
app.use((req, res, next) => {
console.log('Time:', Date.now())
next()
})
app.use(<middleware function>)의 형태를 띄고있다. 앞서 설명한대로 미들웨어 함수는 req 객체, res 객체, next 함수를 인자로 가지고 있다. app.use를 사용했으므로 저 안의 미들웨어는 유저의 "every single request"마다 실행될 것이다. 유저가 해당 서버의 페이지를 새로고침하거나 어떤 링크를 누를 때마다 콘솔에 현재 시간이 찍힌다는 의미이다. 위 코드의 미들웨어는 콘솔에 현재 시간을 찍고 next()를 호출한다. 이 다음 미들웨어가 실행된다는 말이다.
이번엔 어떤 path를 받는 미들웨어 코드의 예시이다.
app.use('/user/:id', (req, res, next) => {
console.log('Request Type:', req.method)
next()
})
"<사이트의 url>/user/<특정 id숫자값>"이 서버 요청으로 들어오면, 위의 미들웨어가 실행된다. 역시 app.use이므로 요청이 어떤 method임에 상관 없이 요청이 들어올 때마다 실행된다. 만약 유저가 get요청을 했으면 "Request Type: GET", post 요청을 했다면 "Request Type: POST"가 콘솔에 찍힐 것이다. 여기서 역시 next()를 호출했으므로 req type을 콘솔에 찍고 다음 미들웨어가 실행되게 된다.
위 코드 바로 밑에 아래 코드를 추가했다.
app.get('/user/:id', (req, res, next) => {
res.send('USER')
})
res.send는 'USER'라는 plain text를 유저에게 응답으로 보내는 부분이다. 사용자에게 res 객체 전송을 완료했으므로 Req->Res cycle이 끝났다고 볼 수 있다.
req type을 찍는 미들웨어가 next()를 호출하면 사용자에게 'USER'응답을 전송하는 위 미들웨어가 실행된다. next()가 다음 middleware를 호출한다는 것은 바로 이 의미이다.
같은 path에 관한 연속된 middleware들은 굳이 코드를 나누지 않고 이렇게 한꺼번에 쓸 수 있다. use() 함수의 인자를 middleware들로 계속 추가하는 것이다.
app.use('/user/:id', (req, res, next) => {
console.log('Request URL:', req.originalUrl)
next()
}, (req, res, next) => {
console.log('Request Type:', req.method)
next()
})
app.use는 클라이언트가 보내는 모든 req에 대해 method 상관 없이 실행된다. app.METHOD_NAME으로 특정 method에 관한 처리를 할 수 있다.
app.get('/user/:id', (req, res, next) => {
console.log('ID:', req.params.id)
next()
}, (req, res, next) => {
res.send('User Info')
})
method가 get이든 put이든 delete이든 post이든 상관없이 실행됐던 app.use와 다르게, app.get은 클라이언트의 요청이 get일 때만 실행된다. req.params.id로 path의 :id부분을 콘솔에 찍은 뒤, 'User Info'라는 텍스트를 응답으로 전송하는 코드다.
express가 추가로 제공하는 기능 중, middleware를 skip할 수 있는 기능이 있다. next 함수에 특정 인자를 추가하는 것이다.
app.get('/user/:id', (req, res, next) => {
// if the user ID is 0, skip to the next route
if (req.params.id === '0') next('route')
// otherwise pass the control to the next middleware function in this stack
else next()
}, (req, res, next) => {
// send a regular response
res.send('regular')
})
// handler for the /user/:id path, which sends a special response
app.get('/user/:id', (req, res, next) => {
res.send('special')
})
:id에서 id의 값이 0이면 'special'이라는 text가, 그렇지 않으면 'regular'이라는 text가 유저에게 응답으로 보내지는 시스템이다. id가 0이면 다음 미들웨어를 스킵하는 next('route')코드가 실행되어서 res.send('regular')가 아닌 res.send('special')로 가기 때문이다. 반대로 id가 0이 아니라면 next()코드가 실행되어 res.send('regular')에서 응답 처리가 끝나 res.send('special')코드는 실행되지 않는다.
보통 실제 프로젝트에서 app.METHOD_NAME을 이용해 미들웨어들을 집어넣을 땐 함수를 따로 만들어 넣게 된다.
function logOriginalUrl (req, res, next) {
console.log('Request URL:', req.originalUrl)
next()
}
function logMethod (req, res, next) {
console.log('Request Type:', req.method)
next()
}
const logStuff = [logOriginalUrl, logMethod]
app.get('/user/:id', logStuff, (req, res, next) => {
res.send('User Info')
})
logStuff 대신 logOriginalUrl, logMethod를 직접 함수 인자로 나열해줘도 된다.
추가적으로 다룰 내용이 있긴 하지만 express를 이용한 일반적인 프로젝트를 진행할 때 알고 있어야할 middleware 관련 지식은 이정도면 충분하다.
2. Error Handling
express에서 error handling은 error handling을 위한 custom middleware를 설정해주고(사실 이건 선택사항이긴 하다. express엔 built-in error handler가 존재하기 때문) next()함수에 string 'route'를 제외한 인자를 집어넣는 것이다. next('route')는 앞서 설명했듯 다음 미들웨어를 스킵하는 역할을 하기 때문에.. error handling과는 관련 없다.
공식문서를 참고하며 어떻게 해야하는지 살펴보자. 들어가기 전에, 기본적으로 try-catch 블락이 어떤 것인지, throw error가 어떤 것인지 알고 있다고 가정한다.
Express error handling
Error Handling Error Handling refers to how Express catches and processes errors that occur both synchronously and asynchronously. Express comes with a default error handler so you don’t need to write your own to get started. Catching Errors It’s impor
expressjs.com
공식문서는 sync / async 함수에서의 error handling을 나눠 설명하고 있다.
sync 함수에서 에러 발생시 함수가 throw error를 하면 별다른 후속조치가 필요하지 않다. express가 알아서 에러를 받고
그러나 async 함수에서 에러 발생시, 에러를 캐치하여 next(err) 호출을 명시적으로 해줘야 한다는데.. express 5부터는 promise에 reject가 실행되면 자동으로 next()를 불러준다고 한다. 뭐지.. 왜.. 나눠 설명한거지....
app.get('/user/:id', async (req, res, next) => {
const user = await getUserById(req.params.id)
res.send(user)
})
가령 위와 같은 비동기 실행 함수가 존재하는 코드에서, getUserById 함수가 어떤 이유에서 에러가 나면(DB 서버와의 연결이 끊겼다거나, :id가 valid하지 않다거나...) 자동으로 express error handler가 작동한다는 것이다. 그렇지 않으면 위 미들웨어의 body 부분을 try 블락로 묶고, catch(err)을 한 뒤 next(err) 호출을 해줘야한다.
app.get('/', [
function (req, res, next) {
fs.writeFile('/inaccessible-path', 'data', next)
},
function (req, res) {
res.send('OK')
}
])
이건 그냥 가져와본 공식문서의 예시이다. node.js의 fs.writeFile()은 path, content, callback의 인자로 이뤄진 비동기 함수이다. 만약 path 파일에 content 내용을 적다 에러가 나면 callback 함수의 인자로 error object가 전달된다(없으면 전달안됨). 위 코드의 첫번째 미들웨어에서, 만약 error가 없다면 next()가 호출되고, error가 있다면 next(error);가 호출될 것이다. 전자는 얌전히 다음 미들웨어를, 후자는 next에 무언가 인자가 전달되었으므로 error handler를 실행할 것이다.
next 인자를 이용해서 async function의 error handling을 아래와 같이 할 수도 있다.
app.get('/', [
function (req, res, next) {
fs.readFile('/maybe-valid-file', 'utf-8', (err, data) => {
res.locals.data = data
next(err)
})
},
function (req, res) {
res.locals.data = res.locals.data.split(',')[1]
res.send(res.locals.data)
}
])
fs.readFile 실행 도중 error가 발생하면 err parameter가 설정되고, 그러면 next(err)의 인자가 존재하는 채로 넘어가 express의 error handler가 작동한다. 에러가 없으면 next()가 실행되어 data split 작업이 진행될 것이고.
만약에! next(err)를 진행하지 않고, 저 두 미들웨어를 하나로 합쳤다고 가정하면 readFile도중에 에러가 났을 때 express error handler는 작동하지 않은채 fs모듈이 던진 에러에 노드 프로그램 자체가 죽어버릴 것이다. 그러니 방금 설명했던 방법들으로 callback 인자를 이용해 async function의 에러핸들링을 할 수 있다.
이렇게 next(err)로 next에 'route' string을 제외한 인자를 넘기면 express의 built-in error handler가 작동하게 된다. err.statusCode에 있는 상태 코드가 res.statusCode로 들어가고 이에 따라 res.statusMessage가 생성된다. 만약 존재한다면, err.stack에는 상세한 에러 메세지가 위치하게 되고. 이렇게 생성된 에러코드와 메세지를 response로 보내는 일을 하는 것이 express의 default error 핸들러라고 보면 된다.
근데 만약 우리의 custom error handler를 만들고 싶다면? next(err)을 호출했을 때 err 오브젝트를 직접 다뤄서 다른 response를 보내고 싶다면 어떻게 해야할까? 미들웨어를 하나 작성하면 된다. 근데 인자가 4개인.
원래 보편적인 미들웨어의 인자는 req,res,next 3개로 정의된다고 설명한 바 있다. custom error handler 역시 미들웨어로 작성하는데, 맨 앞에 err 인자 하나가 더 붙는다. 예시 코드를 보자.
app.use((err, req, res, next) => {
console.error(err.stack)
res.status(500).send('Something broke!')
})
error handler 미들웨어는 err, req, res, next 4개의 인자가 존재한다. err는 이 전 미들웨어가 next(err)을 호출했을 때의 인자이다. next(err)가 호출되면 인자가 3개 이하인 다음 미들웨어들의 실행은 전부 skip된 채 위 핸들러가 실행된다. next(err)을 실행하면 콘솔에 err.stack의 내용이 찍히고 http 상태코드가 500으로 셋되고 서버로부터 'Something broke'라는 text를 응답으로 받게되는 것이다.
한가지 알아둬야 할 점은, 이 error handling middleware는 기본적으로 express 프로젝트를 돌리기 위해 실행되는 미들웨어(session, urlencoded, methodOverride 등)와 웹 동작에 필요한 route 밑에 넣어줘야 한다는 것이다.
또한 error handler 역시 여러개의 미들웨어로 돌릴 수 있다. app.use((err,req,res,next)=>{})형식의 미들웨어를 여러개 작성한 뒤 각각 순서에 맞춰 next(err)을 호출하면 되는 것이다. error handler middleware에서 명시적으로 응답을 보내주지 않는다면 꼭 next(err)을 호출해야한다는 것을 잊지말자. 에러 핸들러에서 응답을 보내지 않거나 next(err)로 에러를 다음 핸들러에게 넘기지 않으면 프로세스가 거기서 멈추고 garbage collector에게 삭제당한다..
3. Express Error
express에서 미들웨어, 에러 핸들러에 관한 전반적인 사항을 훑어봤으니 이제 프로젝트에 적용시켜보자.
웹 어플리케이션 실행 도중 에러가 날때마다 1.에러 코드(statusCode)와 2.에러 메세지(message)를 사용자에게 ejs파일로 띄울 것이다. 이를 위해 프로젝트의 utils/expressError.js에 위 두가지 내용을 담은 간단한 error 객체를 하나 만들었다.
// utils/expressError.js
class ExpressError extends Error {
constructor(message, statusCode) {
super();
this.message = message;
this.statusCode = statusCode;
}
}
module.exports = ExpressError;
ExpressError은 js의 Error class를 상속받은 class이다. 생성자로 message, statusCode를 받아 객체에 저장함을 알 수 있다. error handler가 돌아가려면 일단 node의 error 객체여야 하므로... 이렇게 상속을 받은 것이다.
이제 저번에 유저의 auth와 관련해 설명 생략했던 ExpressError에 대해 이해할 수 있다.
// controllers/user.js
const { userSchema } = require("../utils/validateSchema");
module.exports.validateUser = (req, res, next) => {
const { error } = userSchema.validate(req.body);
if (error) {
const message = error.details.map((el) => el.message).join(",");
throw new ExpressError(message, 400);
} else {
next();
}
};
userSchma는 schema validate 기능을 가진 객체이다. 에전에 model 설계 포스트에서 validate와 관련한 내용을 다룬 바 있다.
userSchema.validate()는 인자로 전달된 object가 설정한 옵션에 맞는지 확인하고, 만약 에러가 있다면 error key를 반환한다. 없으면 반환하지 않는다. const { error } 을 이용한 destructuring에서 validate에 성공하면 error에 null값이 할당될 것이고, ExpressError 객체가 throw된다. (에러 없으면 next()로 다음 미들웨어 실행) 앞서 express에서 throw error가 일어나면 별다른 후속조치 없이 error handler가 작동된다고 얘기한 바 있다.
app.js에 custom error handler를 다음과 같이 구성했다. app과 관련된 모든 동작 가장 밑에 존재하는 미들웨어이다.
// app.js
// error 받는 마지막 middleware
app.use((err, req, res, next) => {
if (!err.message) err.message = "something went wrong..";
if (!err.statusCode) err.statusCode = 500;
res.status(err.statusCode).render("error", { err });
});
message와 statusCode가 설정되어있지 않으면 서버 내부의 문제를 뜻하는 HTTP 500을 붙여서 views/error.ejs 파일을 렌더할 수 있도록 했다. 에러 메세지 렌더링을 위한 error.ejs파일은 알아서 구성해보도록 하자!
더하여, 에러 핸들러 바로 위에 유저가 invalid한 url path를 입력했을 때 에러를 던지는 과정 역시 추가했다.
// app.js
app.all("*", (req, res, next) => {
next(new ExpressError("page not found..TT", 404));
});
app.all("*")은 위에서 걸리지 않은 모든 path에 관하여 이 미들웨어를 받겠다는 뜻이다. 404 not found에러를 구현했다!
4. catchAsync
mongoose ORM을 이용해서 비동기적으로 DB와 통신을 하기 위해 async / await를 적극적으로 이용했다. 이때 DB와 통신 과정에서 에러가 일어났을 때 에러 핸들링을 하는 과정이 필요하다. await가 붙은 비동기 함수의 에러 핸들링은 try...catch블락으로 해당 코드 부분을 묶는 것이 전통적이다. 그러나 모든 async function에 대해 try...catch 블락을 구성하는 것은 비효율적이고 가독성 면에서 좋지도 않다.
그래서 async function 자체를 인자로 받아서 .then().catch()를 직접 구성하는 함수를 하나 짤 것이다. utils/catchAsync.js 파일에 아래 함수를 구성했다.
// utils/catchAsync.js
module.exports = (func) => {
return (req, res, next) => {
func(req, res, next).catch((e) => next(e));
};
};
func라는 함수를 인자로 받는 함수이고, func(req, res, next) 기능을 가지는 함수를 return하는 함수이다. 말로 설명하니 복잡하군
func은 async middleware function이다. 암시적으로 promise를 반환하며, DB서버와 통신을 하는 기능을 가져서 비동기 함수를 위한 에러 핸들링이 필요하다. 미들웨어이므로 func(req,res,next)로 동작하며, 말했듯 promise를 반환하므로 then().catch() 사용이 가능하다. 에러가 생기면 catch() 인자의 함수에 err 객체를 인자로 전달한다. 앞서 next에 어떤 인자를 주면 error handler가 작동된다고 설명한 바 있다. 에러가 생겨 .catch가 실행되면 next(e)로 인해 app.use((err,req,res,next)=>{})의 핸들러가 실행된다.
그러니까, 다시 한 번 정리하면 위 함수는 인자로 특정 async function을 주었을 때 그 특정 function 기능을 하면서 .catch()로 에러 핸들링이 되는 함수를 반환하는 것이다..... 최대한 말로 풀어서 설명해봤는데 나중의 내가 이걸 보고 이해할 수 있을런지 ㅜ
실제로는 아래와 같이 쓰인다. 앞선 포스트에서 설명을 생략한 catchAsync()부분이다.
// routes/user.js
const catchAsync = require("../utils/catchAsync");
router.post(
"/register",
userController.validateUser,
catchAsync(userController.register)
);
인자로 userController.register라는 미들웨어 함수를 주었다. register은 유저가 post로 보낸 회원가입 정보를 DB에 넣는 과정이 필요하다. 이 작업이 비동기적으로 이뤄지므로 register은 async function이다. catchAsync으로 묶어 register기능을 똑같이 하면서도 에러 e가 발생했을 때 catch하여 next(e)를 호출하는 기능을 추가한 것이다. 앞으로 모든 async function은 catchAsync으로 묶을 것이다.
다음엔 식단관리 앱의 꽃! 인 음식, Food와 관련한 CRUD 구현 과정을 포스트하겠다.