메인 콘텐츠로 건너뛰기

VillageSQL is a drop-in replacement for MySQL with extensions.

All examples on this page work on VillageSQL. Install Now →
Drizzle ORM은 스키마 파일에서 SQL 마이그레이션을 생성하고 타입이 지정된 쿼리 API를 제공하는 TypeScript 쿼리 빌더이며, 자체 중간 쿼리 언어가 없습니다. 이 가이드는 Drizzle과 mysql2 드라이버를 통해 Next.js App Router 애플리케이션을 MySQL에 연결하며, 스키마, 마이그레이션, Server Component에서의 읽기, Server Action에서의 쓰기를 다룹니다. create-next-app --src-dir로 스캐폴딩한 기존 앱을 전제로 하며, 이 옵션이 코드 예제가 사용하는 @/* 임포트 별칭을 설정합니다.

Drizzle과 MySQL 드라이버 설치

Drizzle은 MySQL과 통신하기 위해 mysql2 드라이버가 필요하고, 마이그레이션을 생성하고 실행하기 위해 drizzle-kit이 필요합니다:
drizzle-kit은 빠르게 변하며, latest와 함께 dist-tags 아래에 사전 릴리스 태그(beta, rc)를 게시해 왔습니다. 설치하기 전에 npm view drizzle-orm dist-tags와 npm view drizzle-kit dist-tags를 확인해 latest가 릴리스 후보가 아니라 실제로 안정 릴리스를 가리키는지 확인하세요. .env에 연결 문자열을 설정하세요:

데이터베이스 사용자 만들기

애플리케이션을 root로 연결하는 대신, 전용 데이터베이스와 그 범위로 제한된 사용자를 만드세요:
Unix 소켓을 통한 연결은 어떤 IP 기반 권한이 있든 항상 'localhost' 호스트와 대조됩니다. 'drizzle_app'@'127.0.0.1'로만 만든 사용자는 소켓을 통해 인증할 수 없습니다. 127.0.0.1로의 TCP 연결은 역방향 DNS 조회로 대조되므로, 그 연결이 'localhost' 권한도 만족하는지는 고정된 MySQL 규칙이 아니라 서버가 127.0.0.1을 어떻게 해석하느냐에 달려 있습니다. 애플리케이션이 실제로 사용하는 호스트에 권한을 부여하고, 127.0.0.1과 localhost가 서로 바꿔 쓸 수 있다고 믿지 마세요.

스키마 정의

Drizzle의 MySQL 스키마는 drizzle-orm/mysql-core의 함수를 사용해 각 열을 기술합니다:
src/db/schema.ts
varchar는 명시적인 length가 필요합니다. MySQL은 PostgreSQL과 달리 열 타입에 최대 길이를 박아 넣은 채로 VARCHAR를 저장하므로, Drizzle에는 의지할 기본값이 없고 값을 미리 받아야 합니다.

drizzle-kit 설정

drizzle-kit은 스키마가 어디에 있고 데이터베이스에 어떻게 접근하는지 알기 위해 설정 파일을 읽습니다:
drizzle.config.ts
drizzle-kit은 설정 파일이 실행되기 전에 스스로 .env를 읽으므로, 여기서는 dotenv 임포트 없이도 process.env.DATABASE_URL이 이미 채워져 있습니다. 이것은 drizzle-kit에만 해당합니다: 이 가이드 뒷부분의 검증 스크립트가 보여주듯이, 일반 Node나 tsx 스크립트에는 그런 로더가 내장되어 있지 않습니다. package.json에 마이그레이션 스크립트를 추가하세요:
package.json

마이그레이션 생성 및 실행

drizzle-kit generate는 스키마 파일을 마지막으로 생성된 마이그레이션과 비교해 새 SQL 파일을 씁니다:
drizzle-kit은 설정과 스키마 경로를 절대 경로로 출력합니다. 위의 /path/to/your-app은 여러분의 프로젝트 디렉터리를 대신합니다. 생성된 파일 이름에는 무작위 두 단어 접미사가 포함되므로 여러분의 것은 다를 것입니다. 순서에 의미가 있는 것은 숫자 접두사(0000)뿐입니다. 스키마 변경 없이 db:generate를 다시 실행하면 두 번째 파일을 쓰는 대신 No schema changes, nothing to migrate를 보고합니다. 마이그레이션 자체는 일반 SQL입니다:
drizzle/0000_premium_tiger_shark.sql
적용하세요:
drizzle-kit migrate는 처음 실행될 때 어떤 마이그레이션 파일이 이미 적용되었는지 추적하기 위해 __drizzle_migrations 테이블을 만듭니다. 두 테이블이 모두 존재하는지 확인하세요:

데이터베이스 클라이언트 만들기

mysql2 드라이버는 풀을 통해 연결하고, Drizzle은 그 풀을 감쌉니다:
src/db/index.ts
mode: "default" 옵션은 mysql2 드라이버에만 있으며, Drizzle의 PostgreSQL이나 SQLite 드라이버에는 대응하는 것이 없습니다. 생략하면 조용한 기본값이 아니라 타입 오류입니다.

Server Component에서 게시물 읽기

Server Component는 중간에 API 라우트 없이 렌더링 중에 db.select()를 직접 호출할 수 있습니다:
src/app/page.tsx

Server Action으로 게시물 삽입

Server Action은 서버에서 실행되며 폼의 action prop에 직접 전달할 수 있습니다:
src/app/actions.ts
revalidatePath("/")는 모든 요청마다 렌더링되는 라우트에서도 필요합니다. 이것이 없으면 Server Action의 응답에 해당 액션을 호출한 페이지의 새로 고친 사본이 포함된다는 보장이 없습니다.

정적 렌더링 함정

npm run build는 애플리케이션을 컴파일하지만, 라우트별로 페이지를 빌드 시점에 한 번 사전 렌더링할 수 있는지, 아니면 모든 요청마다 렌더링해야 하는지도 결정합니다. 같은 라우트에 다른 동적 API(cookies(), headers(), searchParams 없음)가 없이 데이터베이스를 읽는 Server Component는 Next.js에 요청별 렌더링을 강제하는 아무것도 주지 않으므로, Next.js는 빌드 중에 페이지를 한 번 사전 렌더링하고 그 뒤로 모든 방문자에게 같은 HTML을 제공합니다. 이것은 재현 가능했습니다: page.tsx에서 export const dynamic = "force-dynamic";을 제거하고 다시 빌드하자 라우트가 정적으로 표시되었습니다:
그 빌드 뒤에 데이터베이스에 직접 삽입한 행은 빌드된 애플리케이션을 npm run start로 시작했을 때 나타나지 않았습니다. 페이지는 빌드 시점에 포착된 행 수와 내용을 계속 제공했습니다. page.tsx에 export const dynamic = "force-dynamic";을 추가하고 다시 빌드하자 라우트가 동적으로 바뀌었습니다:
그 변경 뒤에는 빌드 후에 삽입한 행이 다음 요청에 즉시 나타났고, 다시 빌드할 필요가 없었습니다. 데이터베이스를 읽고 빌드 뒤에 이루어진 쓰기를 반영해야 하는 모든 페이지에는 export const dynamic = "force-dynamic";이, 또는 요청별 렌더링을 강제하는 같은 효과를 가진 다른 Next.js API(cookies() 읽기 같은)가 필요합니다.

ORM과 독립적으로 쓰기 경로 검증

Drizzle을 통해 삽입하고 Drizzle 자체의 반환값만 확인하는 스크립트는 행이 데이터베이스에 도달했음을 증명하지 않습니다. 드라이버가 예외를 던지지 않고 반환했음을 증명할 뿐입니다. 별도의 쿼리로 검증하세요:
src/db/verify-insert.ts
TypeScript를 직접 실행하는 tsx로 실행하세요:
스크립트 맨 위의 import "dotenv/config"는 여기서 필수입니다. Next.js는 애플리케이션을 위해 .env를 자동으로 로드하지만, tsx로 실행하는 독립 스크립트는 프레임워크 연결이 없는 일반 Node 프로세스이고, Node 자체는 .env 파일을 읽지 않습니다. 그런 다음 애플리케이션을 전혀 거치지 않은 쿼리로 행이 존재하는지 확인하세요:

자주 묻는 질문

VillageSQL에서도 MySQL과 같은 방식으로 동작하나요?

예. Drizzle의 MySQL 방언과 mysql2 드라이버는 표준 MySQL 와이어 프로토콜을 사용하고, VillageSQL은 같은 프로토콜과 클라이언트 도구를 가진 MySQL의 드롭인 대체품입니다. 위의 스키마, 마이그레이션, 쿼리 코드 중 MySQL 버전에 특정한 것은 없습니다.

drizzle-kit generate와 migrate를 써야 하나요, 아니면 drizzle-kit push를 써야 하나요?

generate와 migrate는 데이터베이스에 대해 실행되기 전에 검토하고 커밋할 수 있는 SQL 마이그레이션 파일을 쓰며, 이 가이드가 사용하는 방식입니다. drizzle-kit push는 스키마 파일을 실제 데이터베이스와 비교해 마이그레이션 파일 없이 차이를 직접 적용합니다. push는 초기 프로토타이핑에서는 더 빠르지만 무엇이 언제 바뀌었는지 기록을 남기지 않으며, 다른 개발자나 환경이 스키마에 의존하게 되면 마이그레이션 파일을 대신할 수 없습니다.

마이그레이션 폴더에 왜 __drizzle_migrations라는 테이블이 있나요?

drizzle-kit migrate는 첫 실행 시 이 테이블을 만들어 이미 적용한 마이그레이션 파일을 해시로 기록합니다. migrate가 이미 실행된 마이그레이션을 두 번 적용하는 대신 건너뛰어야 함을 아는 방법입니다.

Server Action 대신 Server Component에서 db.insert()를 직접 호출할 수 있나요?

Server Component는 렌더링 중에만 실행됩니다. 사용자 동작에 반응해 쓰기를 촉발할 브라우저 요청이 없습니다. 쓰기 경로에는 Server Action, Route Handler, 또는 API 라우트를 호출하는 클라이언트 측 코드가 필요합니다.

문제 해결

함께 보기