Node.js 인터뷰 질문 41

질문: Node.js에서 미들웨어 개념에 대해 설명하고, Express.js에서 어떻게 구현되는지 예제와 함께 설명해주세요.

답변:

미들웨어는 Node.js 웹 애플리케이션, 특히 Express.js와 같은 프레임워크에서 핵심적인 개념입니다. 미들웨어는 요청(request)과 응답(response) 사이의 중간 처리 계층으로, 요청이 들어오고 응답이 전송되는 사이에 다양한 기능을 수행할 수 있습니다.

미들웨어의 기본 개념

미들웨어는 다음과 같은 특징을 가집니다:

  1. 순차적 실행: 미들웨어는 정의된 순서대로 실행됩니다.
  2. 요청-응답 사이클 제어: 각 미들웨어는 요청 객체(req), 응답 객체(res), 다음 미들웨어 함수(next)에 접근할 수 있습니다.
  3. 요청 처리 종료 또는 계속: 미들웨어는 요청-응답 사이클을 종료하거나 다음 미들웨어로 제어를 넘길 수 있습니다.
  4. 요청/응답 수정: 미들웨어는 요청이나 응답 객체를 수정하여 다음 미들웨어에 전달할 수 있습니다.

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에서 실행 중입니다");
});

커스텀 미들웨어 설계 원칙

효과적인 미들웨어를 설계할 때 유의할 점:

  1. 단일 책임 원칙: 각 미들웨어는 하나의 기능만 담당합니다.
  2. 에러 처리: 미들웨어 내부에서 발생한 오류를 적절히 처리하고 next(err)를 통해 오류 처리 미들웨어로 전달합니다.
  3. 비동기 코드 처리: 비동기 작업 후에 next()를 호출하여 미들웨어 체인이 계속 진행되도록 합니다.
  4. 성능 고려: 미들웨어는 요청마다 실행되므로 성능에 미치는 영향을 고려해야 합니다.
  5. 재사용성: 미들웨어는 다양한 상황에서 재사용할 수 있도록 설계합니다.

미들웨어 실행 흐름 이해하기

미들웨어 간의 실행 흐름을 시각화한 예제:

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 웹 애플리케이션을 구축하는 데 핵심적입니다.

results matching ""

    No results matching ""