직접 만들어보는 To Do List - Express.js + React.js + SQLite (4)

SQLite를 이용해 express에서 CRUD를 구현하는 API 서버를 작성합니다. api/todos 를 통해서 들어오는 get,post,put,patch,delete 요청을 받고, 그에 알맞은 값을 응답하는 백엔드 소스를 작성합니다.

직접 만들어보는 To Do List - Express.js + React.js + SQLite (4)
Photo by Mateusz Haberny / Unsplash
  1. Express.js 설치
  2. Express.js - Route 설정
  3. React 설치
  4. Backend API
  5. Frontend - React

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를 통해 들어온 요청 중에 valuecompleted 라는 값을 받아올 것입니다. 이 요청을 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 에서 가져온 함수들을 연결해주는 것으로 끝입니다. 역시 마찬가지로 마지막에 routerexport 해줍니다.

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 를 이용해 요청을 하고 응답을 받는 프론트엔드 소스를 작성하겠습니다.