Node.js 인터뷰 질문 39

질문: Node.js의 HTTP 모듈에 대해 설명하고, 간단한 웹 서버를 만드는 방법을 코드와 함께 설명해주세요.

답변:

Node.js의 HTTP 모듈은 HTTP 서버와 클라이언트 기능을 구현하기 위한 핵심 모듈입니다. 이 모듈을 사용하면 HTTP 서버를 생성하고 클라이언트 요청을 처리할 수 있으며, HTTP 클라이언트로서 다른 서버에 요청을 보낼 수도 있습니다.

HTTP 모듈 개요

HTTP 모듈은 Node.js의 핵심 모듈 중 하나로, 외부 패키지 설치 없이 require('http')를 통해 사용할 수 있습니다. 주요 기능은 다음과 같습니다:

  1. HTTP 서버 생성 및 요청 처리
  2. HTTP 클라이언트 기능 제공
  3. 서버와 클라이언트 간의 데이터 스트리밍
  4. 헤더, 상태 코드, 쿠키 등 HTTP 메시지 관리

간단한 HTTP 서버 만들기

다음은 HTTP 모듈을 사용하여 간단한 웹 서버를 만드는 기본 예제입니다:

const http = require("http");

// 서버 생성
const server = http.createServer((req, res) => {
  // 요청 정보 출력
  console.log(`요청 방식: ${req.method}`);
  console.log(`요청 URL: ${req.url}`);
  console.log(`요청 헤더: ${JSON.stringify(req.headers)}`);

  // 응답 헤더 설정
  res.setHeader("Content-Type", "text/html; charset=utf-8");

  // URL에 따른 다른 응답 처리
  if (req.url === "/") {
    res.statusCode = 200;
    res.end("<h1>메인 페이지에 오신 것을 환영합니다!</h1>");
  } else if (req.url === "/about") {
    res.statusCode = 200;
    res.end("<h1>소개 페이지입니다</h1>");
  } else {
    // 404 Not Found 응답
    res.statusCode = 404;
    res.end("<h1>페이지를 찾을 수 없습니다</h1>");
  }
});

// 포트 3000에서 서버 실행
const PORT = 3000;
server.listen(PORT, () => {
  console.log(`서버가 http://localhost:${PORT} 에서 실행 중입니다`);
});

이 코드는 다음과 같은 기능을 수행합니다:

  1. HTTP 모듈을 불러옵니다.
  2. createServer 메소드로 서버를 생성합니다. 이 메소드는 요청 핸들러 함수를 인자로 받습니다.
  3. 요청 핸들러 함수는 요청(req)과 응답(res) 객체를 매개변수로 받습니다.
  4. 서버는 요청 URL에 따라 다른 콘텐츠로 응답합니다.
  5. listen 메소드로 특정 포트에서 서버를 시작합니다.

요청(Request) 객체

req 객체는 클라이언트로부터 받은 HTTP 요청에 대한 정보를 포함합니다. 주요 속성과 메소드는 다음과 같습니다:

  • req.method: 요청 메소드(GET, POST, PUT, DELETE 등)
  • req.url: 요청 URL
  • req.headers: 요청 헤더 객체
  • req.httpVersion: HTTP 버전

요청 본문 데이터를 읽는 예제:

const http = require("http");

const server = http.createServer((req, res) => {
  if (req.method === "POST") {
    let body = "";

    // 데이터를 청크로 받기
    req.on("data", (chunk) => {
      body += chunk.toString();
    });

    // 데이터 수신이 완료되면 처리
    req.on("end", () => {
      console.log("요청 본문:", body);

      try {
        const data = JSON.parse(body);
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(
          JSON.stringify({
            message: "데이터를 성공적으로 받았습니다",
            data: data,
          })
        );
      } catch (error) {
        res.writeHead(400, { "Content-Type": "application/json" });
        res.end(
          JSON.stringify({
            message: "잘못된 JSON 형식입니다",
          })
        );
      }
    });
  } else {
    res.writeHead(405, { "Content-Type": "text/plain" });
    res.end("POST 요청만 허용됩니다");
  }
});

server.listen(3000, () => {
  console.log("서버가 포트 3000에서 실행 중입니다");
});

응답(Response) 객체

res 객체는 클라이언트에게 보낼 HTTP 응답을 구성하는 데 사용됩니다. 주요 메소드는 다음과 같습니다:

  • res.writeHead(statusCode[, statusMessage][, headers]): 응답 헤더 작성
  • res.setHeader(name, value): 개별 헤더 설정
  • res.write(chunk[, encoding][, callback]): 응답 본문 작성
  • res.end([data][, encoding][, callback]): 응답 완료

다양한 형식의 응답을 보내는 예제:

const http = require("http");
const fs = require("fs");
const path = require("path");

const server = http.createServer((req, res) => {
  // HTML 응답
  if (req.url === "/html") {
    res.writeHead(200, { "Content-Type": "text/html" });
    res.end("<html><body><h1>HTML 응답입니다</h1></body></html>");
  }
  // JSON 응답
  else if (req.url === "/json") {
    const data = {
      name: "Node.js",
      version: process.version,
      description: "JavaScript 런타임",
    };

    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(data));
  }
  // 파일 응답 (스트리밍)
  else if (req.url === "/file") {
    const filePath = path.join(__dirname, "example.txt");

    // 파일이 존재하는지 확인
    fs.access(filePath, fs.constants.F_OK, (err) => {
      if (err) {
        res.writeHead(404, { "Content-Type": "text/plain" });
        res.end("파일을 찾을 수 없습니다");
        return;
      }

      // 파일 스트림 생성 및 응답
      const fileStream = fs.createReadStream(filePath);
      res.writeHead(200, { "Content-Type": "text/plain" });

      // 파일 스트림을 응답에 파이프
      fileStream.pipe(res);

      // 오류 처리
      fileStream.on("error", (error) => {
        res.writeHead(500, { "Content-Type": "text/plain" });
        res.end("서버 오류가 발생했습니다");
      });
    });
  }
  // 리다이렉션
  else if (req.url === "/redirect") {
    res.writeHead(302, { Location: "/" });
    res.end();
  }
  // 기본 응답
  else {
    res.writeHead(200, { "Content-Type": "text/plain" });
    res.end("기본 응답입니다");
  }
});

server.listen(3000, () => {
  console.log("서버가 포트 3000에서 실행 중입니다");
});

HTTP 클라이언트 사용하기

HTTP 모듈을 사용하여 다른 서버에 요청을 보내는 방법도 있습니다:

const http = require("http");

// GET 요청 예제
const getOptions = {
  hostname: "example.com",
  port: 80,
  path: "/api/data",
  method: "GET",
  headers: {
    "User-Agent": "Node.js HTTP Client",
  },
};

const getReq = http.request(getOptions, (res) => {
  console.log(`상태 코드: ${res.statusCode}`);

  let data = "";

  // 데이터 수신
  res.on("data", (chunk) => {
    data += chunk;
  });

  // 응답 완료
  res.on("end", () => {
    console.log("응답 데이터:", data);
  });
});

// 오류 처리
getReq.on("error", (error) => {
  console.error("요청 오류:", error.message);
});

// 요청 종료
getReq.end();

// POST 요청 예제
const postData = JSON.stringify({
  name: "홍길동",
  email: "hong@example.com",
});

const postOptions = {
  hostname: "example.com",
  port: 80,
  path: "/api/users",
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Content-Length": Buffer.byteLength(postData),
  },
};

const postReq = http.request(postOptions, (res) => {
  console.log(`상태 코드: ${res.statusCode}`);

  let data = "";

  res.on("data", (chunk) => {
    data += chunk;
  });

  res.on("end", () => {
    console.log("응답 데이터:", data);
  });
});

postReq.on("error", (error) => {
  console.error("요청 오류:", error.message);
});

// 요청 본문 전송
postReq.write(postData);
postReq.end();

간편한 HTTP 요청을 위한 http.get()

간단한 GET 요청을 위해 http.get() 메소드를 사용할 수 있습니다:

const http = require("http");

http
  .get("http://example.com/api/data", (res) => {
    const { statusCode } = res;
    const contentType = res.headers["content-type"];

    let error;
    if (statusCode !== 200) {
      error = new Error(`요청 실패. 상태 코드: ${statusCode}`);
    } else if (!/^application\/json/.test(contentType)) {
      error = new Error(`잘못된 컨텐츠 유형: ${contentType}`);
    }

    if (error) {
      console.error(error.message);
      // 불필요한 데이터 소비
      res.resume();
      return;
    }

    res.setEncoding("utf8");
    let rawData = "";

    res.on("data", (chunk) => {
      rawData += chunk;
    });
    res.on("end", () => {
      try {
        const parsedData = JSON.parse(rawData);
        console.log(parsedData);
      } catch (e) {
        console.error(`데이터 파싱 오류: ${e.message}`);
      }
    });
  })
  .on("error", (e) => {
    console.error(`HTTP 요청 오류: ${e.message}`);
  });

HTTP 서버의 고급 기능

1. 이벤트 처리

HTTP 서버는 다양한 이벤트를 발생시킵니다:

const server = http.createServer();

// request 이벤트
server.on("request", (req, res) => {
  // 요청 처리
});

// connection 이벤트
server.on("connection", (socket) => {
  console.log("새 연결이 생성되었습니다");
});

// close 이벤트
server.on("close", () => {
  console.log("서버가 종료되었습니다");
});

// error 이벤트
server.on("error", (error) => {
  console.error("서버 오류:", error);
});

server.listen(3000);

2. 타임아웃 설정

클라이언트 연결에 타임아웃을 설정할 수 있습니다:

const server = http.createServer((req, res) => {
  res.writeHead(200);
  res.end("Hello World");
});

// 서버 연결 타임아웃 설정 (밀리초)
server.timeout = 5000;

// 또는 개별 요청에 타임아웃 설정
server.on("request", (req, res) => {
  req.setTimeout(5000, () => {
    res.writeHead(408); // Request Timeout
    res.end("요청 시간이 초과되었습니다");
  });
});

server.listen(3000);

실용적인 HTTP 서버 예제: REST API 서버

간단한 사용자 관리 REST API 서버 예제:

const http = require("http");
const url = require("url");

// 메모리 데이터베이스 역할
const users = [
  { id: 1, name: "김철수", email: "kim@example.com" },
  { id: 2, name: "홍길동", email: "hong@example.com" },
];

const server = http.createServer((req, res) => {
  // CORS 헤더 설정
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");

  // OPTIONS 요청에 대한 처리
  if (req.method === "OPTIONS") {
    res.writeHead(204);
    res.end();
    return;
  }

  // URL 파싱
  const parsedUrl = url.parse(req.url, true);
  const path = parsedUrl.pathname;
  const trimmedPath = path.replace(/^\/+|\/+$/g, "");

  // 사용자 API 경로 처리
  if (trimmedPath === "api/users") {
    // 모든 사용자 조회 (GET)
    if (req.method === "GET") {
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(JSON.stringify(users));
    }
    // 새 사용자 추가 (POST)
    else if (req.method === "POST") {
      let body = "";

      req.on("data", (chunk) => {
        body += chunk.toString();
      });

      req.on("end", () => {
        try {
          const newUser = JSON.parse(body);
          newUser.id = users.length + 1; // 간단한 ID 생성
          users.push(newUser);

          res.writeHead(201, { "Content-Type": "application/json" });
          res.end(JSON.stringify(newUser));
        } catch (error) {
          res.writeHead(400, { "Content-Type": "application/json" });
          res.end(JSON.stringify({ error: "잘못된 요청입니다" }));
        }
      });
    } else {
      res.writeHead(405, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ error: "허용되지 않는 메소드입니다" }));
    }
  }
  // 특정 사용자 API 경로 처리
  else if (trimmedPath.match(/^api\/users\/\d+$/)) {
    const id = parseInt(trimmedPath.split("/").pop());
    const userIndex = users.findIndex((user) => user.id === id);

    if (userIndex === -1) {
      res.writeHead(404, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ error: "사용자를 찾을 수 없습니다" }));
      return;
    }

    // 특정 사용자 조회 (GET)
    if (req.method === "GET") {
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(JSON.stringify(users[userIndex]));
    }
    // 사용자 정보 업데이트 (PUT)
    else if (req.method === "PUT") {
      let body = "";

      req.on("data", (chunk) => {
        body += chunk.toString();
      });

      req.on("end", () => {
        try {
          const updatedUser = JSON.parse(body);
          users[userIndex] = { ...users[userIndex], ...updatedUser, id };

          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify(users[userIndex]));
        } catch (error) {
          res.writeHead(400, { "Content-Type": "application/json" });
          res.end(JSON.stringify({ error: "잘못된 요청입니다" }));
        }
      });
    }
    // 사용자 삭제 (DELETE)
    else if (req.method === "DELETE") {
      const deletedUser = users.splice(userIndex, 1)[0];

      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(JSON.stringify(deletedUser));
    } else {
      res.writeHead(405, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ error: "허용되지 않는 메소드입니다" }));
    }
  }
  // 기타 경로에 대한 처리
  else {
    res.writeHead(404, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ error: "리소스를 찾을 수 없습니다" }));
  }
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`서버가 포트 ${PORT}에서 실행 중입니다`);
});

HTTP 모듈의 한계와 대안

HTTP 모듈은 간단한 웹 서버를 구축하는 데 유용하지만, 복잡한 애플리케이션을 개발할 때는 다음과 같은 한계가 있습니다:

  1. 라우팅 기능이 제한적입니다.
  2. 미들웨어 지원이 없습니다.
  3. 오류 처리가 복잡합니다.
  4. 파일 업로드, 폼 처리 등 고급 기능을 직접 구현해야 합니다.

이러한 한계를 극복하기 위해 Express, Koa, Fastify와 같은 웹 프레임워크를 사용하는 것이 일반적입니다. 이러한 프레임워크는 HTTP 모듈을 기반으로 구축되었지만 더 많은 기능과 편의성을 제공합니다.

요약

  • HTTP 모듈은 Node.js에서 웹 서버와 클라이언트를 구현하기 위한 핵심 모듈입니다.
  • 기본 사용법:
    • http.createServer()로 서버 생성
    • 요청(req)과 응답(res) 객체를 처리하는 콜백 함수 제공
    • server.listen()으로 특정 포트에서 서버 시작
  • 주요 기능:
    • 요청 정보 접근 (req.method, req.url, req.headers)
    • 응답 헤더 및 상태 코드 설정 (res.writeHead(), res.setHeader())
    • 응답 데이터 전송 (res.write(), res.end())
    • HTTP 클라이언트 기능 (http.request(), http.get())
  • 고급 사용법:
    • 이벤트 기반 프로그래밍
    • 파일 스트리밍
    • REST API 구현
    • CORS 및 보안 헤더 관리

HTTP 모듈은 Node.js 웹 개발의 기초가 되는 모듈로, 이를 이해하면 Express와 같은 상위 수준 프레임워크의 동작 원리도 더 잘 이해할 수 있습니다.

results matching ""

    No results matching ""