Node.js 인터뷰 질문 41
질문: Node.js에서 미들웨어 개념에 대해 설명하고, Express.js에서 어떻게 구현되는지 예제와 함께 설명해주세요.
답변:
미들웨어는 Node.js 웹 애플리케이션, 특히 Express.js와 같은 프레임워크에서 핵심적인 개념입니다. 미들웨어는 요청(request)과 응답(response) 사이의 중간 처리 계층으로, 요청이 들어오고 응답이 전송되는 사이에 다양한 기능을 수행할 수 있습니다.
미들웨어의 기본 개념
미들웨어는 다음과 같은 특징을 가집니다:
- 순차적 실행: 미들웨어는 정의된 순서대로 실행됩니다.
- 요청-응답 사이클 제어: 각 미들웨어는 요청 객체(req), 응답 객체(res), 다음 미들웨어 함수(next)에 접근할 수 있습니다.
- 요청 처리 종료 또는 계속: 미들웨어는 요청-응답 사이클을 종료하거나 다음 미들웨어로 제어를 넘길 수 있습니다.
- 요청/응답 수정: 미들웨어는 요청이나 응답 객체를 수정하여 다음 미들웨어에 전달할 수 있습니다.
Express.js에서의 미들웨어
Express.js는 미들웨어 기반 웹 프레임워크로, 미들웨어 함수는 다음 형태를 가집니다:
function middleware(req, res, next) {
// 미들웨어 로직 수행
// 다음 미들웨어로 제어 전달
next();
}
미들웨어 함수는 다음 세 가지 매개변수를 받습니다:
- req: HTTP 요청 객체
- res: HTTP 응답 객체
- next: 다음 미들웨어 함수를 호출하는 콜백 함수
Express.js 미들웨어 유형
Express.js에서는 다양한 유형의 미들웨어를 사용할 수 있습니다:
1. 애플리케이션 레벨 미들웨어
app.use()
또는 app.METHOD()
(GET, POST 등)를 사용하여 애플리케이션 인스턴스에 바인딩됩니다.
const express = require("express");
const app = express();
// 모든 요청에 적용되는 미들웨어
app.use((req, res, next) => {
console.log("요청 시간:", Date.now());
next();
});
// 특정 경로에만 적용되는 미들웨어
app.use("/user", (req, res, next) => {
console.log("사용자 경로 요청");
next();
});
// 특정 HTTP 메소드 및 경로에 적용되는 미들웨어
app.get("/user", (req, res, next) => {
console.log("사용자 GET 요청");
next();
});
app.listen(3000);
2. 라우터 레벨 미들웨어
라우터 레벨 미들웨어는 express.Router()
인스턴스에 바인딩됩니다. 애플리케이션 레벨 미들웨어와 동일한 방식으로 작동하지만, 특정 라우터 인스턴스에 한정됩니다.
const express = require("express");
const app = express();
const router = express.Router();
// 라우터 레벨 미들웨어
router.use((req, res, next) => {
console.log("라우터 미들웨어 실행 시간:", Date.now());
next();
});
// 특정 경로에 대한 라우터 미들웨어
router.get(
"/user/:id",
(req, res, next) => {
console.log("사용자 ID:", req.params.id);
next();
},
(req, res) => {
res.send("사용자 정보");
}
);
// 라우터를 애플리케이션에 마운트
app.use("/api", router);
app.listen(3000);
3. 오류 처리 미들웨어
오류 처리 미들웨어는 일반 미들웨어와 동일하지만, 4개의 인자(err, req, res, next)를 받습니다.
const express = require("express");
const app = express();
app.get("/user/:id", (req, res, next) => {
// 예를 들어, 사용자 ID가 0이면 오류 발생
const userId = parseInt(req.params.id);
if (userId === 0) {
// 오류 생성 및 다음 미들웨어로 전달
const err = new Error("잘못된 사용자 ID");
err.status = 400;
return next(err);
}
// 정상 응답
res.send(`사용자 ID: ${userId}`);
});
// 오류 처리 미들웨어
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).send({
error: {
message: err.message,
status: err.status || 500,
},
});
});
app.listen(3000);
4. 내장 미들웨어
Express 4.x부터는 몇 가지 내장 미들웨어가 제공됩니다:
- express.static: 정적 파일을 제공하는 미들웨어
- express.json: JSON 요청 본문을 파싱하는 미들웨어
- express.urlencoded: URL 인코딩된 요청 본문을 파싱하는 미들웨어
const express = require("express");
const app = express();
const path = require("path");
// 정적 파일 미들웨어
app.use(express.static(path.join(__dirname, "public")));
// JSON 파싱 미들웨어
app.use(express.json());
// URL 인코딩 파싱 미들웨어
app.use(express.urlencoded({ extended: false }));
app.post("/api/data", (req, res) => {
console.log("요청 본문:", req.body);
res.json({ received: req.body });
});
app.listen(3000);
5. 서드파티 미들웨어
널리 사용되는 서드파티 미들웨어의 예시입니다:
const express = require("express");
const morgan = require("morgan"); // 로깅 미들웨어
const cookieParser = require("cookie-parser"); // 쿠키 파싱 미들웨어
const session = require("express-session"); // 세션 관리 미들웨어
const helmet = require("helmet"); // 보안 헤더 미들웨어
const cors = require("cors"); // CORS 미들웨어
const app = express();
// 로깅 미들웨어
app.use(morgan("dev"));
// 보안 헤더 미들웨어
app.use(helmet());
// CORS 미들웨어
app.use(cors());
// 쿠키 파싱 미들웨어
app.use(cookieParser());
// 세션 미들웨어
app.use(
session({
secret: "your-secret-key",
resave: false,
saveUninitialized: true,
cookie: { secure: true },
})
);
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.listen(3000);
미들웨어 사용 사례
미들웨어는 다양한 용도로 사용됩니다:
1. 로깅
요청에 대한 정보를 기록하는 미들웨어:
function loggingMiddleware(req, res, next) {
const start = Date.now();
// res.on('finish') 이벤트는 응답이 완료될 때 발생
res.on("finish", () => {
const duration = Date.now() - start;
console.log(
`${req.method} ${req.originalUrl} - ${res.statusCode} - ${duration}ms`
);
});
next();
}
app.use(loggingMiddleware);
2. 인증 및 권한 부여
사용자 인증 및 권한 검사를 수행하는 미들웨어:
function authMiddleware(req, res, next) {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ message: "인증 토큰이 필요합니다" });
}
try {
// JWT 토큰 검증 예시
const decoded = jwt.verify(token.replace("Bearer ", ""), "your-secret-key");
req.user = decoded; // 인증된 사용자 정보를 요청 객체에 추가
next();
} catch (error) {
return res.status(401).json({ message: "유효하지 않은 토큰입니다" });
}
}
// 특정 라우트에 인증 미들웨어 적용
app.get("/api/protected", authMiddleware, (req, res) => {
res.json({ message: "인증된 사용자용 보호된 데이터", user: req.user });
});
3. 요청/응답 변환
요청이나 응답 데이터를 변환하는 미들웨어:
function dataTransformMiddleware(req, res, next) {
// 원래 send 메소드를 저장
const originalSend = res.send;
// send 메소드를 확장
res.send = function (data) {
// 응답 데이터가 객체인 경우 변환
if (typeof data === "object" && data !== null) {
// 타임스탬프 추가
data.timestamp = new Date().toISOString();
// API 버전 추가
data.apiVersion = "v1";
}
// 원래 send 메소드 호출
return originalSend.call(this, data);
};
next();
}
app.use(dataTransformMiddleware);
app.get("/api/data", (req, res) => {
res.json({ name: "John", age: 30 });
// 응답: { "name": "John", "age": 30, "timestamp": "2023-01-15T12:34:56.789Z", "apiVersion": "v1" }
});
4. 에러 핸들링
애플리케이션 전체에 대한 오류 처리 미들웨어:
// 비동기 오류를 캐치하는 래퍼 함수
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
app.get(
"/async-route",
asyncHandler(async (req, res) => {
// 비동기 작업 (예: 데이터베이스 쿼리)
const data = await someAsyncOperation();
res.json(data);
})
);
// 404 처리 미들웨어 (모든 라우터 다음에 위치해야 함)
app.use((req, res, next) => {
res.status(404).json({ message: "리소스를 찾을 수 없습니다" });
});
// 오류 처리 미들웨어
app.use((err, req, res, next) => {
console.error(err.stack);
// 개발 환경에서는 상세한 오류 정보 제공
if (process.env.NODE_ENV === "development") {
return res.status(err.status || 500).json({
message: err.message,
stack: err.stack,
error: err,
});
}
// 프로덕션 환경에서는 제한된 정보만 제공
res.status(err.status || 500).json({
message: "서버 오류가 발생했습니다",
});
});
미들웨어 체이닝
여러 미들웨어를 순차적으로 실행하는 체이닝은 다음과 같이 구현할 수 있습니다:
const express = require("express");
const app = express();
// 1. 요청 시간 기록 미들웨어
function timeLogger(req, res, next) {
req.requestTime = Date.now();
console.log(`요청 시간: ${new Date(req.requestTime).toISOString()}`);
next();
}
// 2. 사용자 인증 미들웨어
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader) {
req.user = { role: "guest" };
} else {
// 실제로는 토큰 검증 등의 로직이 필요
req.user = { role: "user", id: 123 };
}
console.log(`인증된 사용자 역할: ${req.user.role}`);
next();
}
// 3. 권한 검사 미들웨어 (클로저 사용)
function authorize(role) {
return (req, res, next) => {
if (req.user.role !== role) {
return res.status(403).json({ message: "접근 권한이 없습니다" });
}
next();
};
}
// 미들웨어 체이닝 적용
app.get("/public", timeLogger, (req, res) => {
res.json({ message: "공개 데이터", time: req.requestTime });
});
app.get("/private", timeLogger, authenticate, authorize("user"), (req, res) => {
res.json({ message: "인증된 사용자용 데이터", user: req.user });
});
app.get("/admin", timeLogger, authenticate, authorize("admin"), (req, res) => {
res.json({ message: "관리자용 데이터" });
});
app.listen(3000, () => {
console.log("서버가 포트 3000에서 실행 중입니다");
});
커스텀 미들웨어 설계 원칙
효과적인 미들웨어를 설계할 때 유의할 점:
- 단일 책임 원칙: 각 미들웨어는 하나의 기능만 담당합니다.
- 에러 처리: 미들웨어 내부에서 발생한 오류를 적절히 처리하고 next(err)를 통해 오류 처리 미들웨어로 전달합니다.
- 비동기 코드 처리: 비동기 작업 후에 next()를 호출하여 미들웨어 체인이 계속 진행되도록 합니다.
- 성능 고려: 미들웨어는 요청마다 실행되므로 성능에 미치는 영향을 고려해야 합니다.
- 재사용성: 미들웨어는 다양한 상황에서 재사용할 수 있도록 설계합니다.
미들웨어 실행 흐름 이해하기
미들웨어 간의 실행 흐름을 시각화한 예제:
app.use((req, res, next) => {
console.log("첫 번째 미들웨어 시작");
next();
console.log("첫 번째 미들웨어 종료"); // next() 이후 실행
});
app.use((req, res, next) => {
console.log("두 번째 미들웨어 시작");
next();
console.log("두 번째 미들웨어 종료"); // next() 이후 실행
});
app.get("/test", (req, res) => {
console.log("라우트 핸들러 실행");
res.send("테스트 응답");
});
// 출력 순서:
// 첫 번째 미들웨어 시작
// 두 번째 미들웨어 시작
// 라우트 핸들러 실행
// 두 번째 미들웨어 종료
// 첫 번째 미들웨어 종료
이 예제는 미들웨어가 스택 구조로 실행됨을 보여줍니다. next()
를 호출하면 다음 미들웨어로 제어가 넘어가고, 그 미들웨어의 실행이 완료된 후 이전 미들웨어의 나머지 코드가 실행됩니다.
요약
- 미들웨어는 Node.js, 특히 Express.js에서 HTTP 요청-응답 사이클 중간에 특정 기능을 수행하는 함수입니다.
- 주요 특징:
- 요청(req), 응답(res), 다음 미들웨어 함수(next)에 접근
- 순차적 실행
- 요청/응답 수정 가능
- 요청 사이클 종료 또는 다음 미들웨어로 제어 전달
- 미들웨어 유형:
- 애플리케이션 레벨 미들웨어
- 라우터 레벨 미들웨어
- 오류 처리 미들웨어
- 내장 미들웨어
- 서드파티 미들웨어
- 일반적인 사용 사례:
- 로깅
- 인증 및 권한 부여
- 요청/응답 변환
- 에러 핸들링
- 미들웨어 체이닝을 통해 여러 미들웨어를 순차적으로 실행하여 복잡한 웹 애플리케이션 구축 가능
Express.js의 미들웨어 개념을 이해하는 것은 효율적이고 유지보수가 용이한 Node.js 웹 애플리케이션을 구축하는 데 핵심적입니다.