Loading
Loading
블로그 개발 일지 #5 - API 제작
2024년 11월 18일
백엔드 관련 공부를 본격적으로 해본 적은 없었지만, 이전 팀 프로젝트에서 MongoDB Atlas를 활용해 간단한 API를 만든 경험이 있었습니다. 당시 부트캠프에서 제공된 API 외에 추가적인 기능을 구현하고 싶어서 MongoDB Atlas를 사용하여 진행했습니다.
이번 블로그 프로젝트에서는 직접 백엔드 작업을 하고 서버를 배포하기로 했습니다. 이를 위해 선택한 기술은 Prisma와 PostgreSQL입니다.
프로젝트에서 사용할 주요 데이터 모델을 정의했습니다. 모델은 User, Post, Comment의 세 가지로 구성됩니다.
User 모델은 사용자 정보를 관리하기 위해 설계되었습니다.
사용자는 게시물과 댓글의 주체로서 중요한 역할을 합니다. 이를 통해 사용자와 관련된 데이터(게시물, 댓글 등)를 체계적으로 관리할 수 있습니다.
model User { id String @id @default(uuid()) email String @unique name String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt posts Post[] comments Comment[] }
Post 모델은 블로그 게시물을 관리하기 위해 설계되었습니다.
게시물은 특정 사용자에 의해 작성되고, 댓글 및 다양한 속성과 연관됩니다.
model Post { id Int @id @default(autoincrement()) slug String @unique coverImg String? category String? title String content String tags String[] likes Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId String comments Comment[] }
Comment 모델은 게시물에 달린 댓글 및 대댓글을 관리하기 위해 설계되었습니다.
댓글은 특정 게시물 및 사용자를 기반으로 작성됩니다.
model Comment { id Int @id @default(autoincrement()) content String likes Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: SetDefault) userId String @default("anonymous") post Post @relation(fields: [postId], references: [id], onDelete: Cascade) postId Int parentComment Comment? @relation("CommentReplies", fields: [parentCommentId], references: [id], onDelete: SetNull) parentCommentId Int? replies Comment[] @relation("CommentReplies") @@index([parentCommentId]) }
PostgreSQL을 기반으로 각 모델 간의 관계를 설정하여 데이터베이스를 설계했습니다. 이를 통해 유저, 게시물, 댓글 간의 데이터 흐름을 직관적으로 관리할 수 있습니다.
테스트를 하기 전에 데이터베이스 구조를 점검하고 API의 동작을 검증하기 위해 목 데이터를 추가했습니다. 목 데이터는 ChatGPT를 활용해 생성했으며, Prisma의 시드 기능을 사용하여 데이터베이스에 삽입했습니다.
목 데이터를 생성하기 위해 다음과 같은 데이터를 준비했습니다.
Prisma를 활용하여 목 데이터를 삽입하기 위한 시드 스크립트를 작성했습니다.
import { PrismaClient } from "@prisma/client"; import { USER, COMMENT, POST } from "./mock.js"; const prisma = new PrismaClient(); async function main() { await prisma.comment.deleteMany(); await prisma.post.deleteMany(); await prisma.user.deleteMany(); await prisma.user.createMany({ data: USER, skipDuplicates: true }); await prisma.post.createMany({ data: POST, skipDuplicates: true }); await prisma.comment.createMany({ data: COMMENT, skipDuplicates: true }); } main() .then(async () => { await prisma.$disconnect(); }) .catch(async (e) => { console.error(e); await prisma.$disconnect(); process.exit(1); });
위 스크립트를 실행하기 위해 다음 명령어를 사용했습니다:
npx prisma db seed
API에서 클라이언트가 전달한 데이터를 검증하기 위해 Superstruct 라이브러리를 사용했습니다. 데이터의 스키마와 구조를 선언적으로 정의하여 API 개발의 안정성을 높였습니다.
import * as s from "superstruct"; import isUuid from "is-uuid"; const Uuid = s.define("Uuid", (value) => isUuid.v4(value)); export const CreateUser = s.object({ email: s.define("Email", isEmail), name: s.size(s.string(), 1, 20), }); export const UpdateUser = s.partial(CreateUser);
Superstruct를 통해 데이터를 효율적으로 검증하고, API의 신뢰성을 높였습니다.
비동기 핸들러에서 발생하는 예외를 중앙에서 처리하기 위해 asyncHandler 함수를 작성했습니다. 이를 통해 각 핸들러에서 반복적으로 try-catch 블록을 작성하지 않아도 되도록 하여 코드의 가독성과 유지보수성을 높였습니다.
function asyncHandler(handler) { return async function (req, res) { try { await handler(req, res); } catch (e) { if (e.name === "StructError" || e instanceof Prisma.PrismaClientValidationError) { res.status(400).send({ message: e.message ?? "Bad Request: Invalid input data." }); } else if (e instanceof Prisma.PrismaClientKnownRequestError) { switch (e.code) { case "P2025": res.status(404).send({ message: "Resource not found." }); break; case "P2002": res.status(409).send({ message: `Unique constraint failed on: ${e.meta?.target || "unknown field"}` }); break; default: res.status(400).send({ message: e.message ?? "Bad Request: Known error." }); } } else { res.status(500).send({ message: e.message ?? "Internal Server Error." }); } } }; }
asyncHandler
를 통해 에러 처리를 체계적으로 구현함으로써 API의 안정성과 유지보수성을 높였습니다.