프로그래밍 언어/NODE JS

쿼리 수행하기

· 코딩마이데이

views 폴더 안에 mongoose.html과 error.html 파일을 만듭니다.

views/mongoose.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>몽구스 서버</title>
    <style>
      table {
        border: 1px solid black;
        border-collapse: collapse;
      }
      table th,
      table td {
        border: 1px solid black;
      }
    </style>
  </head>
  <body>
    <div>
      <form id="user-form">
        <fieldset>
          <legend>사용자 등록</legend>
          <div><input id="username" type="text" placeholder="이름" /></div>
          <div><input id="age" type="number" placeholder="나이" /></div>
          <div>
            <input id="married" type="checkbox" /><label for="married"
              >결혼 여부</label
            >
          </div>
          <button type="submit">등록</button>
        </fieldset>
      </form>
    </div>
    <br />
    <table id="user-list">
      <thead>
        <tr>
          <th>아이디</th>
          <th>이름</th>
          <th>나이</th>
          <th>결혼 여부</th>
        </tr>
      </thead>
      <tbody>
        {% for user in users %}
        <tr>
          <td>{{user.id}}</td>
          <td>{{user.name}}</td>
          <td>{{user.age}}</td>
          <td>{{ '기혼' if user.married else '미혼'}}</td>
        </tr>
        {% endfor %}
      </tbody>
    </table>
    <br />
    <div>
      <form id="comment-form">
        <fieldset>
          <legend>댓글 등록</legend>
          <div>
            <input id="userid" type="text" placeholder="사용자 아이디" />
          </div>
          <div><input id="comment" type="text" placeholder="댓글" /></div>
          <button type="submit">등록</button>
        </fieldset>
      </form>
    </div>
    <br />
    <table id="comment-list">
      <thead>
        <tr>
          <th>아이디</th>
          <th>작성자</th>
          <th>댓글</th>
          <th>수정</th>
          <th>삭제</th>
        </tr>
      </thead>
      <tbody></tbody>
    </table>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="/mongoose.js"></script>
  </body>
</html>

 

views/error.html

<h1>{{message}}</h1>
<h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre>

 

public 폴더 안에 mongoose.js 파일도 만듭니다.

 

public/mongoose.js

// 사용자 이름을 눌렀을 때 댓글 로딩
document.querySelectorAll("#user-list tr").forEach((el) => {
  el.addEventListener("click", function () {
    const id = el.querySelector("td").textContent;
    getComment(id);
  });
});
// 사용자 로딩
async function getUser() {
  try {
    const res = await axios.get("/users");
    const users = res.data;
    console.log(users);
    const tbody = document.querySelector("#user-list tbody");
    tbody.innerHTML = "";
    users.map(function (user) {
      const row = document.createElement("tr");
      row.addEventListener("click", () => {
        getComment(user._id);
      });
      // 로우 셀 추가
      let td = document.createElement("td");
      td.textContent = user._id;
      row.appendChild(td);
      td = document.createElement("td");
      td.textContent = user.name;
      row.appendChild(td);
      td = document.createElement("td");
      td.textContent = user.age;
      row.appendChild(td);
      td = document.createElement("td");
      td.textContent = user.married ? "기혼" : "미혼";
      row.appendChild(td);
      tbody.appendChild(row);
    });
  } catch (err) {
    console.error(err);
  }
}
// 댓글 로딩
async function getComment(id) {
  try {
    const res = await axios.get(`/users/${id}/comments`);
    const comments = res.data;
    const tbody = document.querySelector("#comment-list tbody");
    tbody.innerHTML = "";
    comments.map(function (comment) {
      // 로우 셀 추가
      const row = document.createElement("tr");
      let td = document.createElement("td");
      td.textContent = comment._id;
      row.appendChild(td);
      td = document.createElement("td");
      td.textContent = comment.commenter.name;
      row.appendChild(td);
      td = document.createElement("td");
      td.textContent = comment.comment;
      row.appendChild(td);
      const edit = document.createElement("button");
      edit.textContent = "수정";
      edit.addEventListener("click", async () => {
        // 수정 클릭 시
        const newComment = prompt("바꿀 내용을 입력하세요");
        if (!newComment) {
          return alert("내용을 반드시 입력하셔야 합니다");
        }
        try {
          await axios.patch(`/comments/${comment._id}`, {
            comment: newComment,
          });
          getComment(id);
        } catch (err) {
          console.error(err);
        }
      });
      const remove = document.createElement("button");
      remove.textContent = "삭제";
      remove.addEventListener("click", async () => {
        // 삭제 클릭 시
        try {
          await axios.delete(`/comments/${comment._id}`);
          getComment(id);
        } catch (err) {
          console.error(err);
        }
      });
      // 버튼 추가
      td = document.createElement("td");
      td.appendChild(edit);
      row.appendChild(td);
      td = document.createElement("td");
      td.appendChild(remove);
      row.appendChild(td);
      tbody.appendChild(row);
    });
  } catch (err) {
    console.error(err);
  }
}
// 사용자 등록 시
document.getElementById("user-form").addEventListener("submit", async (e) => {
  e.preventDefault();
  const name = e.target.username.value;
  const age = e.target.age.value;
  const married = e.target.married.checked;
  if (!name) {
    return alert("이름을 입력하세요");
  }
  if (!age) {
    return alert("나이를 입력하세요");
  }
  try {
    await axios.post("/users", { name, age, married });
    getUser();
  } catch (err) {
    console.error(err);
  }
  e.target.username.value = "";
  e.target.age.value = "";
  e.target.married.checked = false;
});
// 댓글 등록 시
document
  .getElementById("comment-form")
  .addEventListener("submit", async (e) => {
    e.preventDefault();
    const id = e.target.userid.value;
    const comment = e.target.comment.value;
    if (!id) {
      return alert("아이디를 입력하세요");
    }
    if (!comment) {
      return alert("댓글을 입력하세요");
    }
    try {
      await axios.post("/comments", { id, comment });
      getComment(id);
    } catch (err) {
      console.error(err);
    }
    e.target.userid.value = "";
    e.target.comment.value = "";
  });

 

HTML 쪽보다는 서버 코드 위주로 보면 됩니다. script 태그에는 버튼들을 눌렀을 때 서버의 라우터로 AJAX 요청을 보내는 코드가 들어 있습니다.

조금 뒤에 만들 라우터들을 미리 app.js에 연결합니다.

 

app.js

const express = require("express");
const path = require("path");
const morgan = require("morgan");
const nunjucks = require("nunjucks");

const connect = require("./schemas");
const indexRouter = require("./routes");
const usersRouter = require("./routes/users");
const commentsRouter = require("./routes/comments");

const app = express();
app.set("port", process.env.PORT || 3002);
app.set("view engine", "hmtl");
nunjucks.configure("views", {
  express: app,
  watch: true,
});
connect();

app.use(morgan("dev"));
app.use(express.static(path.join(__dirname, "public")));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.use((req, res, next) => {
  const error = new Error(
    `${req.method} ${req._construct.url} 라우터가 없습니다.`
  );
  error.status = 404;
  next(error);
});

app.use((err, req, next) => {
  res.locals.message = err.message;
  res.locals.error = process.env.NODE_ENV !== "production" ? err : {};
  res.status(err.status || 500);
  res.reder("error");
});

app.use("/", indexRouter);
app.use("/users", usersRouter);
app.use("/comments", commentsRouter);

app.listen(app.get("port"), () => {
  console.log(app.get("port"), " 번 포트에서 대기 중");
});

 

이제 라우터를 작성해보겠습니다.

 

routes/index.js

const express = require("express");
const User = require("../schemas/user");

const router = express.Router();

router.get("/", async (req, res, next) => {
  try {
    const users = await User.find({});
    res.render("mongoose", { users });
  } catch (err) {
    console.error(err);
    next(err);
  }
});

module.exports = router;

 

먼저 GET /로 접속했을 때의 라우터입니다. User.find({}) 메서드로 모든 사용자를 찾은 뒤, mongoose.html을 렌더링할 때 users 변수로 넣습니다. find 메서드는 User 스키마를 require한 뒤 사용할 수 있습니다. 몽고디비의 db.users.find({}) 쿼리와 같습니다.

몽구스도 기본적으로 프로미스를 지원하므로 async/await과 try/catch문을 사용해서 각각 조회 성공 시와 실패 시의 정보를 얻을 수 있습니다. 이렇게 미리 데이터베이스에서 데이터를 조회한 후 템플릿 렌더링에 사용할 수 있습니다.

다음은 users.js입니다.

 

routes/users.js

const express = require("express");
const User = require("../schemas/user");
const Comment = require("../schemas/comment");

const router = express.Router();

router
  .route("/")
  .get(async (req, res, next) => {
    try {
      const users = await User.find({});
      res.json(users);
    } catch (err) {
      console.error(err);
      next(err);
    }
  })
  .post(async (req, res, next) => {
    try {
      const user = await User.create({
        name: req.body.name,
        age: req.body.age,
        married: req.body.married,
      });
      console.log(user);
      res.status(201).json(user);
    } catch (err) {
      console.error(err);
      next(err);
    }
  });

router.get("/:id/comments", async (req, res, next) => {
  try {
    const comments = await Comment.find({ commenter: req.params.id }).populate(
      "commenter"
    );
    console.log(comments);
    res.json(comments);
  } catch (err) {
    console.error(err);
    next(err);
  }
});

module.exports = router;

 

GET /users와 POST /users 주소로 요청이 들어올 때의 라우터입니다. 각각 사용자를 조회하는 요청과 사용자를 등록하는 요청을 처리합니다. GET /에서도 사용자 데이터를 조회했지만 GET /users에서는 데이터를 JSON 형식으로 반환한다는 점에서 차이가 있습니다.

사용자를 등록할 때는 먼저 모델 .create 메서드로 저장합니다. 몽고디비와 메서드가 다르므로 몽구스용 메서드를 따로 외워야 합니다. 정의한 스키마에 부합하지 않는 데이터를 넣었을 때는 몽구스가 에러를 발생시킵니다. _id는 자동으로 생성됩니다.

GET /users/:id/comments 라우터는 댓글 다큐먼트를 조회하는 라우터입니다. find 메서드에는 옵션이 추가되어 있습니다. 먼저 댓글을 쓴 사용자의 아이디로 댓글을 조회한 뒤 populate 메서드로 관련 있는 컬렉션의 다큐먼트를 불러올 수 있습니다. Comment 스키마 commenter 필드의 ref가 User로 되어 있으므로, 자동으로 users 컬렉션에서 사용자 다큐먼트를 찾아 합칩니다. commenter 필드가 사용자 다큐먼트로 치환됩니다. 이제 commenter 필드는 ObjectId가 아니라 그 ObjectId를 가진 사용자 다큐먼트가 됩니다.

 

routes/comments.js

const express = require('express');
const Comment = require('../schemas/comment');

const router = express.Router();

router.post('/', async (req, res, next) => {
  try {
    const comment = await Comment.create({
      commenter: req.body.id,
      comment: req.body.comment,
    });
    console.log(comment);
    const result = await Comment.populate(comment, { path: 'commenter' });
    res.status(201).json(result);
  } catch (err) {
    console.error(err);
    next(err);
  }
});

router.route('/:id')
  .patch(async (req, res, next) => {
    try {
      const result = await Comment.update({
        _id: req.params.id,
      }, {
        comment: req.body.comment,
      });
      res.json(result);
    } catch (err) {
      console.error(err);
      next(err);
    }
  })
  .delete(async (req, res, next) => {
    try {
      const result = await Comment.remove({ _id: req.params.id });
      res.json(result);
    } catch (err) {
      console.error(err);
      next(err);
    }
  });

module.exports = router;

 

댓글에 관련된 CRUD 작업을 하는 라우터입니다. POST /comments, PATCH /comments/:id, DELETE /comments/:id를 등록했습니다.

POST /comments 라우터는 다큐먼트를 등록하는 라우터입니다. Comment.create 메서드로 댓글을 저장합니다. 그 후 populate 메서드로 프로미스의 결과로 반환된 comment 객체에 다른 컬렉션 다큐먼트를 불러옵니다. path 옵션으로 어떤 필드를 합칠지 설정하면 됩니다. 합쳐진 결과를 클라이언트로 응답합니다.

PATCH /comments/:id 라우터는 다큐먼트를 수정하는 라우터입니다. 수정에는 update 메서드를 사용합니다. update 메서드의 첫 번째 인수로는 어떤 다큐먼트를 수정할지를 나타낸 쿼리 객체를 제공하고, 두 번째 인수로는 수정할 필드와 값이 들어 있는 객체를 제공합니다. 시퀄라이즈와는 인수의 순서가 반대입니다. 몽고디비와 다르게 $set 연산자를 사용하지 않아도 기입한 필드만 바꿉니다. 따라서 실수로 다큐먼트를 통째로 수정할 일이 없어 안전합니다.

DELETE /comments/:id 라우터는 다큐먼트를 삭제하는 라우터입니다. remove 메서드를 사용해 삭제합니다. remove 메서드에도 어떤 다큐먼트를 삭제할지에 대한 조건을 첫 번째 인수에 넣습니다.

npm start로 웹 서버를 실행해봅시다.

$ npm start
> learn-mongoose@0.0.1 start 
> nodemon app

[nodemon] 2.0.16
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node app.js`
3002 번 포트에서 대기 중
몽고디비 연결 성공
Mongoose: users.createIndex({ name: 1 }, { unique: true, background: true })

 

서버 실행 후 http://localhost:3002에 접속하면 아래와 같은 화면이 나옵니다. 아이디가 ObjectId라는 점만 다르고 7.6.5절의 애플리케이션과 하는 동작은 같습니다. 대신 몽구스와 시퀄라이즈, 몽고디비와 MySQL의 차이점 때문에 코드가 다릅니다. 두 데이터베이스를 비교해서 보면 됩니다.

몽구스 화면

'프로그래밍 언어 > NODE JS' 카테고리의 다른 글

프로젝트 구조 갖추기 (2)  (0) 2025.10.08
프로젝트 구조 갖추기 (1)  (0) 2025.10.05
스키마 정의하기  (0) 2025.09.29
몽고디비 연결하기  (1) 2025.09.26
몽구스 사용하기  (0) 2025.09.23