개발/Next.js

Next.js 프로젝트에 Drizzle ORM 도입하기 (feat. MySQL)

dev_jiwonpark 2025. 11. 4. 21:20

이미지 최적화를 공부하면서 인프라를 공부해야겠다는 다짐을 했다.

하지만 단순히 인프라를 공부하는 것 보단 실습을 하면서 체득하는 것이 더 좋을 것 같아.

간단한 사이드 프로젝트를 만들기로 다짐했다. 이미지를 최대한 많이 다룰 수 있는 앱을 생각해보니 SNS 앱밖에 떠오르지 않아서..
Next.js 로 SNS 앱을 개발하려고 한다. 동시에 데이터베이스 관리에 Drizzle ORM을 도입했다.
이 글에서는 직접 SQL을 작성하는 방식과 Drizzle을 사용하는 방식을 비교하며 실제로 어떤 문제들을 해결할 수 있었는지 기록해보려고 한다.


ORM이란?

ORM(Object-Relational Mapping)은 객체 지향 프로그래밍 언어와 관계형 데이터베이스 사이의 데이터를 변환해주는 도구이다.

쉽게 말해, SQL을 직접 작성하지 않고 코드로 데이터베이스를 다룰 수 있게 해주는 것 이다.

 

코드로 비교해보자면 다음과 같다.

만약 ORM 없이 직접 SQL을 작성한다면?

import mysql from 'mysql2/promise';

// 1. DB 연결 생성
const connection = await mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'mydb'
});

// 2. SQL 쿼리 문자열 직접 작성
const [rows] = await connection.query(
  "SELECT * FROM users WHERE email = ?",  // SQL 문자열
  [email]  // 파라미터 배열
);

// 3. 결과가 any 타입 - 타입 안정성 없음
const user = rows[0];  // user: any
console.log(user.email);  // 오타 있어도 컴파일 에러 안남
console.log(user.emial);  // 런타임에서야 에러가 발견된다.

// 4. 연결 종료 잊으면 메모리 누수
await connection.end();

 

위의 문제점은 아래와 같다.

1. SQL문을 문자열로 작성했기 때문에 오타가 있어도 실행 전까지는 모른다.

2. 결과값이 any 타입이기 때문에 Typescript의 타입 체크가 무용지물이다.(확인)

3. 매번 연결 생성/ 종료를 관리해줘야 한다. 만약 종료를 잊으면 메모리 누수가 발생한다.

 

하지만 ORM을 사용한다면? (Drizzle)

import { db } from '@/lib/db';
import { users } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';

// 1. 타입이 자동으로 추론됨
const user = await db.query.users.findFirst({
  where: eq(users.email, email)  
});

// 2. schema 설정으로 user의 타입이 명확함
// user: { id: string; email: string; name: string; ... } | undefined

// 3. IDE가 속성을 자동완성해줌
console.log(user.email);   // 자동완성 지원
console.log(user.emial);   // 컴파일 시점에 에러가 발생한다.
                          //  -> error : Property 'emial' does not exist

 

직접 SQL문을 작성하는것과는 다르게 아래와 같은 장점들이 있다.

 

1. SQL을 직접 작성하지 않고 사용하는 언어로 데이터베이스에 접근할 수 있다.

2. 객체지향적으로 코드를 작성할 수 있기 때문에 비즈니스 로직에만 집중할 수 있다.

3. 데이터베이스 시스템이 추상화되어 있어 종속성이 낮다.

-> MySQL을 쓰다가 PostgreSQL로 바꿔도 코드 변경이 거의 없습니다. 스키마 정의와 설정 파일만 수정하면 된다.

4. 스키마가 코드로 정의되어 있어서, 별도의 ERD 문서 없이도 데이터 구조를 파악할 수 있다.

5. 자동으로 타입을 추론하여 타입 안정성을 보장한다.

6. 연결 관리가 자동화된다.

 

하지만 프로젝트의 복잡성이 커질수록 사용 난이도도 올라간다.

간단한 CRUD는 쉽지만, 복잡한 조인이나 서브쿼리가 필요할 때는 ORM 문법이 오히려 더 복잡할 수 있다.

또는 복잡하고 무거운 쿼리는 ORM으로 해결이 불가능한 경우가 있다.

매우 최적화된 성능이 필요하거나, 데이터베이스 특화 기능을 써야 할 때는 Raw SQL을 직접 작성해야 할 수도 있다.


 

https://orm.drizzle.team/docs/get-started/mysql-new

 

Drizzle ORM - MySQL

Drizzle ORM is a lightweight and performant TypeScript ORM with developer experience in mind.

orm.drizzle.team

 

실제로 위 공식문서를 참고해 프로젝트에 Drizzle을 어떻게 적용했는지를 작성해보려고 한다.
우선 기본적인 파일 구조는 아래와 같다.

 

1단계: 프로젝트 환경 설정

1.1 의존성 설치

# Drizzle ORM과 MySQL 드라이버 설치
npm install drizzle-orm mysql2

# Drizzle Kit (마이그레이션 도구) 설치
npm install -D drizzle-kit

# 환경변수 로딩용 (마이그레이션에 필요)
npm install dotenv

 

1.2 환경변수 설정

.env.local 파일에 데이터베이스 연결 정보 추가

# .env.local
DB_HOST=your-database-host
DB_PORT=3306
DB_USER=admin
DB_PASSWORD=your-password
DB_NAME=mydb

 

2단계: Drizzle 설정 파일 생성

프로젝트 루트에 drizzle.config.ts 생성

// drizzle.config.ts
import type { Config } from "drizzle-kit";
import { config } from "dotenv";

// .env.local 파일 로드
config({ path: ".env.local" });

export default {
  schema: "./src/lib/db/schema.ts",  // 스키마 파일 위치
  out: "./drizzle",                   // 마이그레이션 파일 저장 위치
  dialect: "mysql",                   // 사용할 데이터베이스
  dbCredentials: {
    host: process.env.DB_HOST!,
    port: Number(process.env.DB_PORT),
    user: process.env.DB_USER!,
    password: process.env.DB_PASSWORD!,
    database: process.env.DB_NAME!,
  },
} satisfies Config;

공식문서에선 database url 형식으로 아래와 같이 한줄로 작성되어 있다.

DATABASE_URL="mysql://admin:your-password@your-host:3306/mydb"

 

3단계: 스키마 정의

3.1 User 테이블 스키마

// lib/db/schema.ts
import { mysqlTable, varchar, text, timestamp } from "drizzle-orm/mysql-core";

export const users = mysqlTable("users", {
  id: varchar("id", { length: 36 }).primaryKey().$defaultFn(() => crypto.randomUUID()),
  email: varchar("email", { length: 255 }).notNull().unique(),
  username: varchar("username", { length: 50 }).notNull().unique(),
  password: varchar("password", { length: 255 }),
  name: varchar("name", { length: 100 }).notNull(),
  bio: text("bio"),
  profileImage: varchar("profile_image", { length: 500 }),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

// TypeScript 타입 자동 추론
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;

3.2 Posts 테이블 추가

같은 파일에 Posts 테이블 추가

import { mysqlTable, varchar, text, timestamp, json } from "drizzle-orm/mysql-core";

// ... users 테이블 정의

export const posts = mysqlTable("posts", {
  id: varchar("id", { length: 36 }).primaryKey().$defaultFn(() => crypto.randomUUID()),
  userId: varchar("user_id", { length: 36 }).notNull(),
  content: text("content").notNull(),
  images: json("images").$type<string[]>(), // 이미지 URL 배열
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;

 

4단계: 데이터베이스 연결 설정

lib/db/index.ts 파일 생성

// lib/db/index.ts
import { drizzle } from "drizzle-orm/mysql2";
import mysql from "mysql2/promise";
import * as schema from "./schema";

// MySQL 연결 풀 생성
const connection = mysql.createPool({
  host: process.env.DB_HOST!,
  port: Number(process.env.DB_PORT),
  user: process.env.DB_USER!,
  password: process.env.DB_PASSWORD!,
  database: process.env.DB_NAME!,
});

// Drizzle 인스턴스 생성
export const db = drizzle(connection, { 
  schema, 
  mode: "default" 
});

 

5단계: 마이그레이션 실행

5.1 마이그레이션 파일 생성

Drizzle Kit은 두 가지 마이그레이션 방식을 제공한다.

방법 1: Push 명령어 (빠른 개발용)

로컬 개발 환경에서 빠르게 테스트할 때 사용

npx drizzle-kit push

 

특징

1. 마이그레이션 파일 생성 없이 직접 DB에 반영

2. 스키마 변경사항을 즉시 확인 가능

3. 로컬 개발용으로 적합

4. 변경 이력이 파일로 남지 않음

 

방법 2: Generate + Migrate (프로덕션용)

프로덕션 환경이나 팀 협업에서 사용

 

1단계: 마이그레이션 파일 생성

npx drizzle-kit generate

이 명령어는 `/drizzle` 폴더에 SQL 파일을 생성 한다.

 

2단계: DB에 적용

npx drizzle-kit migrate
 

Next.js 프로젝트에 Drizzle ORM을 도입하면서 데이터베이스 작업 방식이 크게 개선된걸 느낄 수 있었다.

예전에 한창 백엔드 작업을 했을때는 쌩으로 SQL 문자열을 작성하면서 DB 관리를 했었는데 

확실해 ORM을 도입해 작업을 하니 문자열 오타로 인한 시간 낭비를 많이 줄일 수 있었다.

그렇기 때문에 다른 로직 작업에 더 시간을 투자할 수 있어 좋았다.

실제로 타입 안정성이 보장된 쿼리를 작성하다 보니 개발 효율성이 많이 증가한걸 느꼈다. 

이제 기본적인 User와 Post 테이블을 구축했으니 다른 테이블들도 타입 안전하게 빠르게 구현할수 있을 것 같다. 

 

[참고]

https://orm.drizzle.team/docs/get-started/mysql-new

 

Drizzle ORM - MySQL

Drizzle ORM is a lightweight and performant TypeScript ORM with developer experience in mind.

orm.drizzle.team