Express 기본 사용법
Node.js로 웹 서버를 만들어야 한다면 가장 먼저 떠오르는 이름이 바로 Express일 겁니다. 2010년에 처음 등장한 이후로 지금까지 Node.js 생태계에서 압도적인 점유율을 자랑하고 있는 웹 프레임워크인데요. Express는 “최소한의 기능만 제공하되, 필요한 것은 직접 골라 쓰자”라는 철학을 바탕으로 설계되어서, 가벼우면서도 유연한 서버를 구축할 수 있습니다.
이번 포스팅에서는 Express를 처음 접하는 분들을 위해 프로젝트 셋업부터 라우팅, 미들웨어, 요청/응답 처리까지 핵심적인 사용법을 차근차근 알아보겠습니다.
Express 프로젝트 시작하기
먼저 새 디렉토리를 만들고 npm으로 프로젝트를 초기화합니다.
$ mkdir our-express && cd our-express
$ npm init -y
그 다음 Express 패키지를 설치합니다.
$ npm install express
npm 사용법이 익숙하지 않으신 분은 npm install 완벽 가이드를 참고하세요.
이제 index.js 파일을 만들어서 가장 기본적인 Express 서버를 작성해보겠습니다.
const express = require("express");
const app = express();
const port = 3000;
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.listen(port, () => {
console.log(`서버가 http://localhost:${port} 에서 실행 중입니다`);
});
express()를 호출하면 Express 애플리케이션 객체가 생성됩니다.
이 객체의 get() 메서드로 GET 요청에 대한 처리를 등록하고, listen() 메서드로 지정한 포트에서 요청을 받기 시작합니다.
node 명령어로 서버를 실행해볼까요?
$ node index.js
서버가 http://localhost:3000 에서 실행 중입니다
다른 터미널을 열고 curl로 요청을 보내보면 Hello World!가 응답됩니다.
$ curl http://localhost:3000
Hello World!
터미널에서 HTTP 요청을 보내는 curl 커맨드에 대해서는 관련 포스팅을 참고하세요.
ES 모듈 사용
위 예제에서는 require()를 사용하는 CommonJS 모듈 방식으로 Express를 불러왔는데요.
package.json에 "type": "module"을 추가하면 import 문법도 사용할 수 있습니다.
{
"type": "module"
}
import express from "express";
const app = express();
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.listen(3000, () => {
console.log("서버가 http://localhost:3000 에서 실행 중입니다");
});
CommonJS와 ES 모듈의 차이점에 대해서는 CommonJS 모듈 가이드와 Node.js에서 ES 모듈 사용하기를 참고하세요.
앞으로의 예제에서는 좀 더 현대적인 ES 모듈 방식을 사용하겠습니다.
기본 라우팅
라우팅(Routing)은 클라이언트의 요청 URL과 HTTP 메서드에 따라 어떤 코드를 실행할지 결정하는 것인데요.
Express에서는 app.get(), app.post(), app.put(), app.delete() 같은 메서드로 라우트를 등록합니다.
app.get("/users", (req, res) => {
res.send("사용자 목록");
});
app.post("/users", (req, res) => {
res.send("사용자 생성");
});
app.put("/users/:id", (req, res) => {
res.send(`사용자 ${req.params.id} 수정`);
});
app.delete("/users/:id", (req, res) => {
res.send(`사용자 ${req.params.id} 삭제`);
});
각 메서드의 첫 번째 인자는 URL 경로이고, 두 번째 인자는 해당 경로로 요청이 들어왔을 때 실행되는 콜백 함수입니다.
이 콜백 함수는 요청 객체(req)와 응답 객체(res)를 인자로 받습니다.
터미널에서 여러 HTTP 메서드로 테스트해볼까요?
$ curl http://localhost:3000/users
사용자 목록
$ curl -X POST http://localhost:3000/users
사용자 생성
$ curl -X PUT http://localhost:3000/users/42
사용자 42 수정
$ curl -X DELETE http://localhost:3000/users/42
사용자 42 삭제
경로 매개변수
URL에서 동적인 값을 받아야 할 때가 많죠?
Express에서는 경로에 :변수명 형태로 매개변수를 정의하면 req.params 객체를 통해 접근할 수 있습니다.
app.get("/users/:id", (req, res) => {
const userId = req.params.id;
res.json({ id: userId, name: `사용자 ${userId}` });
});
$ curl http://localhost:3000/users/7
{"id":"7","name":"사용자 7"}
여러 개의 매개변수를 동시에 사용하는 것도 가능합니다.
app.get("/users/:userId/posts/:postId", (req, res) => {
res.json(req.params);
});
$ curl http://localhost:3000/users/3/posts/99
{"userId":"3","postId":"99"}
쿼리 스트링
URL의 물음표(?) 뒤에 오는 쿼리 스트링은 req.query 객체로 접근합니다.
페이지네이션이나 검색 필터 같은 기능을 구현할 때 자주 사용하게 됩니다.
app.get("/search", (req, res) => {
const { keyword, page, limit } = req.query;
res.json({ keyword, page, limit });
});
$ curl "http://localhost:3000/search?keyword=express&page=1&limit=10"
{"keyword":"express","page":"1","limit":"10"}
쿼리 스트링으로 넘어오는 값은 항상 문자열 타입이라는 점에 주의해야 합니다.
숫자로 사용해야 한다면 parseInt()나 Number()로 변환이 필요하겠죠?
요청 바디 처리
POST나 PUT 요청에서 클라이언트가 보내는 데이터는 요청 바디(body)에 담겨 옵니다. Express에서 요청 바디를 읽으려면 내장 미들웨어를 먼저 등록해줘야 하는데요.
// JSON 형식의 요청 바디를 파싱
app.use(express.json());
// URL-encoded 형식의 요청 바디를 파싱
app.use(express.urlencoded({ extended: true }));
express.json()은 Content-Type: application/json 헤더가 설정된 요청의 바디를 자동으로 파싱해서 req.body에 넣어줍니다.
express.urlencoded()는 HTML 폼에서 전송되는 application/x-www-form-urlencoded 형식을 처리합니다.
이제 JSON 데이터를 받아서 처리하는 API를 만들어보겠습니다.
app.post("/users", (req, res) => {
const { name, email } = req.body;
res.status(201).json({
message: "사용자가 생성되었습니다",
user: { name, email },
});
});
$ curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name": "Dale", "email": "dale@test.com"}'
{"message":"사용자가 생성되었습니다","user":{"name":"Dale","email":"dale@test.com"}}
HTTP 상태 코드를 설정할 때는
res.status()를 호출한 뒤json()이나send()를 체이닝합니다. 각 상태 코드의 의미에 대해서는 HTTP 상태 코드 안내서를 참고하세요.
미들웨어
미들웨어(Middleware)는 Express의 핵심 개념 중 하나입니다.
요청이 들어오면 응답이 나가기 전까지 거치는 함수들의 파이프라인이라고 생각하면 됩니다.
미들웨어 함수는 req, res, 그리고 next 세 개의 인자를 받는데요.
const logger = (req, res, next) => {
console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
next();
};
여기서 next()를 호출해야 다음 미들웨어나 라우트 핸들러로 넘어갑니다.
next()를 호출하지 않으면 요청이 거기서 멈춰버리니까 주의하세요.
미들웨어를 적용하는 방법은 크게 두 가지인데요.
우선 app.use()로 모든 라우트에 일괄 적용할 수 있습니다.
app.use(logger);
아니면 특정 라우트에만 적용할 수도 있고요.
app.get("/users", logger, (req, res) => {
res.send("사용자 목록");
});
미들웨어가 실행되는 순서는 코드에 등록한 순서를 따릅니다.
그래서 express.json() 같은 바디 파싱 미들웨어는 라우트 정의보다 위에 등록해야 합니다.
라우터로 코드 분리하기
애플리케이션이 커지면 모든 라우트를 하나의 파일에 작성하기 어려워집니다.
Express의 Router를 사용하면 관련 있는 라우트를 모듈 단위로 깔끔하게 분리할 수 있습니다.
import { Router } from "express";
const router = Router();
router.get("/", (req, res) => {
res.json([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
]);
});
router.get("/:id", (req, res) => {
res.json({ id: req.params.id, name: "Alice" });
});
router.post("/", (req, res) => {
res.status(201).json(req.body);
});
export default router;
import express from "express";
import usersRouter from "./routes/users.js";
const app = express();
app.use(express.json());
app.use("/users", usersRouter);
app.listen(3000, () => {
console.log("서버가 http://localhost:3000 에서 실행 중입니다");
});
app.use("/users", usersRouter)를 호출하면 usersRouter에 정의된 모든 라우트 앞에 /users가 자동으로 붙습니다.
그래서 라우터 안에서는 "/", "/:id" 같은 상대 경로만 써주면 됩니다.
이 패턴으로 사용자, 상품, 주문 등 기능별로 라우터 파일을 분리하면 코드를 훨씬 체계적으로 관리할 수 있습니다.
응답 메서드
Express의 응답 객체(res)에는 여러 가지 메서드가 있는데요.
res.send()는 문자열이나 버퍼, 객체 등 다양한 타입의 데이터를 보낼 수 있는 범용 메서드이고, res.json()은 자바스크립트 객체를 JSON 문자열로 변환해서 Content-Type: application/json 헤더와 함께 보내줍니다.
REST API를 만들 때는 주로 res.json()을 쓰게 됩니다.
// 단순 텍스트 응답
app.get("/text", (req, res) => {
res.send("안녕하세요!");
});
// JSON 응답
app.get("/json", (req, res) => {
res.json({ message: "안녕하세요!" });
});
// 상태 코드와 함께 응답
app.get("/not-found", (req, res) => {
res.status(404).json({ error: "리소스를 찾을 수 없습니다" });
});
// 리디렉트
app.get("/old-page", (req, res) => {
res.redirect("/new-page");
});
정적 파일 제공
CSS, 이미지, 자바스크립트 파일 같은 정적 파일을 제공하려면 express.static() 미들웨어를 사용합니다.
app.use(express.static("public"));
이렇게 하면 public 디렉토리 안에 있는 파일들을 URL로 바로 접근할 수 있습니다.
예를 들어, public/style.css 파일은 http://localhost:3000/style.css로 접근 가능합니다.
URL 접두사를 붙이고 싶다면 첫 번째 인자로 경로를 지정하면 됩니다.
app.use("/static", express.static("public"));
그러면 http://localhost:3000/static/style.css로 접근할 수 있게 됩니다.
에러 처리
Express에서 에러를 처리하는 미들웨어는 인자를 4개 받는다는 점이 일반 미들웨어와 다릅니다.
첫 번째 인자가 에러 객체(err)이고, 나머지는 일반 미들웨어와 동일합니다.
app.get("/error", (req, res) => {
throw new Error("의도적인 에러입니다");
});
// 에러 처리 미들웨어 (반드시 라우트 정의 아래에 위치해야 함)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: "서버 내부 오류가 발생했습니다" });
});
에러 처리 미들웨어는 반드시 라우트 정의 아래에 위치해야 합니다.
Express는 동기 코드에서 발생하는 에러는 자동으로 에러 처리 미들웨어로 전달해주지만, 비동기 코드에서 발생하는 에러는 반드시 next()에 에러를 넘겨줘야 합니다.
app.get("/async-error", async (req, res, next) => {
try {
const result = await someAsyncOperation();
res.json(result);
} catch (err) {
next(err);
}
});
Express 5부터는 async 함수에서 발생하는 에러도 자동으로 처리해주기 때문에 try/catch 없이도 동작합니다.
환경 변수 활용
실제로 서버를 운영하다 보면 포트 번호나 데이터베이스 연결 정보 같은 설정값을 환경 변수로 관리해야 할 때가 옵니다.
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`서버가 http://localhost:${port} 에서 실행 중입니다`);
});
process.env.PORT에 값이 설정되어 있으면 그 값을 사용하고, 없으면 기본값으로 3000번 포트를 사용합니다.
환경 변수를
.env파일로 관리하는 방법에 대해서는 dotenv 사용법을 참고하세요.
전체 예제 코드
지금까지 배운 내용을 모아서 간단한 사용자 관리 API를 만들어보겠습니다.
import express from "express";
const app = express();
const port = process.env.PORT || 3000;
// 미들웨어 등록
app.use(express.json());
// 간단한 로깅 미들웨어
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// 인메모리 데이터 저장소
let users = [
{ id: 1, name: "Alice", email: "alice@test.com" },
{ id: 2, name: "Bob", email: "bob@test.com" },
];
let nextId = 3;
// 사용자 목록 조회
app.get("/users", (req, res) => {
res.json(users);
});
// 사용자 상세 조회
app.get("/users/:id", (req, res) => {
const user = users.find((u) => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: "사용자를 찾을 수 없습니다" });
}
res.json(user);
});
// 사용자 생성
app.post("/users", (req, res) => {
const { name, email } = req.body;
const user = { id: nextId++, name, email };
users.push(user);
res.status(201).json(user);
});
// 사용자 수정
app.put("/users/:id", (req, res) => {
const user = users.find((u) => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: "사용자를 찾을 수 없습니다" });
}
const { name, email } = req.body;
if (name) user.name = name;
if (email) user.email = email;
res.json(user);
});
// 사용자 삭제
app.delete("/users/:id", (req, res) => {
const index = users.findIndex((u) => u.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: "사용자를 찾을 수 없습니다" });
}
users.splice(index, 1);
res.status(204).send();
});
// 에러 처리 미들웨어
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: "서버 내부 오류가 발생했습니다" });
});
app.listen(port, () => {
console.log(`서버가 http://localhost:${port} 에서 실행 중입니다`);
});
서버를 띄우고 실제로 요청을 보내볼까요?
$ curl http://localhost:3000/users
[{"id":1,"name":"Alice","email":"alice@test.com"},{"id":2,"name":"Bob","email":"bob@test.com"}]
$ curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name": "Charlie", "email": "charlie@test.com"}'
{"id":3,"name":"Charlie","email":"charlie@test.com"}
$ curl -X PUT http://localhost:3000/users/1 \
-H "Content-Type: application/json" \
-d '{"name": "Alice Kim"}'
{"id":1,"name":"Alice Kim","email":"alice@test.com"}
$ curl -X DELETE http://localhost:3000/users/2 -i
HTTP/1.1 204 No Content
마치며
지금까지 Express의 기본적인 사용법을 훑어보았습니다. 프로젝트 셋업, 라우팅, 미들웨어, 요청/응답 처리, 정적 파일 제공, 에러 핸들링까지 Express로 서버를 만들 때 꼭 알아야 하는 것들을 다뤘는데요.
Express는 정말 많은 기능을 제공하지만, 핵심은 결국 “요청이 들어오면 미들웨어를 거쳐 적절한 응답을 보낸다”는 흐름입니다. 이 흐름을 잘 이해하고 계시면 Express 기반의 서버 애플리케이션을 무리 없이 개발하실 수 있을 겁니다.
Express로 서버를 만든 다음에는 인증 기능을 붙여야 할 때가 많은데요. JWT 토큰을 활용하는 방법은 자바스크립트로 JWT 토큰 발급하고 검증하기를 참고하시고, Passport.js를 이용한 인증 구현은 Passport.js로 Bearer 토큰 기반 API 인증 구현하기를 참고하세요. 그리고 CORS 문제로 고생하시고 계시다면 CORS 완벽 가이드도 도움이 될 겁니다.
좀 더 구조화된 프레임워크를 원하신다면 NestJS도 살펴보시길 추천드립니다. Express 위에서 동작하면서 모듈, 컨트롤러, 서비스 같은 체계적인 아키텍처를 제공합니다.
Express의 공식 문서는 expressjs.com에서 확인하실 수 있습니다.
This work is licensed under
CC BY 4.0