직접 만들어보는 To Do List - Express.js + React.js + SQLite (4)
SQLite와 uuid 설치
백엔드 API 설정을 먼저 마무리합니다. 필요한 라이브러리를 설치합니다.
npm i sqlite3 uuid
- sqlite3 - SQLite 데이터 베이스 라이브러리
- uuid - 중복되지 않는 id를 생성하는 라이브러리
SQLite는 복잡한 구성 없이 파일 하나만으로도 구성이 가능하기 때문에 널리 사랑받습니다. 오픈 소스 데이터베이스이기 때문에 무료로 사용할 수 있습니다.
표준 SQL 문법을 지원하기 때문에 학습 목적으로도 나쁘지 않은 선택입니다.
SQLite를 사용하기 위해 db.js 파일을 아래와 같이 작성해줍니다.
// db.js
const sqlite3 = require("sqlite3").verbose()
const db = new sqlite3.Database("./todo.db")
// todos 테이블 생성
db.serialize(() => {
db.run(
"CREATE TABLE if not exists todos(id TEXT, title TEXT, completed BOOLEAN)",
)
})
module.exports = db
아래와 같이 3개의 필드를 가진 todos라는 테이블을 생성해줍니다.
- id
- title
- completed
id는 uuid 라이브러리를 통해 생성해 중복되지 않는 유일한 아이디를 갖게 됩니다. 이 아이디를 기반으로 수정하거나 삭제할 수 있습니다.
title은 할 일의 내용이 들어갈 부분입니다.
completed는 할 일의 완료 여부를 true , false로 저장합니다.
백엔드 구성
지금까지는 모든 내용을 Server.js에 작성했습니다. 그렇게 작업해도 작동하는데는 무리가 없지만, 소스 관리가 어려워지기 때문에 아래와 같이 폴더를 구성하겠습니다.
- routes - get, post, put, patch를 통해 HTTP 요청 담당
- controllers - route에서 들어온 요청을 처리 담당
- middleware - 에러 처리와 같은 다양한 처리를 담당
Controllers 작성
getTodos - 모든 데이터 가져오기
const { v4: uuidv4 } = require("uuid")
const db = require("../db")
const getTodos = (req, res) => {
// todos 테이블에서 모든 데이터를 가져옴
db.all("SELECT * FROM todos", (err, rows) => {
// 에러가 발생한 경우 에러 메시지를 반환
if (err) {
res.status(400)
throw new Error("todos 데이터를 가져올 수 없습니다")
}
// 가져온 todos 데이터와 빈 메시지를 응답으로 반환
res.status(200).json({ todos: rows, message: "" })
})
}
먼저 db.js와 uuid를 호출해 오는 것으로 시작합니다. 일단 getTodos 함수에서는 SELECT * FROM todos
을 통해서 모든 데이터를 호출해 오는 것으로 시작합니다. 에러가 발생했을 경우엔 throw new Error
를 이용해 에러 처리를 할 것인데, 추후에 errorMiddleware를 통해서 에러 처리를 할 것입니다.
성공했을 경우엔 json 형식으로 todos
데이터를 담아서 보내줍니다.
createTodos - 할 일을 생성
const createTodo = (req, res) => {
const { value, completed } = req.body // 요청 바디에서 value와 completed 추출
const id = uuidv4() // UUID 생성
// todos 테이블에 새로운 데이터 추가
db.run(
"INSERT INTO todos(id, title, completed) VALUES(?,?,?)", // SQL 쿼리문
[id, value, false], // 쿼리에 전달할 파라미터
(err) => {
// 에러 발생시 에러메세지 반환
if (err) {
res.status(400)
throw new Error("todos 데이터를 추가할 수 없습니다")
}
// 응답으로 전송할 데이터와 메세지 반환
res.status(201).json({
todos: { id, title: value, completed },
message: `${value} To Do 추가`,
})
},
)
}
request
를 통해 들어온 요청 중에 value
와 completed
라는 값을 받아올 것입니다. 이 요청을 uuid
로 생성된 unique ID
와 더불어 DB
에 저장해줍니다.
저장하고난 결과를 받아서 todos
에 담아서 응답해줍니다.
updateTodo - 할 일 내용을 변경
const updateTodo = (req, res) => {
const { id } = req.params // 요청 파라미터에서 id 추출
const { value } = req.body // 요청 바디에서 value 추출
// todos 테이블에서 해당 id의 title을 value로 업데이트
db.run("UPDATE todos SET title = ? WHERE id = ?", [value, id]).all(
"SELECT * FROM todos",
(err, rows) => {
if (err) {
res.status(400)
throw new Error("todos 데이터를 업데이트할 수 없습니다")
}
res.status(200).json({ todos: rows, message: `${value}로 수정 완료` }) // 수정 완료 메세지와 업데이트된 todos 목록 응답
},
)
}
request
를 통해 요청이 들어올때는 id
값이 전달되도록 할 것입니다. 이 아이디를 기반으로 데이터베이스에서 동일한 아이디를 갖고 있는 내용을 업데이트하는 것이 주된 작업입니다.
업데이트를 완료한 후에는 다시 SELECT * FROM todos
명령을 통해 업데이트 된 내용을 포함해서 전체 내용을 받아와 다시 응답해줍니다.
completeTodo - 완료 여부
const completeTodo = (req, res) => {
// 요청 파라미터와 요청 바디에서 필요한 정보를 추출합니다.
const { id } = req.params
const { completed } = req.body
// 할일 테이블의 해당 아이디의 완료 여부를 업데이트합니다.
db.run("UPDATE todos SET completed = ? WHERE id = ?", [completed, id]).all(
"SELECT * FROM todos",
(err, rows) => {
// 에러가 발생하면 에러 메시지를 응답합니다.
if (err) {
res.status(400)
throw new Error("todos 데이터를 업데이트할 수 없습니다")
}
// 업데이트된 할일 목록과 완료 여부에 따른 메시지를 응답합니다.
res
.status(200)
.json({ todos: rows, message: `${completed ? "완료" : "진행중"}` })
},
)
}
전체적인 맥락은 위의 updateTodo
와 크게 다르지 않습니다. 요청 들어온 id
를 기반으로 찾아 completed
값만 교체해준 후 다시 목록을 업데이트해서 응답해줍니다.
deleteTodo - 할 일 삭제
const deleteTodo = (req, res) => {
const { id } = req.params // 요청 파라미터에서 id 추출
// todos 테이블에서 해당 id의 할 일을 삭제
db.run("DELETE FROM todos WHERE id = ?", [id]).all(
"SELECT * FROM todos",
(err, rows) => {
// 에러 발생시 에러메세지 응답
if (err) {
res.status(400)
throw new Error("todos 데이터를 가져올 수 없습니다")
}
// 삭제된 할 일 목록과 삭제 성공 메세지 응답
res.status(200).json({ todos: rows, message: `삭제 성공` })
},
)
}
필요에 따라서 할 일을 삭제해야 할 경우도 있습니다. 그래서 마찬가지로 요청 받은 id
를 기반으로 삭제한 후 다시 목록을 응답해줍니다.
전체적인 흐름은 위와 같고, 모든 소스를 정리하면 아래와 같습니다.
// controllers/todoControllers.js
const { v4: uuidv4 } = require("uuid")
const db = require("../db")
/**
* @description todos를 가져오는 함수
* @param {Object} req - 요청 객체
* @param {Object} res - 응답 객체
*/
const getTodos = (req, res) => {
// todos 테이블에서 모든 데이터를 가져옴
db.all("SELECT * FROM todos", (err, rows) => {
// 에러가 발생한 경우 에러 메시지를 반환
if (err) {
res.status(400)
throw new Error("todos 데이터를 가져올 수 없습니다")
}
// 가져온 todos 데이터와 빈 메시지를 응답으로 반환
res.status(200).json({ todos: rows, message: "" })
})
}
/**
* 할 일을 생성하는 함수입니다.
* @param {*} req - 요청 객체
* @param {*} res - 응답 객체
*/
const createTodo = (req, res) => {
const { value, completed } = req.body // 요청 바디에서 value와 completed 추출
const id = uuidv4() // UUID 생성
// todos 테이블에 새로운 데이터 추가
db.run(
"INSERT INTO todos(id, title, completed) VALUES(?,?,?)", // SQL 쿼리문
[id, value, false], // 쿼리에 전달할 파라미터
(err) => {
// 에러 발생시 에러메세지 반환
if (err) {
res.status(400)
throw new Error("todos 데이터를 추가할 수 없습니다")
}
// 응답으로 전송할 데이터와 메세지 반환
res.status(201).json({
todos: { id, title: value, completed },
message: `${value} To Do 추가`,
})
},
)
}
/**
* 할 일을 업데이트하는 함수
* @param {object} req - 요청 객체
* @param {object} res - 응답 객체
*/
const updateTodo = (req, res) => {
const { id } = req.params // 요청 파라미터에서 id 추출
const { value } = req.body // 요청 바디에서 value 추출
// todos 테이블에서 해당 id의 title을 value로 업데이트
db.run("UPDATE todos SET title = ? WHERE id = ?", [value, id]).all(
"SELECT * FROM todos",
(err, rows) => {
if (err) {
res.status(400)
throw new Error("todos 데이터를 업데이트할 수 없습니다")
}
res.status(200).json({ todos: rows, message: `${value}로 수정 완료` }) // 수정 완료 메세지와 업데이트된 todos 목록 응답
},
)
}
/**
* 완료된 할일을 업데이트하는 함수입니다.
*
* @param {Object} req - 요청 객체
* @param {Object} req.params - 요청 파라미터 객체
* @param {string} req.params.id - 할일 아이디
* @param {Object} req.body - 요청 바디 객체
* @param {boolean} req.body.completed - 할일 완료 여부
* @param {Object} res - 응답 객체
*/
const completeTodo = (req, res) => {
// 요청 파라미터와 요청 바디에서 필요한 정보를 추출합니다.
const { id } = req.params
const { completed } = req.body
// 할일 테이블의 해당 아이디의 완료 여부를 업데이트합니다.
db.run("UPDATE todos SET completed = ? WHERE id = ?", [completed, id]).all(
"SELECT * FROM todos",
(err, rows) => {
// 에러가 발생하면 에러 메시지를 응답합니다.
if (err) {
res.status(400)
throw new Error("todos 데이터를 업데이트할 수 없습니다")
}
// 업데이트된 할일 목록과 완료 여부에 따른 메시지를 응답합니다.
res
.status(200)
.json({ todos: rows, message: `${completed ? "완료" : "진행중"}` })
},
)
}
/**
* 할 일 삭제 함수
* @param {Object} req - 요청 객체
* @param {Object} res - 응답 객체
*/
const deleteTodo = (req, res) => {
const { id } = req.params // 요청 파라미터에서 id 추출
// todos 테이블에서 해당 id의 할 일을 삭제
db.run("DELETE FROM todos WHERE id = ?", [id]).all(
"SELECT * FROM todos",
(err, rows) => {
// 에러 발생시 에러메세지 응답
if (err) {
res.status(400)
throw new Error("todos 데이터를 가져올 수 없습니다")
}
// 삭제된 할 일 목록과 삭제 성공 메세지 응답
res.status(200).json({ todos: rows, message: `삭제 성공` })
},
)
}
module.exports = {
getTodos,
createTodo,
completeTodo,
deleteTodo,
updateTodo,
}
마지막 부분에는 module.exports
를 통해서 각 함수를 다른 파일에서도 사용할 수 있도록 명시해줍니다.
routes/todoRoutes.js
const express = require("express")
const router = express.Router()
const {
getTodos,
createTodo,
completeTodo,
deleteTodo,
updateTodo,
} = require("../controllers/todosController")
// 전체 할 일 조회
router.get("/todos", getTodos)
// 새로운 할 일 추가
router.post("/todos/create", createTodo)
// 개별 할 일 수정
router.put("/todos/:id", updateTodo)
// 개별 할 일 토글
router.patch("/todos/:id", completeTodo)
// 개별 할 일 삭제
router.delete("/todos/:id", deleteTodo)
module.exports = router
todoControllers.js
파일에서 주된 작업들을 모두 작성해주었기 때문에 todoRoutes.js
파일은 상대적으로 작성해줄 내용이 적습니다.
get, post,put,patch, delete 요청에 따라 controllers
에서 가져온 함수들을 연결해주는 것으로 끝입니다. 역시 마찬가지로 마지막에 router
를 export
해줍니다.
errorMiddlerware.js
/**
* 에러 핸들러 함수
* @param {Error} err - 발생한 에러 객체
* @param {Object} req - 요청 객체
* @param {Object} res - 응답 객체
* @param {Function} next - 다음 미들웨어 함수
*/
const errorHandler = (err, req, res, next) => {
// 응답 상태 코드를 설정한다. 상태 코드가 없을 경우 500을 사용한다.
const statusCode = res.statusCode ? res.statusCode : 500
// 응답 상태 코드를 설정한다.
res.status(statusCode)
// 에러 메시지와 스택 정보를 응답으로 전송한다.
res.json({
message: err.message,
stack: process.env.NODE_ENV === "production" ? null : err.stack,
})
}
module.exports = { errorHandler }
에러가 발생했을 경우에 에러 처리를 담당하는 미들웨어
입니다. 해당 내용은 Brad Traversy 의 소스를 사용했습니다.
server.js
const express = require("express")
const cors = require("cors")
const { errorHandler } = require("./middleware/errorMiddleware")
const app = express()
const PORT = 5000
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use(cors({ origin: "http://localhost:5173" }))
app.use("/api", require("./routes/todosRoutes"))
// app.get("*", (req, res) => {
// res.json({ message: "To Do API" })
// })
app.use(errorHandler)
app.listen(PORT, () => {
console.log(`http://localhost:${PORT} 에서 서버 실행중`)
})
server.js
에서는 전반적으로 작성된 내용들을 전부 연결해주는 역할만 담당하면 됩니다. 상대적으로 파일이 매우 가벼워졌다는 것을 알 수 있습니다.
api
라우트로 들어오는 요청은 todosRoutes.js
로 연결 될 것이고, api/todos
를 통해서 POST
, PUT
, PATCH
, GET
, DELETE
요청이 들어올때, 각각 알맞은 controller
함수들과 연결되도록 구성해준 내용들입니다.
다음 포스팅에서는 REACT
를 이용해 요청을 하고 응답을 받는 프론트엔드 소스를 작성하겠습니다.