Node.js 인터뷰 질문 25

질문: Node.js에서 미들웨어 개념은 무엇이고 어떻게 작동하나요?

답변:

미들웨어는 Node.js 웹 애플리케이션, 특히 Express와 같은 프레임워크에서 핵심적인 개념입니다. 미들웨어는 요청과 응답 사이에 위치하여 요청 처리 과정에 추가적인 기능을 제공하는 함수입니다. 이러한 함수들은 요청 객체(req), 응답 객체(res), 그리고 다음 미들웨어 함수를 호출하는 next 함수에 접근할 수 있습니다.

미들웨어의 기본 개념

미들웨어 함수는 다음과 같은 세 가지 매개변수를 가집니다:

function myMiddleware(req, res, next) {
  // 요청(req)에 대한 작업 수행

  // 선택적으로 응답(res)에 대한 작업 수행

  // 다음 미들웨어로 제어 전달
  next();

  // 또는 응답을 직접 종료
  // res.end();
}

미들웨어는 다음과 같은 작업을 수행할 수 있습니다:

  1. 요청 객체(req) 또는 응답 객체(res)를 수정
  2. 요청-응답 주기 종료
  3. 다음 미들웨어 함수 호출
  4. 오류 처리

Express에서의 미들웨어

Express는 미들웨어를 기반으로 작동하는 웹 프레임워크입니다. Express에서 미들웨어는 다음과 같이 사용됩니다:

const express = require("express");
const app = express();

// 애플리케이션 레벨 미들웨어
app.use((req, res, next) => {
  console.log("시간:", Date.now());
  next();
});

// 특정 경로에 미들웨어 적용
app.use("/user/:id", (req, res, next) => {
  console.log("요청 유형:", req.method);
  next();
});

// 라우트 핸들러도 미들웨어
app.get("/user/:id", (req, res, next) => {
  res.send("사용자 정보");
});

app.listen(3000);

미들웨어의 유형

1. 애플리케이션 레벨 미들웨어

app.use() 또는 app.METHOD()(예: app.get, app.post 등)를 사용하여 모든 요청 또는 특정 경로의 요청에 적용됩니다.

// 모든 요청에 적용되는 미들웨어
app.use((req, res, next) => {
  console.log("미들웨어가 모든 요청을 처리합니다");
  next();
});

// /user 경로의 요청에만 적용되는 미들웨어
app.use("/user", (req, res, next) => {
  console.log("사용자 경로 요청을 처리합니다");
  next();
});

2. 라우터 레벨 미들웨어

라우터 레벨 미들웨어는 애플리케이션 레벨 미들웨어와 동일하게 작동하지만, express.Router() 인스턴스에 바인딩됩니다.

const express = require("express");
const router = express.Router();

// 라우터 레벨 미들웨어
router.use((req, res, next) => {
  console.log("라우터 미들웨어가 실행됩니다");
  next();
});

router.get("/user/:id", (req, res, next) => {
  res.send("사용자 정보");
});

app.use("/api", router); // '/api' 경로에 라우터를 마운트

3. 오류 처리 미들웨어

오류 처리 미들웨어는 네 개의 인자(err, req, res, next)를 갖는 특별한 미들웨어 함수입니다.

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send("무언가 잘못되었습니다!");
});

// 일반 미들웨어에서 오류를 다음 미들웨어로 전달
app.use((req, res, next) => {
  try {
    // 에러가 발생할 수 있는 코드
    if (somethingWrong) {
      throw new Error("에러 발생!");
    }
    next();
  } catch (err) {
    next(err); // 오류 처리 미들웨어로 에러 전달
  }
});

4. 내장 미들웨어

Express는 몇 가지 내장 미들웨어를 제공합니다:

// JSON 파싱을 위한 미들웨어
app.use(express.json());

// URL 인코딩된 데이터 파싱을 위한 미들웨어
app.use(express.urlencoded({ extended: true }));

// 정적 파일 제공을 위한 미들웨어
app.use(express.static("public"));

5. 서드파티 미들웨어

다양한 기능을 제공하는 많은 서드파티 미들웨어가 있습니다:

const morgan = require("morgan"); // HTTP 요청 로깅
const helmet = require("helmet"); // HTTP 헤더 보안 강화
const cors = require("cors"); // CORS 지원

app.use(morgan("dev"));
app.use(helmet());
app.use(cors());

미들웨어 실행 순서

미들웨어는 정의된 순서대로 실행됩니다. 순서가 중요하므로 신중하게 구성해야 합니다.

// 항상 실행되는 미들웨어
app.use((req, res, next) => {
  console.log("항상 실행");
  next();
});

// '/user' 경로에만 실행되는 미들웨어
app.use("/user", (req, res, next) => {
  console.log("사용자 경로일 때만 실행");
  next();
});

// 특정 HTTP 메서드와 경로에서만 실행되는 미들웨어
app.get("/user", (req, res, next) => {
  console.log("GET /user 요청일 때만 실행");
  next();
});

// 응답 보내기
app.get("/user", (req, res) => {
  res.send("사용자 정보");
});

// 이 미들웨어는 이전 핸들러가 응답을 종료했기 때문에 실행되지 않음
app.use((req, res, next) => {
  console.log("실행되지 않음");
  next();
});

미들웨어 체인 및 제어 흐름

next() 함수는 미들웨어 체인에서 다음 미들웨어로 제어를 전달합니다. next()를 호출하지 않으면 요청이 멈추고 다음 미들웨어는 실행되지 않습니다.

app.use((req, res, next) => {
  console.log("미들웨어 1");
  next(); // 다음 미들웨어로 이동
});

app.use((req, res, next) => {
  console.log("미들웨어 2");
  // next()를 호출하지 않으면 여기서 멈춤
  res.send("응답 종료");
});

app.use((req, res, next) => {
  console.log("이 미들웨어는 실행되지 않음");
  next();
});

next()에 인자를 전달하면 오류 처리 미들웨어로 제어가 이동합니다:

app.use((req, res, next) => {
  console.log("미들웨어 1");
  next("에러 발생"); // 오류 처리 미들웨어로 이동
});

app.use((req, res, next) => {
  console.log("이 미들웨어는 실행되지 않음");
  next();
});

// 오류 처리 미들웨어
app.use((err, req, res, next) => {
  console.error("오류:", err);
  res.status(500).send("서버 오류");
});

미들웨어 작성 방법

커스텀 미들웨어를 작성하는 방법을 살펴보겠습니다:

1. 간단한 로깅 미들웨어

function logger(req, res, next) {
  console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
  next();
}

app.use(logger);

2. 인증 미들웨어

function authenticate(req, res, next) {
  const token = req.header("Authorization");

  if (!token) {
    return res.status(401).json({ message: "인증 토큰이 없습니다" });
  }

  try {
    const decoded = verifyToken(token);
    req.user = decoded; // 요청 객체에 사용자 정보 추가
    next();
  } catch (error) {
    res.status(401).json({ message: "유효하지 않은 토큰입니다" });
  }
}

// 특정 라우트에 인증 미들웨어 적용
app.get("/protected", authenticate, (req, res) => {
  res.send(`안녕하세요, ${req.user.name}님!`);
});

3. 속도 제한 미들웨어

function rateLimit(limit, time) {
  const requests = {};

  return (req, res, next) => {
    const ip = req.ip;

    // 현재 IP의 요청 기록 확인
    requests[ip] = requests[ip] || { count: 0, resetTime: Date.now() + time };

    // 시간 초과 확인 및 초기화
    if (Date.now() > requests[ip].resetTime) {
      requests[ip] = { count: 0, resetTime: Date.now() + time };
    }

    // 요청 수 증가
    requests[ip].count++;

    // 제한 초과 확인
    if (requests[ip].count > limit) {
      return res.status(429).json({ message: "너무 많은 요청" });
    }

    next();
  };
}

// 1분당 100개 요청으로 제한
app.use(rateLimit(100, 60 * 1000));

미들웨어의 실제 활용 사례

1. API 응답 형식 통일화

function standardizeResponse(req, res, next) {
  // res.send, res.json 메서드를 오버라이드
  const originalJson = res.json;
  const originalSend = res.send;

  res.json = function (data) {
    return originalJson.call(this, {
      success: true,
      data,
      timestamp: new Date(),
    });
  };

  res.error = function (statusCode, message, details = null) {
    return this.status(statusCode).json({
      success: false,
      error: { message, details },
      timestamp: new Date(),
    });
  };

  next();
}

app.use(standardizeResponse);

// 사용 예
app.get("/users", (req, res) => {
  const users = [{ id: 1, name: "홍길동" }];
  res.json(users); // { success: true, data: [{ id: 1, name: '홍길동' }], timestamp: ... }
});

app.get("/error-demo", (req, res) => {
  res.error(400, "잘못된 요청", {
    field: "email",
    message: "유효한 이메일이 필요합니다",
  });
});

2. 데이터 검증 미들웨어

const { validationResult, check } = require("express-validator");

// 검증 규칙 정의
const validateUser = [
  check("name")
    .trim()
    .isLength({ min: 3 })
    .withMessage("이름은 3글자 이상이어야 합니다"),
  check("email").isEmail().withMessage("유효한 이메일을 입력하세요"),
  check("password")
    .isLength({ min: 6 })
    .withMessage("비밀번호는 6자 이상이어야 합니다"),

  // 검증 결과 처리
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    next();
  },
];

// 라우트에 검증 미들웨어 적용
app.post("/users", validateUser, (req, res) => {
  // 유효성 검사를 통과했으므로 사용자 생성 로직
  res.status(201).json({ message: "사용자가 생성되었습니다" });
});

미들웨어 설계 원칙

효과적인 미들웨어를 설계하기 위한 몇 가지 원칙:

  1. 단일 책임 원칙: 각 미들웨어는 한 가지 작업만 수행해야 합니다.
  2. 재사용성: 미들웨어는 여러 상황에서 재사용할 수 있도록 설계해야 합니다.
  3. 구성 가능성: 미들웨어는 쉽게 구성할 수 있어야 합니다(옵션 매개변수 등).
  4. 에러 처리: 적절한 에러 처리 메커니즘을 포함해야 합니다.
  5. 성능 고려: 미들웨어는 애플리케이션 성능에 영향을 미치므로 효율적으로 설계해야 합니다.

미들웨어 패턴의 장점

  1. 모듈성: 애플리케이션 로직을 작고 재사용 가능한 조각으로 분리
  2. 유연성: 애플리케이션 기능을 쉽게 확장하고 수정 가능
  3. 관심사 분리: 인증, 로깅, 데이터 유효성 검사 등의 관심사를 분리
  4. 코드 재사용: 여러 라우트에서 동일한 미들웨어 재사용
  5. 테스트 용이성: 작은 단위로 분리되어 있어 테스트하기 쉬움

미들웨어는 Node.js 웹 애플리케이션의 핵심적인 아키텍처 패턴으로, 복잡한 웹 서버 기능을 모듈화하고 확장하는 데 강력한 방법을 제공합니다. Express와 같은 프레임워크는 이 패턴을 채택하여 개발자가 유연하고 모듈화된 애플리케이션을 구축할 수 있게 합니다.

results matching ""

    No results matching ""