Express.js 완벽 마스터하기: 초보자도 쉽게 배우는 미들웨어, next 메커니즘, 라우팅의 기초부터 활용까지
안녕하세요!
이번 글에서는 Node.js에서 가장 인기 있는 웹 프레임워크인 Express.js에 대해 자세히 알아볼 건데요. Express.js는 간단하면서도 강력한 기능 덕분에 전 세계 개발자들에게 사랑받고 있습니다.
프레임워크라는 말이 조금 어렵게 느껴질 수도 있는데요.
쉽게 말하면, 미리 정해진 구조와 규칙대로 우리가 원하는 기능을 빠르게 만들어낼 수 있도록 도와주는 도구입니다.
프레임워크를 쓰면 똑같은 기능을 매번 새롭게 개발하지 않고, 미리 만들어진 코드를 활용해서 더 쉽고 효율적으로 개발할 수 있거든요.
다들 아시겠지만 Express 프레임워크는 크게 다음과 같은 좋은 기능들을 제공합니다.
- 미들웨어(middleware) 기능: 서버로 들어오는 모든 요청과 응답을 중간에서 처리하고 관리할 수 있습니다.
- 라우트(route) 기능: 사용자의 요청 URL과 HTTP 메서드(GET, POST 등)에 따라 서로 다른 로직을 실행할 수 있습니다.
- 템플릿 렌더링 기능: HTML 페이지를 동적으로 만들어 사용자에게 보여줄 수 있습니다.
이 세 가지 기능 덕분에 Express는 웹 개발에서 가장 필수적인 프레임워크 중 하나로 자리잡았다고 해도 과언이 아닌데요.
오늘은 Express의 원리를 이해하시는 데 도움이 될만한 접근법으로 Express를 공부해 보겠습니다.
Express의 기본 작동 원리를 코드로 쉽게 살펴보기
Express를 처음 접하는 초보자라면, 공식 홈페이지에서 제공하는 간단한 예제를 통해 기본 원리를 먼저 이해하는 게 좋은데요.
아래는 Express 공식 홈페이지에 나오는 가장 간단한 "Hello World" 예제입니다.
Express Hello World 예제 코드
// express 모듈 불러오기
const express = require('express');
// express 애플리케이션 생성하기
const app = express();
// 사용할 포트 번호 설정
const port = 3000;
// 루트 경로('/')로 GET 요청이 들어오면 'Hello World!'라고 응답하기
app.get('/', (req, res) => {
res.send('Hello World!');
});
// 설정한 포트에서 서버 시작하기
app.listen(port, () => {
console.log(`서버가 http://localhost:${port} 에서 실행 중입니다.`);
});
이 코드를 한 줄씩 살펴보면 다음과 같은데요.
- 먼저 Express 프레임워크를 불러와서
app
이라는 서버 애플리케이션을 생성합니다. - 그런 다음, 사용자가 웹 브라우저에서 주소창에
http://localhost:3000/
을 입력하면 서버가 "Hello World!"라는 텍스트를 화면에 보여줍니다. - 마지막으로
app.listen()
으로 서버를 실행시켜서, 실제로 사용자 요청을 받을 수 있도록 합니다.
다들 여기까지는 알고 계셔서 정말 쉽고 간단한데요.
Express-generator를 이용해서 심도있게 이해해 보겠습니다.
Express-generator란 무엇이고 왜 사용할까요?
Express로 큰 프로젝트를 만들 때는 위처럼 매번 처음부터 모든 코드를 짜는 게 번거로울 수 있습니다.
그래서 Express에서는 미리 기본적인 구조를 만들어주는 편리한 도구를 제공하는데요.
이게 바로 express-generator입니다.
express-generator는 Express 프로젝트를 빠르게 만들어주는 명령어 도구인데요.
이걸 사용하면, 웹 개발을 위한 초기 폴더 구조와 파일들을 자동으로 만들어줍니다.
따라서 처음부터 모든 걸 직접 만들 필요 없이, 빠르게 개발을 시작할 수 있게 됩니다.
express-generator를 설치하려면 터미널에서 다음 명령어를 입력하면 됩니다.
npm install -g express-generator
설치 후 프로젝트를 생성할 때는 다음처럼 간단히 명령어를 입력합니다.
express myapp
위 명령어를 실행하면, 자동으로 기본적인 폴더 구조가 만들어지는데요.
예를 들어, 아래와 같은 폴더와 파일들이 생성됩니다.
myapp
├── app.js
├── bin
│ └── www
├── package.json
├── public
│ ├── images
│ ├── javascripts
│ └── stylesheets
│ └── style.css
├── routes
│ ├── index.js
│ └── users.js
└── views
├── error.jade
├── index.jade
└── layout.jade
여기서 중요한 파일이 바로 app.js
인데요.
이 파일은 Express 애플리케이션의 핵심 코드를 담고 있습니다.
express-generator로 만든 app.js 파일 쉽게 이해하기
생성된 app.js
파일은 Express 서버의 주요 설정을 담고 있는데요.
이해하기 쉽게 코드를 간략히 설명해보면,
// 필요한 모듈 불러오기
const createError = require('http-errors');
const express = require('express');
const path = require('path');
const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');
// express 애플리케이션 생성하기
const app = express();
// 뷰(view) 엔진(템플릿 엔진) 설정하기
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
// 사용자가 보낸 데이터를 처리하는 미들웨어 설정
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// 정적 파일(css, js, 이미지 등)이 위치한 폴더 설정
app.use(express.static(path.join(__dirname, 'public')));
// 라우트(경로별 처리 로직) 설정
app.use('/', indexRouter); // 메인 페이지
app.use('/users', usersRouter); // 사용자 관련 페이지
// 존재하지 않는 페이지(404)에 대한 처리
app.use((req, res, next) => {
next(createError(404));
});
// 에러 처리 로직
app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
res.status(err.status || 500);
res.render('error');
});
// Express 애플리케이션 내보내기
module.exports = app;
위 코드에서 핵심은 다음과 같은데요.
- 앱에서 사용할 템플릿 엔진과 뷰 경로를 지정합니다.
- 데이터(POST 요청 등)를 처리하는 미들웨어를 설정합니다.
- 정적 파일(css, 이미지 등)을 제공하는 폴더를 지정합니다.
- URL 경로별로 어떤 로직을 실행할지 라우트를 등록합니다.
- 잘못된 URL을 처리하고, 오류 발생 시 사용자에게 적절한 화면을 보여줍니다.
이제 express-generator가 무엇이고, app.js 파일이 어떤 역할을 하는지 충분히 이해하셨으리라 생각하는데요.
그럼 이제 본격적으로 시작해볼까요?
Express.js 완벽 마스터하기: 직접 만들면서 배우는 Express 원리 (미들웨어, next 메커니즘, 라우트 처리)
여기서부터는 직접 간단한 Express 클래스(LikeExpress)를 만들면서 Express의 핵심 원리들을 친절하게 파헤쳐 볼 건데요.
Express의 핵심 원리는 크게 세 가지인데요.
- 미들웨어 등록은 어떻게 하는지,
- next 메커니즘은 어떤 원리인지,
- 라우트 처리는 어떻게 이루어지는지
이 세 가지를 하나씩 직접 구현하면서 공부해 보겠습니다.
LikeExpress 클래스 직접 만들면서 배우기
지금부터 우리가 만들 간단한 LikeExpress 클래스는 Express의 가장 중요한 기능만 아주 단순하게 구현한 버전입니다.
클래스를 구현하는 순서는 이렇게 됩니다.
- 클래스의 기본 구조를 잡고,
- HTTP 요청을 처리할 미들웨어 등록 기능을 추가하고,
- 요청과 미들웨어를 이어주는 next 메커니즘을 구현한 후,
- 마지막으로 요청 경로를 구분하고 처리하는 라우트 처리 및 경로 매칭 기능을 만듭니다.
LikeExpress 클래스를 통해 Express의 원리를 이해하면, 실제 Express 프레임워크도 훨씬 자신 있게 다룰 수 있을겁니다.
이제부터 실제로 Express의 핵심 원리를 코드로 직접 구현해볼 건데요.
우리가 만들 클래스의 이름은 바로 LikeExpress입니다.
Express의 핵심 기능만 간단하게 담고 있어서, Express를 처음 배우는 분들도 쉽게 따라올 수 있습니다.
LikeExpress 클래스의 핵심 기능은 다음과 같이 3가지인데요.
- 미들웨어 등록
- 라우트 매칭 처리
- next 메커니즘
이 세 가지를 구현하기 위해서 다음과 같은 4단계를 차근차근 따라가겠습니다.
1단계: 클래스의 기본 구조 잡기
먼저 LikeExpress 클래스가 가질 기본적인 뼈대를 살펴볼 건데요.
이 클래스에서 제공할 메서드(기능)는 다음과 같습니다.
use()
: 일반적인 미들웨어를 등록하는 메서드get()
,post()
: 특정 HTTP 요청 방식(GET, POST)에 따라 미들웨어를 등록하는 메서드listen()
: 실제 HTTP 서버를 만들어서 요청을 처리할 메서드
아래가 클래스의 가장 기본적인 구조 코드입니다.
const http = require('http');
class LikeExpress {
constructor() {} // 클래스 초기화 메서드
use() {} // 일반 미들웨어 등록
get() {} // GET 요청 미들웨어 등록
post() {} // POST 요청 미들웨어 등록
// HTTP 서버에서 사용할 콜백 함수
callback() {
return (req, res) => {
// JSON 형태로 응답을 보내도록 도와주는 간단한 메서드 추가
res.json = function(data) {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify(data));
};
};
}
// 서버를 실행시키는 메서드
listen(...args) {
const server = http.createServer(this.callback());
server.listen(...args);
}
}
module.exports = () => new LikeExpress();
2단계: 미들웨어 등록 기능 구현하기
Express는 요청을 처리할 때 미들웨어라는 함수를 이용해서 순차적으로 처리하는데요.
우리가 만든 LikeExpress 클래스에서도 미들웨어 함수를 배열 형태로 저장할 공간을 만들어야 합니다.
다음과 같이 constructor()
와 미들웨어 관리 메서드를 추가합니다.
constructor() {
// 미들웨어 함수를 저장할 배열
this.routes = {
all: [], // 모든 요청에서 실행할 미들웨어
get: [], // GET 요청에서 실행할 미들웨어
post: [], // POST 요청에서 실행할 미들웨어
};
}
// 미들웨어를 등록하는 내부 메서드
register(path) {
const info = {};
if (typeof path === "string") {
info.path = path;
info.stack = Array.prototype.slice.call(arguments, 1);
} else {
info.path = '/';
info.stack = Array.prototype.slice.call(arguments, 0);
}
return info;
}
// 각각의 미들웨어 등록 메서드 구현
use() {
const info = this.register.apply(this, arguments);
this.routes.all.push(info);
}
get() {
const info = this.register.apply(this, arguments);
this.routes.get.push(info);
}
post() {
const info = this.register.apply(this, arguments);
this.routes.post.push(info);
}
이렇게 하면 미들웨어를 등록할 수 있는 기본 구조가 완성됩니다.
2단계-1: 미들웨어 등록 과정 이해하기
위에서 미들웨어를 등록하는 코드(use()
, get()
, post()
)를 구현한 건데요. 여기서 핵심은:
- 사용자가 등록한 미들웨어를 내부적으로 적절한 배열(
this.routes
)에 저장해서 관리합니다. - 미들웨어를 등록할 때 경로를 지정하지 않으면 기본적으로 '/'(루트 경로)에 적용됩니다.
- 경로가 지정된 경우, 특정 경로로 들어오는 요청에만 적용됩니다.
이렇게 등록된 미들웨어는 나중에 요청이 들어왔을 때 순서대로 실행됩니다.
3단계: 요청 URL과 라우트 매칭 처리하기
미들웨어가 여러 개 등록되었을 때, 실제로 요청이 들어오면 어떤 미들웨어를 실행할지 결정해야 합니다.
이때 사용하는 것이 바로 라우트 매칭 기능인데요. 요청 URL과 등록된 미들웨어의 경로를 비교해서 실행할 미들웨어만 골라냅니다.
다음과 같이 구현합니다.
match(method, url) {
let stack = [];
// favicon 요청 무시하기
if (url === "/favicon") return stack;
// 모든 요청에 대한 미들웨어 + 특정 메서드(GET/POST)의 미들웨어
let routes = [...this.routes.all, ...this.routes[method]];
routes.forEach(route => {
if (url.indexOf(route.path) === 0) {
stack = stack.concat(route.stack);
}
});
return stack;
}
이렇게 하면 요청이 들어왔을 때 어떤 미들웨어를 실행해야 하는지 쉽게 판별할 수 있습니다.
4단계: next 메커니즘 직접 구현하기
Express에서 가장 중요한 개념 중 하나는 바로 next 메커니즘인데요.
이건 미들웨어들이 순차적으로 실행되게 해주는 핵심 장치입니다.
아래 코드로 직접 next 메커니즘을 구현합니다.
handle(req, res, stack) {
const next = () => {
// 차례대로 미들웨어를 하나씩 꺼내서 실행
const middleware = stack.shift();
if (middleware) middleware(req, res, next);
};
next();
}
// callback 함수에서 매칭된 미들웨어 실행하기
callback() {
return (req, res) => {
res.json = function(data) {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify(data));
};
const stack = this.match(req.method.toLowerCase(), req.url);
this.handle(req, res, stack);
};
}
이렇게 하면 등록된 미들웨어들이 순서대로 실행됩니다.
마무리하며
이렇게 간단한 LikeExpress 클래스 구현을 통해 Express 프레임워크의 핵심 개념과 원리를 자연스럽게 이해할 수 있는데요.
이 짧은 코드만으로도 Express가 어떻게 작동하는지 좀 더 깊게 공부하실 수 있을 겁니다.
Express를 더 깊이 있게 공부하고 싶다면, 바로 지금 이 코드를 따라 직접 작성해보는 걸 추천합니다.
실제로 코드로 따라해보면 머릿속에 확실히 개념이 잡히거든요.
'Javascript' 카테고리의 다른 글
TypeScript Bottom 타입 never 완벽 분석 – never의 다양한 응용 한눈에 보기 (0) | 2025.03.19 |
---|---|
TypeScript에서의 Array 타입 표기법: T[] vs. Array<T> 완벽 분석 (0) | 2025.03.19 |
TypeScript 심볼 완벽 분석: 타입 레벨에서의 심볼 활용과 고급 패턴 (0) | 2025.03.15 |
TypeScript 조건부 타입 완벽 가이드: 유니온 타입과 유틸리티 타입 활용의 모든 것 (0) | 2025.03.15 |
TypeScript Mapped Types 완벽 정복: 기본부터 고급 활용까지 (0) | 2025.03.13 |