위키피디아의 정의를 가져와보자.
미들웨어란 서로 다른 시스템, 애플리케이션이 원활하게 통신할 수 있도록 중간에서 매개 역할을 하는 소프트웨어를 뜻한다.
AWS공식문서에서는 이렇게 설명한다.
서로 다른 애플리케이션이 서로 통신하는데 사용되는 소프트웨어다.
예를들어 클라이언트와 서버는 통신을 한다.
이때 요청과 응답 사이에 중개자 역할을 하는 소프트웨어를 미들웨어라 볼 수 있다.
마지막으로 express문서에서는 미들웨어에 대해 이렇게 설명한다.
애플리케이션의 요청-응답 주기 중 다음 미들웨어 함수에 대한 액세스 권한을 갖는 함수.
request와 response 주기 사이에 다음 미들웨어 함수에 대한 액세스 권한을 갖는다.
즉, request와 response 사이에서 중개자 역할을 하며, 다음 미들웨어를 호출할 수 있는 권한을 갖는다.
그렇다면 미들웨어가 서버측에서 어떤역할을 하는지 간단하게 예시로 보자.
//express 예시
const express = require('express')
const app = express()
const myLogger = function (req, res, next) {
console.log('LOGGED')
next()
}
app.use(myLogger)
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(3000)
myLogger()
함수는 미들웨어다. 즉, 요청과 응답 사이에 호출되는 함수다.
실제로 /
경로로 접속시 먼저 콘솔에 'LOGGED'
문자열이 찍히고, 그다음 'Hello World'
가 화면에 나온다.
간단한 미들웨어를 한 번 구현해보자.
next()
함수로 다음 미들웨어를 호출할 수 있어야 한다.const middlewares = [];
// 미들웨어 실행 함수
const executeMiddlewares = (middlewares, req, res, done) => {
let index = 0;
const next = (err) => {
if (err) {
return done(err);
}
if (index >= middlewares.length) {
return done();
}
const middleware = middlewares[index++];
middleware(req, res, next);
};
next();
};
//...
executeMiddlewares(middlewares, req, res, (err) => {
if (err) {
res.writeHead(500, { "Content-Type": "text/plain" });
res.end("Internal Server Error");
return;
}
// API 실행
api(req, res);
});
핵심은 클로져를 이용해 인덱스를 저장하는 것이다. 미들웨어를 실행중인 컨텍스트 내에서 다음 미들웨어 함수에 대해 액세스 권한을 가져야 하기 때문이다.
또한 에러를 처리하기위해 만약 에러 발생시 콜백함수로 제어권을 넘겨
해당 조건문을 실행하게 한다!
실제로 미들웨어가 적용되는지 테스트해본다.
const logMiddleware = (req, res, next) => {
console.log(`메서드:${req.method}는 링크:${req.url} 접속시 실행됨`);
next();
};
const documentsRouter = router();
documentsRouter.get("/documents", getDocumentListController);
documentsRouter.use(logMiddleware);
미들웨어가 잘 실행되는 모습을 볼 수 있다.
노션클론 리팩토링13편에서 CORS문제를 해결했던 로직을 미들웨어로 바꾸어 보겠다.
const CORS_HEADER = {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
const cors = (req, res, next) => {
for (const key in CORS_HEADER) {
res.setHeader(key, CORS_HEADER[key]);
}
next();
};
module.exports = cors;
이렇게 모듈로 뺴주고
documentsRouter.use(cors);
다른 미들웨어처럼 사용하면 정상 작동하는 모습을 볼 수 있다.
express는 node.js의 http모듈을 추상화하여 보다 더 편리하게 개발할수 있게 도와주는 프레임워크다.
해당 프레임워크 사용시 기본적으로 사용하는 미들웨어가 몇가지 있는데, 개중 하나가 req.body
를 파싱해주는 body-parser
이다.
저번 회차에서 해당문제를 겪어 해결한적이 있다. 따라서 req.body
를 buffer
에서 string
으로 파싱하는 로직을 미들웨어로 꺼내보자.
const bodyParser = async (req, res, next) => {
const method = req.method;
if (method !== "POST" && method !== "PUT") return next();
let body = [];
req.on("data", (chunk) => {
body.push(chunk);
});
await new Promise((resolve, reject) => {
req.on("end", () => {
try {
const buffer = Buffer.concat(body);
req.body = JSON.parse(buffer.toString());
resolve();
} catch (error) {
reject(error);
}
});
});
next();
};
module.exports = bodyParser;
return
을 사용할땐 주의해야한다. 다음 미들웨어를 호출하는 next()
함수를 호출하지 않는다면, 영원히 미들웨어 컨텍스트에 갇히게 된다!
cors
미들웨어와 마찬가지로 use
를 이용하여 미들웨어를 부착한다.
documentsRouter.use(bodyParser);
문제없이 화면이 잘 나온다.
SPA는 실제 주소가 바뀌지만, 해당 경로의 문서가 서버에 처음부터 존재하지 않는다.
클라이언트측 JS를 이용해 화면을 전부 그려내는 터라, 만약 SPA에 대응하는 작업을 하지 않고 특정 경로에서 새로고침시 404 NOT FOUND
를 볼 수 도 있다.
이를 한 번 해결해보자.
현재 웹서버 로직은 대략 이렇다.
const http = require("http");
const fs = require("fs");
const path = require("path");
const PORT = 3000;
const server = http.createServer((req, res) => {
let filePath = path.join(
__dirname,
req.url === "/" ? "/public/index.html" : req.url
);
const extname = path.extname(filePath);
// MIME 타입 설정
const mimeTypes = {
".html": "text/html",
".css": "text/css",
".js": "application/javascript",
};
const contentType = mimeTypes[extname] || "text/plain";
// 파일 읽기
fs.readFile(filePath, (err, data) => {
if (!err) {
//오류가 아닐시, 파일 반환
res.writeHead(200, { "Content-Type": contentType });
res.end(data);
return;
}
if (err.code === "ENOENT") {
// 파일을 찾을 수 없음
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("404 Not Found");
return;
}
// 기타 서버 오류
res.writeHead(500, { "Content-Type": "text/plain" });
res.end("Internal Server Error");
});
});
server.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
/
경로로 요청시 /public/index.html
을 반환함.fs.readFile()
과 경로를 이용해 파일을 가져온다.'ENOENT'
코드(파일을 찾을 수 없음)라면 404를 반환한다.여기서 2-b의 오류로 인하여 새로고침시 404 not found
가 표시되게 된다.
예를들어 http://localhost:3000/documents/1
주소에서 새로고침시, 서버측컴퓨터경로/documents/1
로 파일을 요청하게 된다.
이때 당연히 documents/1
이라는 폴더는 없을 것이므로, 2-b오류를 반환하게 된다.
그렇다면 'ENOENT'
오류시 /public/index.html
을 반환하게 하면 되지 않을까?
if (err.code === "ENOENT") {
// html 요청이면 index.html 반환 (SPA 새로고침 대응)
fs.readFile(
path.join(__dirname, "/public/index.html"),
(error, indexData) => {
if (error) {
res.writeHead(500, { "Content-Type": "text/plain" });
res.end("Internal Server Error");
return;
}
res.writeHead(200, { "Content-Type": "text/html" });
res.end(indexData);
}
);
return;
}
해당 요청의 에러코드가 ENOENT
라면 /public/index.html
을 반환한다.
새로고침을 해도 문서가 잘 유지되는 모습을 볼 수 있다.
영현님 2024년 마지막 날까지 열심히 하시는군요..! 항상 열심히 하시는 모습 덕분에 저도 많이 자극 받을 수 있네요ㅎㅎ
2024년 한 해 수고하셨어요 :) 내년에 영현님께 꼭 좋은 일이 있기를 기원할게요!!🙏
2025년도 화이팅하시구 새해 복 많이 받으세요~!