관리 메뉴

진취적 삶

04 http 모듈로 서버 만들기 본문

개발 도서/Node.js 교과서

04 http 모듈로 서버 만들기

hp0724 2023. 7. 14. 11:42

서버는 클라이언트가 있기에 동작한다. 클라이언트에서 서버로 요청을 보내고 ,서버에서는 요청의 내용을 읽고 처리한 뒤 클라이언트에 응답을 보낸다.

const http = require("http");
http.createServer((req, res) => {
    
});

http 서버가 있어야 웹브라우저의 요청을 처리할수 있으므로 http 모듈을 사용

req = request res = response 을 줄여서 표현한다. req 객체는 요청에 관한 정보들을 ,res 객체는 응답에 관한 정보들을 담고 있다.

const http = require("http");

http
  .createServer((req, res) => {
    res.writeHead(200, { "Content-type": "text/html; charset=utf-8" });
    res.write("<h1> Hello node </h1>");
    res.end("<p> hello server </p>");
  })
  .listen(8080, () => {
    // 서버 연결
    console.log("8080 번 포트에서 서버 대기중입니다 ");
  });
  • res.wrtieHead : 응답에 대한 정보를 기록하는 메서드 첫번째 인수는 성공적 요청임을 뜻하는 200 , 두번째 인수는 콘텐츠 형식의 html 과 한글표시를 위한 utf-8으로 지정 이 정보가 기록되는 부분을 헤더라고 한다.
  • res.write : 첫번째 인수 클라이언트에 보낼 데이터 ,데이터가 기록되는 부분을 본문이라고 한다.
  • res.end : 응답을 종료하는 메서드

localhost :현재 컴퓨터의 내부 조소 외부에서는 접근할수 없고, 자신의 컴퓨터에서만 접근 가능

숫자 주소는 IP (internet protocol)

포트: 서버 내에서 프로세스를 구분하는 번호이다. 80번 포트를 사용하면 주소에서 포트를 생략할수 있다.

일반적으로 80번 포트를 이미 다른 서비스가 사용할 확률이 높기 때문에 충돌 방지로 다른포트 사용

서버에 listening 이벤트 리스너를 붙이기

const http = require("http");
const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-type": "text/html; charset=utf-8" });
  res.write("<h1>hello node</h1>");
  res.end("<p>hello server </p>");
});
server.listen(8080);

server.on("listening", () => {
  console.log("8080 대기 ");
});
server.on("error", (error) => {
  console.error(error);
});

여러 서버 실행도 가능

const http = require("http");
http
  .createServer((req, res) => {
    res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });
    res.write("<h1>hello node</h1>");
    res.end("<p> hello server </h1>");
  })
  .listen(8080, () => {
    console.log("8080 서버 연결 ");
  });
http
  .createServer((req, res) => {
    res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });
    res.write("<h1>hello node</h1>");
    res.end("<p>hello server</h1>");
  })
  .listen(8081, () => {
    console.log("8081번 포트 대기 ");
  });

html 파일 만들어서 읽기

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>node js 웹서버</h1>
    <p>만들준비 됨?</p>
  </body>
</html>
const http = require("http");
const fs = require("fs").promises;

http
  .createServer(async (req, res) => {
    try {
      const data = await fs.readFile("./server2.html");
      res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
      res.end(data);
    } catch (error) {
      console.error(error);
      res.writeHead(500, { "Content-Type": "text/plain;charset=ut

res.writeHead 에서 첫 번째 인수로 상태 코드를 넣는다.

  • 2XX :성공을 알리는 코드
  • 3XX : 리다이렉션(다른 페이지로 이동) 을 알리는 상태 코드
  • 4XX :요청 요류 나타냄
  • 5XX : 서버 오류를 나타냄 요

요청 처리 과정중에 에러가 발생했다고 해서 응답을 보내지 않으면 안된다. 요청이 성공했든 실패했든 응답을 클라이언트로 보내서 요청이 마무리 되었음을 알려야한다. 응답을 보내지 않으면 클라이언트는 서버로부터 응답이 오길 하염없이 기다리다가 일정시간이 지난후 Timeout 처리

4.2 REST와 라우티 사용하기

서버에 요청을 보낼 때는 주소를 통해 요청의 내용을 표현한다. 요청의 내용이 주소를 통해 표현되므로 서버가 이해하기 쉬운 주소를 사용하는것이 좋다. 여기서 REST 등장

REST: REpresentational state transfer 줄임말 서버의 자원을 정의하고 자원에 대한 주소를 지정하는 방법

주소는 의미를 명확히 전달하기 위해 명사로 구성된다.

주소 외에서도 HTTP 요청 메서드를 사용하다

  • GET: 서버 자원을 가져오고자 할 때 사용한다. 요청의 본문에 데이터를 넣지 않는다. 데이터를 서버로 보내야 한다면 쿼리스트링을 사용
  • POST : 서버에 자원을 새로 등록하고자 할 때 사용 .요청 본문에 새로 등록할 데이터를 넣어 보낸다.
  • PUT : 서버의 자원을 요청에 들어있는 자원으로 치환하고자 할때 사용 .요청의 본문에 치환할 데이터를 넣어 보낸다 .
  • PATCH : 서버 자원의 일부만 수정하고자 할 때 사용한다. 요청의 본문에 일부 수정할 데이터를 넣어 보낸다 .
  • DELETE :서버의 자원을 삭제하고자 할 때 사용한다. 요청의 본문에 데이터를 넣지 않는다.
  • OPTIONS: 요청을 하기전에 통신 옵션 설정

HTTP 통신을 사용하면 클라이언트가 누구든 상관없이 같은 방식으로 서버와 소통할수 있다. REST를 따르는 서버를 RESTful 하다고 표현한다 .

HTTP 메서드 주소 역할

GET / restFront.html 파일 제공
GET /about about.html 파일제공
GET /users 사용자 목록 제공
GET 기타 기타 정적 파일 제공
POST /user 사용자 등록
PUT /user/사용자 id 해당 id의 사용자 수정
DELETE /user/사용자id 해당 id의 사용자 제거
const http = require("http");
const fs = require("fs").promises;
const path = require("path");

const users = {}; // 데이터 저장용

http
  .createServer(async (req, res) => {
    try {
      console.log(req.method, req.url);
      if (req.method === "GET") {
        if (req.url === "/") {
          const data = await fs.readFile(
            path.join(__dirname, "restFront.html")
          );
          res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });
          return res.end(data);
        } else if (req.url === "/about") {
          const data = await fs.readFile(path.join(__dirname, "about.html"));
          res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });

          //return 을 붙여야 함수 종료
          return res.end(data);
        } else if (req.url === "/users") {
          res.writeHead(200, { "Content-Type": "text/plain;charset=utf-8" });
          return res.end(JSON.stringify(users));
        }
        // 주소가 /도 아니고 /about 도 아니면
        try {
          const data = await fs.readFile(path.join(__dirname, req.url));
          return res.end(data);
        } catch (error) {
          //주소에 해당하는 라우트를 찾지못했다는 404 발생
          console.error(error);
        }
      } else if (req.method === "POST") {
        if (req.url === "/user") {
          let body = "";
          //요청의 body를 stream 형식으로 받음
          req.on("data", (data) => {
            body += data;
          });
          //요청의 body를 받은후 실행
          return req.on("end", () => {
            console.log("POST 본문 (Body):", body);
            const { name } = JSON.parse(body);
            const id = Date.now();
            users[id] = name;
            res.writeHead(201, { "Content-Type": "text/html;charset=utf-8" });

            res.end("등록 성공");
          });
        }
      } else if (req.method === "PUT") {
        if (req.url.startsWith("/user/")) {
          const key = req.url.split("/")[2];
          let body = "";
          req.on("data", (data) => {
            body += data;
          });
          return req.on("end", () => {
            console.log("PUT 본문 :".body);
            users[key] = JSON.parse(body).name;
            res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });

            return res.end(JSON.stringify(users));
          });
        }
      } else if (req.method === "DELETE") {
        if (req.url.startsWith("/user/")) {
          const key = req.url.split("/")[2];
          delete users[key];
          res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });

          return res.end(JSON.stringify(users));
        }
      }
      res.writeHead(404);
      return res.end("Not found");
    } catch (error) {
      console.error(error);
      res.writeHead(200, { "Content-Type": "text/plain;charset=utf-8" });

      res.end(error.message);
    }
  })
  .listen(8082, () => {
    console.log("8082번 포트에서 서버 대기중 ");
  });

Netwokr 탭에서 요청 내용을 실시간으로 확인할수 있다.

Name 은 요청 주소를 Method는 요청 메서드를 , status는 HTTP 응답 코드를 Protocol은 통신 프로토콜 ,Type은 요청의 종류를 의미한다.

헤더와 본문

요청과 응답은 모두 헤더와 본문을 갖고 있다. 헤더는 요청 또는 응답에 대한 정보를 갖고있는곳이고 ,본문은 서버와 클라이언트 간에 주고받을 실제 데이터를 담아두는 공간이다.

General은 공통된 헤더이고Response Headers 는 응답의 헤더 ,Request Headers는 요청의 헤더이다.

4.3 쿠키와 세션 이해하기

클라이언트에서 보내는 요청에는 한가지 단점이 있다. 누가 요청을 보내는지 모른다는것

요청 보내는 ip주소나 브라우저의 정보를 받을수는 있지만 여러 컴퓨터가 공통으로 IP 주소를 갖거나 한 컴퓨터를 여러 사람이 사용할수 있다.

그럼 로그인을 구현하자. 로그인 구현은 쿠키와 세션을 알아야 한다.

내가 누구인지 기억하기 위해 서버는 요청에 대한 응답을 할때 쿠키를 보낸다.

쿠키는 단순한 키 - 값 쌍이다.

서버는 미리 클라이언트에 요청자를 추정할 만한 정보를 쿠키로 만들어 보내고, 그다음부터는 클라이언트로부터 쿠키를 받아 요청자를 파악한다.

쿠키는 요청의 헤더에 담겨 전송된다.

const http = require("http");
http
  .createServer((req, res) => {
    console.log(req.url, req.headers.cookie);
    res.writeHead(200, { "Set-Cookie": "mycookie=test" });
    res.end("hello cookie");
  })
  .listen(8083, () => {
    console.log("8083번 포트에서 서버 대기중 ");
  });

쿠키는 req.headers.cookie 에 들어있다.

/ undefined /favicon.ico mycookie=test

favicon 은 웹사이트 탭에 보이는 이미지를 뜻한다.

첫번째 요청 (/) 보내기 전에는 브라우저가 어떠한 쿠키 정보도 갖고 있지 않았다.

서버는 응답의 헤더에 mycookie=test 라는 쿠키를 심으라고 브라우저에 명령(set-Cokkie)했다.

브라우저는 쿠키를 심고 두번째 요청에 (/favicon.ico ) 헤더에 쿠키가 들어있음을 확인

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>쿠키&섹션 이해하기</title>
  </head>
  <body>
    <form action="/login">
      <input id="name" name="name" placeholder="이름을 입력하세요" />
      <button id="login">로그인</button>
    </form>
  </body>
</html>
const http = require("http");
const fs = require("fs").promises;
const path = require("path");

const parseCookies = (cookie = "") =>
  cookie
    .split(";")
    .map((v) => v.split("="))
    .reduce((acc, [k, v]) => {
      acc[k.trim()] = decodeURIComponent(v);
      return acc;
    }, {});
http
  .createServer(async (req, res) => {
    const cookies = parseCookies(req.headers.cookie);

    // 주소가 /login으로 시작하는 경우
    if (req.url.startsWith("/login")) {
      const url = new URL(req.url, "<http://localhost:8084>");
      const name = url.searchParams.get("name");
      const expires = new Date();
      //쿠키 유효시간을 현재 시간 +5분으로 설정
      expires.setMinutes(expires.getMinutes() + 5);
      res.writeHead(302, {
        Location: "/",
        "Set-Cookie": `name=${encodeURIComponent(
          name
        )}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
      });
      res.end();
      //주소가 /이면서 name이라는 쿠키가 있는 경우
    } else if (cookies.name) {
      res.writeHead(200, { "Content-Type": "text/plain;charset=utf-8" });
      res.end(`${cookies.name}님 안녕하세요 `);
    } else {
      //주소가 /이면서 name이라는 쿠키가 없는 경우
      try {
        const data = await fs.readFile(path.join(__dirname, "cookie2.html"));
        res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });
        res.end(data);
      } catch (error) {
        res.writeHead(500, { "Content-Type": "text/plain;charset=utf-8" });
        res.end(error.message);
      }
    }
  })
  .listen(8084, () => {
    console.log("8084번 포트에서 서버 대기중입니다. ");
  });

주소가 /login 과 / 로 시작하는것 까지 두개 주소별로 분기 처리

http://localhost:8084/ 로 접속하면 /로 요청을 보내는것 form을 통해 로그인 요청을 보낼때 /login으로 요청을 보내게 된다.

  • parseCookies 함수는 쿠키 문자열을 쉽게 사용하기위해 자바스크립트 객체 형식으로 바꾸는 함수 이 함수를 거치면 {키-값}형식으로 바꿈 이 함수가 문자열을 객체로 바꿔줌
  • form은 GET 요청인 경우 데이터를 쿼리스트링으로 보내기에 URL 객체로 쿼리스트링 부분을 분석 302응답코드와 리다이렉트 주소와 함께 쿠키를 헤더에 넣는다 헤더에는 한글을 설정할수 없기에 name 변수를 encodeURIComponent 메서드로 인코딩한다
  • 그외의 경우(/로접속하는등) ,쿠키가 있는지 없는지 확인하고 쿠키가 없다면 로그인할수있는 페이지로 보낸다 . 처음 방문한 경우에는 쿠키가 없기때문에 cookie2.html 이 전송

Set-Cookie

  • 쿠키명= 쿠키값 : 기본적인 쿠키의 값이다 mycookie = test
  • Expires=날짜 : 만료 기한이다. 이 기한이 지나면 쿠키가 제거
  • Max-age = 초 : Expires 와 비슷하지만 날짜 대신 초를 입력할수 있다 해당 초가 지나면 쿠키 제거
  • Domain =도메인명 : 쿠키가 전송될 도메인을 특정할수 있다. 기본값은 현재 도메인
  • Path=URL :쿠키가 전송될 URL을 특정할수 있다. 기본값은 ‘/’아고 이 경우 모든 URL에서 쿠키를 전송할수 있다.
  • Secure :HTTPS일 경우에만 쿠키가 전송
  • HttpOnly:설정 시 자바스크립트에서 쿠키에 접근할수없다. 쿠키 조작을 방지하기위해 설정하는것이 좋다.

코드가 잘 작동하지만 Application 탭에서 쿠키가 노출되어있다. 이 방식은 쿠키가 조작될 위험도 있다. 이름 같은 민감한 개인정보를 쿠키에 넣어두는 것은 적절하지 못하다 .

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

const parseCookies = (cookie = "") =>
  cookie
    .split(";")
    .map((v) => v.split("="))
    .reduce((acc, [k, v]) => {
      acc[k.trim()] = decodeURIComponent(v);
      return acc;
    }, {});

const session = {};
http
  .createServer(async (req, res) => {
    const cookies = parseCookies(req.headers.cookie);
    console.log("cookies", cookies); // 디버깅을 위한 코드
    if (req.url.startsWith("/login")) {
      const url = new URL(req.url, "<http://localhost:8085>");
      const name = url.searchParams.get("name");
      const expires = new Date();
      expires.setMinutes(expires.getMinutes() + 5);
      const uniqueInt = Date.now();
      session[uniqueInt] = {
        name,
        expires,
      };
      res.writeHead(302, {
        Location: "/",
        "Set-Cookie": `session=${uniqueInt}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
      });
      res.end();
    } else if (
      cookies.session &&
      session[cookies.session].expires > new Date()
    ) {
      res.writeHead(200, { "Content-type": "text/html; charset=utf-8" });
      res.end(`${session[cookies.session].name}님 안녕하세요`);
    } else {
      try {
        const data = await fs.readFile(
          path.join(__dirname, "cookie2.html"),
          (err, data) => {
            if (err) throw err; // 콜백 함수에서 에러 발생 시 throw로 에러를 전달해줍니다.
            res.writeHead(200, { "Content-type": "text/html; charset=utf-8" });
            res.end(data);
          }
        );
      } catch (error) {
        res.writeHead(500, { "Content-type": "text/plain; charset=utf-8" });
        res.end(error.message);
      }
    }
  })
  .listen(8085, () => {
    console.log("8085번 포트에서 서버 대기중 ");
	  });

세션의 경우 서버에 사용자 정보를 저장하고 클라이언트와는 세션 아이디로만 소통한다.

세션 아이디는 쿠키를 사용해서 주고받지 않아도 된다. 세션을 위해 사용하는 쿠키를 세션 쿠키라고 한다.

위의 코드는 보안상 매우 취약

4.4 https 와 http2

https : 웹서버에 SSL 암호화를 추가한다. GET이나 POST 요청을 할 때 오가는 데이터를 암호화해서 중간에 다른 사람이 요청을 가로채더라도 내용을 확인할수 없게 한다.

https 를 사용할려면 인증 기관에서 구입해서 사용한다. 인증서 발급과정은 도메인도 필요하고 복잡함 .

http2 는 SSL 암호화와 더불어 최신 HTTP 프로토콜인 http/2를 사용할수 있다.

4.5 cluster

싱글 프로세스로 동작하는 노드가 CPU 코어를 모두 사용할수 있게 해주는 모듈

cluster 모듈을 설정해 코드 하나당 노드 프로세스 하나가 돌아가게 할수 있다. 메모리를 공유하지 못하는 단점

const cluster = require("cluster");
const http = require("http");
const numCPUs = require("os").cpus().length;

if (cluster.isMaster) {
  console.log(`마스터 프로세스 아이디 :${process.pid}`);
  //cpu 개수만큼 워커를 생산
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  //워커가 종료되었을때
  cluster.on("exit", (worker, code, signal) => {
    console.log(`${worker.process.pid}번 워커가 종료되었습니다 `);
    console.log(`code`, code, `signal`, signal);
  });
} else {
  http
    .createServer((req, res) => {
      res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });
      res.write(`<h1>hello node</h1>`);
      res.end("<p> hello cluster</p>");
    })
    .listen(8086);
  console.log(`${process.pid} 번 워커실행 `);
}

클러스터에는 마스터 프로세스와 워커 프로세스가 있다. 마스터 프로세스는 CPU 개수만큼 워커 프로세스를 만들고 8086번 포트에서 대기한다.

워커 프로세스가 실질적인 일을 하는 프로세스이다.

const cluster = require("cluster");
const http = require("http");
const numCPUs = require("os").cpus().length;

if (cluster.isMaster) {
  console.log(`마스터 프로세스 아이디 :${process.pid}`);
  //cpu 개수만큼 워커를 생산
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  //워커가 종료되었을때
  cluster.on("exit", (worker, code, signal) => {
    console.log(`${worker.process.pid}번 워커가 종료되었습니다 `);
    console.log(`code`, code, `signal`, signal);
    //워커 프로세스 종류 후 새로 하나 생성
    cluster.fork();
  });
} else {
  http
    .createServer((req, res) => {
      res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });
      res.write(`<h1>hello node</h1>`);
      res.end("<p> hello cluster</p>");
      setTimeout(() => {
        process.exit(1);
      }, 1000);
    })
    .listen(8086);
  console.log(`${process.pid} 번 워커실행 `);
}

'개발 도서 > Node.js 교과서' 카테고리의 다른 글

06 익스프레스 웹 서버 만들기  (0) 2023.07.14
05 패키지 매니저  (0) 2023.07.14
03 노드 기능 알아보기  (0) 2023.07.14
02 알아둬야 할 자바스크립트  (0) 2023.07.14
01 노드 시작하기  (0) 2023.07.14