1. 미들웨어(Middleware)란?
- 정의
- Express에서 미들웨어는 요청(req) → 응답(res) 흐름 중간에 삽입되어 부가적인 로직(로그, 인증, 데이터 파싱 등)을 수행하는 함수이다.
- 미들웨어는 보통 아래와 같은 구조를 가진다.
function middleware(req, res, next) {
// 1) 작업 수행
// 2) 계속해서 다음 단계로 넘어갈지, 응답 종료할지 결정
next(); // 다음 미들웨어 또는 라우터로 이동
}
- 역할
- 로그인 여부를 체크하는 인증/인가 로직
- Request Body 파싱(body-parser)
- 정적 파일 제공(express.static)
- 로그 작성(morgan, winston 등)
- 예외 처리, 에러 핸들링 등
- 특징
- 미들웨어는 라우팅보다 먼저 또는 라우팅 후에도 동작할 수 있음.
- 즉, 요청 처리 파이프라인에 여러 미들웨어가 체인 형태로 연결되어, 하나씩 거쳐가면서 로직을 수행한 뒤 최종적으로 라우터(혹은 그전에 응답 종료)로 이어집니다.
2. 라우터(Router)란?
- 정의
- 라우터는 특정 URL 경로와 HTTP 메서드에 대해, “어떤 로직(핸들러)을 실행할지”를 결정하는 경로 분기 역할을 한다.
- 예를 들어:
app.get('/users', (req, res) => {
res.send('GET /users');
});
app.post('/users', (req, res) => {
res.send('POST /users');
});
app.get, app.post와 그 안의 경로와 어떤 동작을 수행할지 등을 전부 포함하는 것이 라우터라고 보면 된다.
const express = require('express');
const path = require('path');
const app = express();
app.set('port', process.env.PORT || 3000);
app.get('/', (req, res) => {
console.log('모든 요청에 실행하고 싶어요.');
res.sendFile(path.join(__dirname, './index.html'));
});
app.post('/', (req, res) => {
console.log('모든 요청에 실행하고 싶어요.');
res.send('hello express');
});
app.get('/test', (req, res) => {
console.log('모든 요청에 실행하고 싶어요.');
res.send('hello test');
});
app.listen(app.get('port'), () => {
console.log('익스프레스 서버 실행');
});
만약 위와 같이 모든 라우터에 공통된 작업을 하고 싶은 경우가 있다면, 이때 미들웨어를 이용하면 된다.
const express = require('express');
const path = require('path');
const app = express();
app.set('port', process.env.PORT || 3000);
app.use((req, res, next) => {
console.log('모든 요청에 실행하고 싶어요.');
next();
});
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, './index.html'));
});
app.post('/', (req, res) => {
res.send('hello express');
});
app.get('/test', (req, res) => {
res.send('hello test');
});
app.listen(app.get('port'), () => {
console.log('익스프레스 서버 실행');
});
위와 같이 app.use((req, res, next) => { next(); }); 를 상단에 두고 해당 작업이 실행된 후 next() 를 통해 경로가 일치한 라우터를 찾아 작업을 수행한다.
3. 라우터 매개변수
const express = require('express');
const path = require('path');
const app = express();
app.set('port', process.env.PORT || 3000);
app.use((req, res, next) => {
console.log('모든 요청에 실행하고 싶어요.');
next();
});
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, './index.html'));
});
app.post('/', (req, res) => {
res.send('hello express');
});
app.get('/category/node', (req, res) => {
res.send('hello test');
});
app.get('/category/react', (req, res) => {
res.send('hello test');
});
app.get('/category/vue', (req, res) => {
res.send('hello test');
});
app.listen(app.get('port'), () => {
console.log('익스프레스 서버 실행');
});
위와 같이 /category/ 경로에 /category/node, /category/react, /category/vue 등 category라는 공통점이 있는 경우에 이것들을 통합할 수 있다.
app.get('/category/:name', (req, res) => {
res.send(`hello ${req.params.name}`);
});
위와 같이 : (콜론)을 사용한 후 원하는 이름을 붙인다. 그리고 사용하기 위해선 req.params.name으로 호출할 수 있다.
const express = require('express');
const path = require('path');
const app = express();
app.set('port', process.env.PORT || 3000);
app.use((req, res, next) => {
console.log('모든 요청에 실행하고 싶어요.');
next();
});
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, './index.html'));
});
app.post('/', (req, res) => {
res.send('hello express');
});
app.get('/category/:name', (req, res) => {
res.send(`hello category`);
});
app.listen(app.get('port'), () => {
console.log('익스프레스 서버 실행');
});
위와 같이 구성한다면 /category/ 경로에 다양한 값이 들어가도 처리된다. 다만 원하는 몇 가지 리소스 이름이 있다면 그것을 위한 분기처리를 해주는 것이 좋아보인다.
주의할 점
app.get('/category/:name', (req, res) => {
res.send(`hello category`);
});
app.get('/category/node', (req, res) => {
res.send(`hello node`);
});
/category/node 를 아래에 입력하였을 경우 그것보다 /category/:name이 우선적으로 처리되기 때문에 /category/node 의 데이터를 받아올 수 없다.
그러므로 반드시 '라우터 파라미터'와 겹치는 경로가 있다면 우선순위를 생각하여야 한다.
4. 미들웨어 특성과 에러
4.1 미들웨어 경로 지정
app.use('/about', (req, res, next) => {
console.log('모든 요청에 실행하고 싶어요.');
next();
});
미들웨어에도 직접 경로를 넣을 수 있다. 위의 코드는 /about일 경우에만 실행되며, 이후 /about 라우터를 찾아 Response한다.
4.2 미들웨어 추가 사용
app.use((req, res, next) => {
console.log('1. 모든 요청에 실행하고 싶어요.');
next();
}, (req, res, next) => {
console.log('2. 모든 요청에 실행하고 싶어요.');
next();
}, (req, res, next) => {
console.log('3. 모든 요청에 실행하고 싶어요.');
next();
});
미들웨어는 여러 개를 실행하도록 구성할 수 있다. 위의 코드에서는 1 -> next() -> 2 -> next() -> 3 -> next() -> 라우터 순서로 실행된다.
4.3 에러처리 미들웨어
app.use((req, res, next) => {
console.log('모든 요청에 실행하고 싶어요.');
next();
}, (req, res, next) => {
throw new Error('에러 발생!');
});
미들웨어에 에러를 발생시키면 클라이언트에 너무 상세한 에러코드가 나오기 때문에 에러 미들웨어는 코드 가장 하단에 구현토록 한다.
const { error } = require('console');
const express = require('express');
const path = require('path');
const app = express();
app.set('port', process.env.PORT || 3000);
app.use((req, res, next) => {
console.log('모든 요청에 실행하고 싶어요.');
next();
}, (req, res, next) => {
throw new Error('에러 발생!');
});
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, './index.html'));
});
app.post('/', (req, res) => {
res.send('hello express');
});
app.get('/category/:name', (req, res) => {
res.send(`hello category`);
});
app.get('/category/node', (req, res) => {
res.send(`hello node`);
});
app.use((error, req, res, next) => {
console.error(error);
res.send('페이지 에러');
});
app.listen(app.get('port'), () => {
console.log('익스프레스 서버 실행');
});
중요한 점은 매개변수로 (error, req, res, next) 네 개를 반드시 넣어줘야 한다는 점이다.
위와 같이 에러 미들웨어를 설정하면 서버 코드가 노출되지 않고 서버에만 기록된다.
4.3.1 404에러 처리
const { error } = require('console');
const express = require('express');
const path = require('path');
const app = express();
app.set('port', process.env.PORT || 3000);
app.use((req, res, next) => {
console.log('모든 요청에 실행하고 싶어요.');
next();
});
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, './index.html'));
});
app.post('/', (req, res) => {
res.send('hello express');
});
app.get('/category/:name', (req, res) => {
res.send(`hello category`);
});
app.get('/category/node', (req, res) => {
res.send(`hello node`);
});
app.use((error, req, res, next) => {
console.error(error);
res.send('페이지 에러');
});
app.listen(app.get('port'), () => {
console.log('익스프레스 서버 실행');
});
위와 같이 구성된 코드에 없는 API를 실행하면 404 에러코드를 발생시킨다. 이는 기본적으로 express에서 제공하는 기능이다.
그러나 이러한 404 에 대한 부분도 커스텀하게 클라이언트에 Response하고 싶을 수 있을 것이다. 이러한 경우 아래와 같이 처리하면 된다.
const { error } = require('console');
const express = require('express');
const path = require('path');
const app = express();
app.set('port', process.env.PORT || 3000);
app.use((req, res, next) => {
console.log('모든 요청에 실행하고 싶어요.');
next();
});
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, './index.html'));
});
app.post('/', (req, res) => {
res.send('hello express');
});
app.get('/category/:name', (req, res) => {
res.send(`hello category`);
});
app.get('/category/node', (req, res) => {
res.send(`hello node`);
});
app.use((req, res, next) => {
res.send('404 Not found!');
});
app.use((error, req, res, next) => {
console.error(error);
res.send('페이지 에러');
});
app.listen(app.get('port'), () => {
console.log('익스프레스 서버 실행');
});
마지막이 에러 미들웨어이며 그 위에 미들웨어를 구성하면 404에 대한 부분을 커스텀하게 처리할 수 있다.
1. 익스프레스 모듈
2. set() - 속성 구성
3. 미들웨어(use~next) - 공통 처리 로직 구성
4. 라우터(get,post,put,delete)
5. 404 미들웨어 - 라우터에 걸리지 않는 경로를 미들웨어가 처리함
6. Error 미들웨어 - 에러가 발생하였을 경우 처리되는 미들웨어
7. listen() - 서버 실행
404 미들웨어는 라우터가 끝난 후 로직을 작성하면 되는데, 이렇게 하면 라우터가 실행되지 않았을 경우(404) 404 미들웨어가 작동하게 된다.
4.3.2 그런데 왜 HTTP 상태코드가 200인걸까?
app.use((req, res, next) => {
res.status(200).send('404 Not found!');
});
기본적으로 위와 같이 status(200)이 숨겨진 속성으로 있어서 200이 호출되는 것이다.
4.3.3 send 는 라우터당 한번만 호출이 가능하다.
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, './index.html'));
res.send('안녕하세요');
});
Error: Can't set headers after they are sent.
at ServerResponse.setHeader (_http_outgoing.js:xxx:xx)
at ServerResponse.header (/path/to/node_modules/express/lib/response.js:xxx:xx)
at ServerResponse.send (/path/to/node_modules/express/lib/response.js:xxx:xx)
...
라우터를 위와 같이 구성 후 서버를 실행하면 콘솔에 에러가 발생한다.
하나의 라우터에는 하나의 Response를 전송하기 때문에 두 번의 전송이 되지 않는 것이다. (에러가 없다하여도 다음 send나 json 등은 무시된다.)
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, './index.html'));
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('안녕하세요');
});
내부적으로는 http 모듈의 writeHead와 end 메서드를 사용하여 통신을 진행하고 있다. (그러나 express에는 위와 같이 사용하지 말것)
4.4 next() 활용법
4.4.1 next(error)
app.use((req, res, next) => {
console.log('모든 요청에 실행하고 싶어요.');
next();
}, (req, res, next) => {
try {
console.log(abcd);
} catch (error) {
next(error);
}
});
미들웨어에서 try catch를 사용할 수 있는데(비동기 로직이므로), 이때 catch 내에 next(error)를 넣게 되면, '에러처리 미들웨어'에서 처리된다.
4.4.2 next('route')
app.get('/', (req, res, next) => {
res.sendFile(path.join(__dirname, './index.html'));
next('route');
}, (req, res) => {
console.log('실행1');
});
app.get('/', (req, res) => {
res.send('실행2');
});
동일한 경로의 라우터가 존재한다면 위와 같이 next('route')를 해주면 다음 '/' 라우터를 찾아 실행한다.
app.get('/', (req, res, next) => {
res.sendFile(path.join(__dirname, './index.html'));
if(false){
next('route');
}
else {
next();
}
}, (req, res) => {
console.log('실행1');
});
app.get('/', (req, res) => {
res.send('실행2');
});
라우터내에서 분기처리(if)가 가능하기 때문에 위의 코드처럼 라우터에 대한 흐름을 제어할 수 있게 된다.
'Node.js' 카테고리의 다른 글
익스프레스 웹 서버 만들기 - 미들웨어(express.static) (1) | 2025.01.14 |
---|---|
익스프레스 웹 서버 만들기 - 미들웨어(morgan, cookieParser, express.json, express.urlencode) (0) | 2025.01.13 |
익스프레스 웹 서버 만들기 - express로 HTML 서빙하기 (0) | 2025.01.12 |
익스프레스 웹 서버 만들기 - express 서버 사용해보기 (1) | 2025.01.12 |
Node Package Manager (npm) (0) | 2025.01.11 |