My Boundary As Much As I Experienced

제로초 Node.js / ch3-4 / http 모듈을 활용한 crud 구현 본문

BackEnd/Node.js

제로초 Node.js / ch3-4 / http 모듈을 활용한 crud 구현

Bumang 2024. 3. 10. 17:10

앞서 node.js의 기본 모듈들을 학습한 후, 이번 시간엔 본격적으로 http모듈만을 이용한 서버 만들기를 진행해봤다.

제공된 코드만 읽지말고 꼭 개량해보고 기능 추가해보라는 제로초 선생님의 말씀대로 한 번 기능들을 추가해봤다.

 

 

추가한 기능:

  • 모두 삭제하기 기능 (유저등록/코멘트)
  • 토끼로 만들기 기능 (유저등록/코멘트)
  • 코멘트 란 읽기/추가/삭제/수정

토끼로 만들기를 누르면 PUT요청을 통해 바로 토끼로 변한다. (야심차게 만든 기능)

 

node 서버를 처음 만들어본 느낌은 '뭐야 이거 자바스크립트잖아?' 이다. (당연한 얘기지만😂)

프론트에서도 서버가 깔끔하게 정제된 정보를 주지않고 날것의 것을 주면 손수 데이터를 가공해서 쓰는데, 노드 서버가 하는 일도 별반 다르지 않다는 것을 알 수 있었다. '프론트에서 받은 요청과 request body에 따라 내 저장소에 있는 내용을 편집하고 그 내용을 다시 보내준다.'는 패턴으로 동작했다.

 

이제 실제 코드에 주석으로 설명해보겠다. 아래는 http서버 스크립트이다.

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

const users = {}; // 유저 데이터 저장용
const comments = {}; // 코멘트 데이터 저장용
// 이런 유저 리스트를 만들때는 배열 자료구조를 사용하는게 일반적일거라 예상했지만,
// 제로초 선생님이 만든걸 보면 객체 자료구조를 사용하였다. 
// 시간복잡도 O/1을 유지하기 위해서 이렇게 짜신 것 같다.
// 서버를 껐다 키면 초기화 된다. 그러므로 서버 장애 후에도 데이터가 유지되려면 데이터베이스가 필요하다는걸 알 수 있다.

http
  .createServer(async (req, res) => {
    try {
      if (req.method === "GET") { // GET 요청일 때
        if (req.url === "/") { // GET요청으로 html파일을 넘겨줄 때
          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") { // GET요청으로 html파일을 넘겨줄 때
          const data = await fs.readFile(path.join(__dirname, "about.html"));
          res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
          return res.end(data);
        } else if (req.url === "/users") { // GET요청으로 JSON을 넘겨줄 때
          res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
          return res.end(JSON.stringify(users));
        } else if (req.url === "/comment") { // GET요청으로 JSON을 넘겨줄 때
          res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
          return res.end(JSON.stringify(comments));
        }
        // /도 /about도 /users도 아니면
        try {
          const data = await fs.readFile(path.join(__dirname, req.url));
          return res.end(data);
        } catch (err) {
          // 주소에 해당하는 라우트를 못 찾았다는 404 Not Found error 발생
        }
      } else if (req.method === "POST") { // 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);
            // Date.now()를 해서 키값을 만들어서, 사용자의 입력값을 밸류값으로 지정한다.
            const id = Date.now();
            users[id] = name;
            res.writeHead(201, { "Content-Type": "text/plain; charset=utf-8" });
            res.end("등록 성공");
          });
        } else if (req.url === "/comment") {
          let body = "";
          // 요청의 body를 stream 형식으로 받음
          req.on("data", (data) => {
            body += data;
          });
          // 요청의 body를 다 받은 후 실행됨
          return req.on("end", () => {
            console.log("POST 코멘트 내용(Body):", body);
            const { comment } = JSON.parse(body);
            const id = Date.now();
            comments[id] = comment;
            res.writeHead(201, { "Content-Type": "text/plain; charset=utf-8" });
            res.end("등록 성공");
          });
        }
      } else if (req.method === "PUT") { // 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):", body);
            users[key] = JSON.parse(body).name;
            res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
            return res.end(JSON.stringify(users));
          });
        } else if (req.url.startsWith("/comment/")) {
          const key = req.url.split("/")[2];
          let body = "";
          req.on("data", (data) => {
            body += data;
          });
          return req.on("end", () => {
            console.log("PUT 코멘트 내용(Body):", body);
            comments[key] = JSON.parse(body).name;
            res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
            return res.end(JSON.stringify(comments));
          });
        } else if (req.url.startsWith("/rabbitify/user/")) {
          const spl = req.url.split("/");
          const key = spl[spl.length - 1];
          console.log(key, "key");
          users[key] = "🐇";
          res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
          return res.end(JSON.stringify(users));
        } else if (req.url.startsWith("/rabbitify/comment/")) {
          const spl = req.url.split("/");
          const key = spl[spl.length - 1];
          console.log(key, "key");
          comments[key] = "🐇";
          res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
          return res.end(JSON.stringify(comments));
        }
      } else if (req.method === "DELETE") { // DELETE 요청일 때
        if (req.url.startsWith("/user/")) {
          const key = req.url.split("/")[2];
          delete users[key];
          res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
          return res.end(JSON.stringify(users));
        } else if (req.url.startsWith("/delete-all/user")) {
          for (let user in users) {
            // for in 순회로 comment의 각 목록을 다 지워줬다.
            // 그냥 users 객체를 let선언으로 바꾼 다음에 새 객체를 {}할당해서 보내는게 성능 상 더 좋긴 할텐데..
            // let선언으로 바꿔서 실수로 객체를 의도치 않게 한 번에 싹 없애버리는 일을 안 만들기 위해서 
            // 그냥 for in 순회로 일일이 다 지우기로 했다. (데이터도 얼마 없으니)
            delete users[user];
          }
          res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
          return res.end(JSON.stringify({})); // 그냥 빈 객체 보내줌
        } else if (req.url.startsWith("/comment/")) {
          const key = req.url.split("/")[2];
          delete comments[key];
          res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
          return res.end(JSON.stringify(comments));
        } else if (req.url.startsWith("/delete-all/comment")) {
          for (let user in comments) { 
            // comments도 for in 순회로 comment의 각 목록을 다 지워줬다.
            delete comments[user];
          }
          res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
          return res.end(JSON.stringify({})); // 그냥 빈 객체 보내줌
        } 
      }
      res.writeHead(404);
      return res.end("NOT FOUND");
    } catch (err) {
      console.error(err);
      res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
      res.end(err.message);
    }
  })
  .listen(8082, () => {
    console.log("8082번 포트에서 서버 대기 중입니다");
  });

 

데이터 자료형을 객체로 했기 때문에 프론트에선 응답받은 객체를 iterable하게 수선해서 써야한다.

객체는 iterable하지 않기 때문에 프론트에선 받은 객체를 뿌려주려면 for in을 하거나 Object.keys()로 키를 추출해서 순회해야된다.

사실 처음엔 이렇게 객체에서 key를 추출해서 순회할 때 '생성된 순서는 지킬 수 없지 않나?' 라는 우려가 있었지만 아니었다.

기본적으로 key값은 유니코드 순으로 정렬된다. 그러므로 Date.now()로 키를 생성했기 때문에

나중에 만들어진 프로퍼티일수록 맨 아래 정렬되어있게 된다. 

 

하여튼 이렇게 http모듈로 서버를 짜면 if지옥을 경험하게 되기 때문에, express라는 라이브러리가 이를 보완하기 위해 등장했다고 한다.

express는 조금 더 깔끔한 문법과 추상화된 유틸함수들을 제공한다. express로 서버 만들기를 곧 배워볼 예정이다.