쿼리 수행하기
조금 전에 배웠던 쿼리로 CRUD 작업을 해봅시다. 모델에서 데이터를 받아 페이지를 렌더링하는 방법과 JSON 형식으로 데이터를 가져오는 방법 두 가지를 알아보겠습니다.
간단하게 사용자 정보를 등록하고 사용자가 등록한 댓글을 가져오는 서버입니다. 먼저 다음과 같이 views 폴더를 만들고 그 안에 sequelize.html 파일과 error.html 파일을 만듭니다.
views/sequlize.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="/sequelize.js"></script>
</body>
</html>
views/error.html
<h1>{{message}}</h1>
<h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre>
public 폴더 안에 sequelize.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.User.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에 연결합니다.
const express = require("express");
const path = require("path");
const morgan = require("morgan");
const nunjucks = require("nunjucks");
const { sequelize } = require("./models");
const indexRouter = require("./routes");
const usersRouter = require("./routes/users");
const commentsRouter = require("./routes/comments");
const app = express();
app.set("port", process.env.PORT || 3001);
app.set("view engine", "html");
nunjucks.configure("views", {
express: app,
watch: true,
});
sequelize
.sync({ force: false })
.then(() => {
console.log("데이터베이스 연결 성공");
})
.catch((err) => {
console.error(err);
});
app.use(morgan("dev"));
app.use(express.static(path.join(__dirname, "public")));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use("/", indexRouter);
app.use("/users", usersRouter);
app.use("/comments", commentsRouter);
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
error.status = 404;
next(error);
});
app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV !== "production" ? err : {};
res.status(err.status || 500);
res.render("error");
});
app.listen(app.get("port"), () => {
console.log(app.get("port"), "번 포트에서 대기 중");
});
라우터의 내용은 다음과 같습니다. sequelize.js에 나오는 GET, POST, PUT, DELETE 요청에 해당하는 라우터를 만듭니다.
routes 폴더를 만들고 그 안에 index.js를 작성하면 됩니다.
const express = require("express");
const User = require("../models/user");
const router = express.Router();
router.get("/", async (req, res, next) => {
try {
const users = await User.findAll();
res.render("sequelize", { users });
} catch (err) {
console.error(err);
next(err);
}
});
module.exports = router;
먼저 GET /로 접속했을 때의 라우터입니다. User.findAll 메서드로 모든 사용자를 찾은 후, sequelize.html을 렌더링할 때 결괏값인 users를 넣습니다.
시퀄라이즈는 프로미스를 기본적으로 지원하므로 async/await과 try/catch문을 사용해서 각각 조회 성공 시와 실패 시의 정보를 얻을 수 있습니다. 이렇게 미리 데이터베이스에서 데이터를 조회한 후 템플릿 렌더링에 사용할 수 있습니다.
다음은 users.js입니다. router.route 메서드로 같은 라우트 경로는 하나로 묶었습니다.
routes/users.js
const express = require("express");
const User = require("../models/user");
const Comment = require("../models/comment");
const router = express.Router();
router
.route("/")
.get(async (req, res, next) => {
try {
const users = await User.findAll();
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.findAll({
include: {
model: User,
where: { id: req.params.id },
},
});
console.log(comments);
res.json(comments);
} catch (err) {
console.error(err);
next(err);
}
});
module.exports = router;
GET /users와 POST /users 주소로 요청이 들어올 때의 라우터입니다. 각각 사용자를 조회하는 요청과 사용자를 등록하는 요청을 처리합니다. GET /에서도 사용자 데이터를 조회했지만, GET /users에서는 데이터를 JSON 형식으로 반환한다는 것에 차이가 있습니다.
GET /users/:id/comments 라우터에는 findAll 메서드에 옵션이 추가되어 있습니다. include 옵션에서 model 속성에는 User 모델을, where 속성에는 :id로 받은 아이디 값을 넣었습니다. :id는 라우트 매개변수로 6.3절에서 설명했습니다. req.params.id로 값을 가져올 수 있습니다. GET /users/1/comments라면 사용자 id가 1인 댓글을 불러옵니다. 조회된 댓글 객체에는 include로 넣어준 사용자 정보도 들어 있으므로 작성자의 이름이나 나이 등을 조회할 수 있습니다.
다음은 comments.js입니다.
routes/comments.js
const express = require("express");
const { User, Comment } = require("../models");
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);
res.status(201).json(comment);
} catch (err) {
console.error(err);
next(err);
}
});
router
.route("/:id")
.patch(async (req, res, next) => {
try {
const result = await Comment.update(
{
comment: req.body.comment,
},
{
where: { id: req.params.id },
}
);
res.json(result);
} catch (err) {
console.error(err);
next(err);
}
})
.delete(async (req, res, next) => {
try {
const result = await Comment.destroy({ where: { 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 라우터는 댓글을 생성하는 라우터입니다. commenter 속성에 사용자 아이디를 넣어 사용자와 댓글을 연결합니다.
PATCH /comments/:id와 DELETE /comments/:id 라우터는 각각 댓글을 수정, 삭제하는 라우터입니다. 수정과 삭제에는 각각 update와 destroy 메서드를 사용합니다. 쿼리가 기억나지 않는다면 7.6.4절을 복습하세요.
이제 npm start로 서버를 실행하고 http://localhost:3001로 접속합니다. 콘솔에는 시퀄라이즈가 수행하는 SQL문이 나오므로 어떤 동작을 하는지 확인할 수 있습니다.
Executing (default): SELECT `id`, `name`, `age`, `married`, `comment`, `created_at` FROM `users` AS `users`;
// 이하 생략
Executing으로 시작하는 SQL 구문을 보고 싶지 않다면 config/config.json의 dialect 속성 밑에 "logging": false를 추가하면 됩니다.
접속 시 GET / 라우터에서 User.findAll 메서드를 호출하므로 그에 따른 SQL문이 실행되는 모습입니다.

사용자의 이름을 누르면 사용자가 등록한 댓글이 나옵니다.

사용자의 아이디와 댓글의 아이디가 둘 다 1이지만 아무런 관련이 없습니다. 그저 첫 번째로 등록된 사용자, 첫 번째로 등록된 댓글이라는 뜻입니다.
사용자와 댓글을 몇 개 더 등록해보겠습니다. 첫 번째 댓글은 수정도 해봤습니다.

