Drizzle ORM과 PostgreSQL을 활용한 Next.js 15 풀스택 앱 만들기
2025-03-17 00:14:19Next.js 15와 Drizzle ORM으로 시작하는 풀스택 웹 개발
현대 웹 개발에서 강력한 백엔드와 반응형 프런트엔드를 통합하는 것은 확장 가능하고 강력한 애플리케이션 구축을 위해 필수적입니다. Next.js 같은 프레임워크는 서버 컴포넌트를 사용하여 다이나믹한 풀스택 애플리케이션을 쉽게 만들 수 있도록 지원하고 있습니다. 어떤 다이나믹한 애플리케이션도 확장 가능하고 강력한 데이터베이스 없이는 작동할 수 없습니다. 비록 MongoDB와 같은 NoSQL 데이터베이스가 널리 사용되고 있지만, 많은 산업에서는 여전히 SQL 데이터베이스를 고수하고 있습니다. 이번 가이드에서는 Drizzle ORM을 사용한 데이터베이스 상호 작용, PostgreSQL을 통한 관계형 데이터 저장, 그리고 Docker를 통한 환경 일관성을 위해 풀스택 Next.js 애플리케이션 설정 방법을 탐구할 것입니다.
프로젝트 개요
이번 튜토리얼에서는 사용자가 다음을 수행할 수 있는 간단한 작업 관리자 애플리케이션을 만들 것입니다:
- 작업의 생성, 읽기, 업데이트 및 삭제
- PostgreSQL을 사용한 데이터 보존
- Drizzle ORM을 사용한 데이터베이스 쿼리 간소화
- Docker를 통한 원활한 개발 및 배포
사전 준비 사항
이 프로젝트를 시작하려면 다음이 필요합니다:
- Node.js(v18 이상)
- Docker
- IDE(예: VS Code)
그리고 TypeScript의 기본적인 지식이 필요합니다.
1단계: Next.js 프로젝트 설정
Next.js 프로젝트를 설정하기 위해 다음 명령을 실행합니다:
npx create-next-app@latest next-drizzle-docker --use-pnpm
pnpm을 패키지 관리자로 사용할 것입니다. 설치 중 TypeScript와 App Router를 선택해야 합니다. 프로젝트 디렉토리로 이동하여 의존성을 설치합니다:
cd next-drizzle-docker
pnpm install
추가적인 의존성을 설치합니다:
pnpm add postgres drizzle-orm drizzle-kit
2단계: Docker Compose로 PostgreSQL 구성
PostgreSQL 데이터베이스를 설정하기 위해 루트 디렉토리에 docker-compose.yaml 파일을 생성합니다:
version: '3.8'
services:
postgres:
image: postgres:15
container_name: next_postgres
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: admin
POSTGRES_DB: tasks_db
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
다음 명령으로 PostgreSQL 서비스를 시작합니다:
docker-compose up -d
데이터베이스가 실행 중임을 확인합니다:
docker ps
3단계: Drizzle ORM 설정
Drizzle ORM 초기화
프로젝트 루트에 drizzle.config.ts 파일을 생성합니다:
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
out: './drizzle',
schema: './db/schema.ts',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL || "postgres://admin:admin@localhost:5432/your_db",
},
});
package.json 파일에서 마이그레이션을 수행하기 위한 새로운 스크립트를 생성합니다:
"scripts": {
// other required scripts
"migrate": "drizzle-kit generate && drizzle-kit push"
}
PostgreSQL 인스턴스 생성
프로젝트의 루트에 config 폴더와 db 서브폴더를 만듭니다. db 서브폴더에 index.ts 파일을 생성하고 PostgreSQL 인스턴스를 만듭니다:
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
// Dev environment solve for "sorry, too many clients already"
declare global {
// eslint-disable-next-line no-var -- only var works here
var db: PostgresJsDatabase | undefined;
}
let db: PostgresJsDatabase;
if (process.env.NODE_ENV === "production") {
db = drizzle(postgres(`${process.env.DATABASE_URL}`));
} else {
if (!global.db) global.db = drizzle(postgres(`${process.env.DATABASE_URL}`));
db = global.db;
}
export { db };
데이터베이스 스키마 생성
프로젝트 디렉토리의 루트에 db/schema.ts 파일을 생성하여 데이터 스키마를 저장합니다:
import { pgTable, serial, text, boolean } from "drizzle-orm/pg-core";
export const tasks = pgTable("tasks_table", {
id: serial("id").primaryKey(),
title: text().notNull(),
description: text().notNull(),
completed: boolean().default(false),
});
4단계: 백엔드 로직 작성
데이터베이스 작업은 모두 repositories 폴더 내에서 작성합니다:
import { db } from "@/config/db";
import { tasks } from "@/config/db/schema";
import { determinePagination } from "@/lib/utils";
import { count, eq, sql } from "drizzle-orm";
const taskReponseBody = {
id: tasks.id,
title: tasks.title,
description: tasks.description,
completed: tasks.completed
};
// create prepared statements for optimized queries
const allTasksCount = db.select({ total: count() })
.from(tasks).prepare("all_tasks_count");
const allTasksQuery = db.select(taskReponseBody)
.from(tasks)
.limit(sql.placeholder('size'))
.offset((Number(sql.placeholder('page')) - 1) * Number(sql.placeholder('size')))
.prepare("all_tasks");
const getAllTasks = async (page = 1, size = 10) => {
try {
const [totalResult, data] = await Promise.all([
allTasksCount.execute(),
allTasksQuery.execute({ page, size }),
]);
const total = totalResult[0].total;
return {
total,
data,
...determinePagination(total, page, size)
};
} catch (error: unknown) {
if (error instanceof Error) throw new Error(error.message);
}
};
determinePagination 함수는 프론트엔드와의 페이지네이션 상태 소통에 도움을 줍니다:
function determinePagination(total: number, page: number, page_size: number) {
if (total <= 0 || page <= 0) {
// No pages available if total is 0 or negative or page number is invalid
return { hasNextPage: false, hasPrevPage: false };
}
const totalPages = Math.ceil(total / page_size); // Total number of pages
const hasPrevPage = page > 1 && page <= totalPages;
const hasNextPage = page < totalPages;
return { hasNextPage, hasPrevPage };
}
유사하게, 작업을 생성, 업데이트, 삭제하는 함수를 작성할 수 있습니다:
const createNewTask = async (data: typeof tasks.$inferInsert) => {
if (!data.title) throw new Error("Title is required");
const createdTask = await db.insert(tasks).values({
title: data.title,
description: data.description,
completed: data.completed
}).returning();
return createdTask;
};
const deleteTask = async (id: number) => {
const deletedTask = await db.delete(tasks).where(eq(tasks.id, id)).returning();
return deletedTask;
};
type UpdateTaskType = {
id: number,
title?: string,
description?: string,
completed?: boolean
}
const updateTask = async (data: UpdateTaskType) => {
if (!data.id) throw new Error("Task id is required");
const updatedTask = await db.update(tasks).set(data).where(eq(tasks.id, data.id)).returning();
return updatedTask;
};
5단계: 프론트엔드와 통신하는 API 작성
app 디렉토리에 새로운 폴더 api 및 서브폴더 tasks를 생성하여 route 파일들을 만듭니다:
app/
api/
tasks/
[taskId]/
route.ts
route.ts
tasks/route.ts 파일에서는 모든 작업을 나열하고 새로운 작업을 생성하는 코드를 작성할 것입니다:
import { NextRequest, NextResponse } from "next/server";
import { createNewTask, getAllTasks } from "@/repositories/tasks.repositories";
// GET tasks
export async function GET(req: NextRequest) {
const searchParams = req.nextUrl.searchParams;
try {
const page = Number(searchParams.get('page')) || 1;
const size = Number(searchParams.get('size')) || 10;
const { total, data, hasNextPage, hasPrevPage } = await getAllTasks(page, size);
return NextResponse.json({
total,
data,
hasNextPage,
hasPrevPage
});
} catch (error) {
return NextResponse.error();
}
}
// POST task
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const createdTask = await createNewTask(body);
return NextResponse.json(createdTask);
} catch (error) {
return NextResponse.error();
}
}
이 가이드는 여러분이 Drizzle ORM과 PostgreSQL을 활용하여 어떻게 Next.js 15 풀스택 앱을 구축할 수 있는지를 보여줍니다. 전체적인 설정 과정과 API를 통해 확장 가능하고 효율적인 애플리케이션을 개발해보세요.
참고하시기 좋은 자료 링크
- Next.js 공식 문서: https://nextjs.org/docs
- PostgreSQL 공식 문서: https://www.postgresql.org/docs
- Docker 공식 문서: https://docs.docker.com
- pnpm 공식 문서: https://pnpm.io