Expo와 Drizzle을 활용해 React Native 앱에서 데이터베이스를 설정하고 사용하는 방법을 단계별로 안내합니다. 간단한 북마크 기능 구현 예제를 통해 Drizzle ORM의 기본 사용법을 익힐 수 있습니다.
`drizzle orm`은 TypeScript 지원과 간편한 설정으로 유명한 현대적인 ORM(Object-Relational Mapping) 라이브러리입니다.
Drizzle을 사용하면 데이터베이스 쿼리를 타입 안전하게 작성할 수 있어, 런타임 오류를 줄이고 개발 생산성을 높일 수 있습니다.
요즘 해외에서도 인기 있는 ORM 중 하나로, 특히 TypeScript와의 뛰어난 호환성 덕분에 많은 개발자들이 선호하고 있습니다.
다음은 최신 npm trends에서 Drizzle ORM의 인기도를 확인할 수 있는 그래프입니다.

npm trends 지표 - 1년
1년 사이, 다운로드 순위 4위에서 2위까지 급상승한 모습을 볼 수 있습니다.
자주 사용하는 ORM인 Prisma, Sequelize, TypeORM과 비교해도 Drizzle ORM의 성장세가 눈에 띕니다.
node 서버 사용시 많이 사용되던 ORM들이 모바일 환경에서는 불가능 하거나 제약이 있지만,
Drizzle ORM은 React Native와 Expo 환경에서도 원활하게 작동하여 모바일 앱 개발자들에게도 큰 인기를 끌고 있습니다.
이제 Expo와 Drizzle을 사용해 React Native 앱에서 데이터베이스를 설정하고 사용하는 방법을 단계별로 알아보겠습니다.
먼저 Expo CLI를 사용해 새로운 Expo 프로젝트를 생성합니다.
bun create expo-app my-app
cd my-app프로젝트가 생성되면 필요한 패키지를 설치합니다.
데이터베이스는 expo-sqlite를 사용하고, Drizzle ORM과 관련 패키지를 설치합니다.
bunx expo install expo-sqlitebun add drizzle-ormbun add -D drizzle-kitexpo에서 drizzle을 사용하기 위해선 반드시 babel 플러그인 설치와 metro 설정이 필요합니다.
bun add babel-plugin-inline-import이제 루트 디렉토리에 babel.config.js 파일을 생성하거나 기존 파일을 열어 다음과 같이 설정합니다.
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
plugins: [
["inline-import", { extensions: [".sql"] }],
"react-native-reanimated/plugin",
],
};
};metro.config.js 파일을 생성하거나 기존 파일을 열어 다음과 같이 설정합니다.
const { getDefaultConfig } = require("expo/metro-config");
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
config.resolver.sourceExts.push("sql"); // <--- add this
module.exports = config;프로젝트 루트에 drizzle.config.ts 파일을 생성하고 다음과 같이 설정합니다.
import { Config, defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./db/schema", // 스키마 파일 경로
out: "./db/migrations", // 마이그레이션 파일 출력 경로
dialect: "sqlite", // 사용할 데이터베이스 종류
driver: "expo", // <--- very important
}) satisfies Config;디렉토리로 설정하면 파일을 분리하여 users.ts, posts.ts 등으로 나눌 수 있어 관리가 편리합니다.
db/schema 디렉토리를 생성하고, 그 안에 bookmark.table.ts 파일을 만들어 데이터베이스 스키마를 정의합니다.
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
export const bookmarkTable = sqliteTable("bookmark", {
id: text("id").primaryKey(),
slug: text("slug").notNull(),
title: text("title").notNull(),
description: text("description"),
domain: text("domain").notNull(),
});
export type Bookmark = typeof bookmarkTable.$inferSelect;sqliteTable 함수를 사용해 bookmark 테이블을 정의합니다.`Drizzle SQLite 테이블 정의 문서`
expo sqlite드라이버는 기본적으로TEXT,INTEGER,REAL,BLOB등 SQLite의 기본 데이터 타입을 지원합니다.
이제 간단한 기본 설정은 끝났습니다. 마이그레이션 파일을 생성해 데이터베이스에 테이블을 생성할 준비가 되었습니다.
터미널에서 다음 명령어를 실행해 마이그레이션 파일을 생성합니다.
bunx drizzle-kit generate이 명령어를 실행하면 db/migrations 디렉토리에 마이그레이션 파일이 생성됩니다.
만약 마이그레이션 파일 이름을 지정하고 싶다면, 다음과 같이 --name 옵션을 사용할 수 있습니다.
bunx drizzle-kit generate --name create_bookmark_table생성된 마이그레이션 파일을 열어 테이블 생성 SQL이 올바르게 작성되었는지 확인합니다.
CREATE TABLE `bookmark` (
`id` text PRIMARY KEY NOT NULL,
`slug` text NOT NULL,
`title` text NOT NULL,
`description` text,
`domain` text NOT NULL
);이제 앱에서 Drizzle ORM을 사용해 데이터베이스에 접근할 수 있습니다.
db/index.ts 파일을 생성해 Drizzle ORM 클라이언트를 설정합니다.
import * as SQLite from "expo-sqlite";
import { drizzle } from "drizzle-orm/expo-sqlite";
export const dbName = "db.db";
// LiveQuery를 쓸 거면 enableChangeListener: true 권장
const expo = SQLite.openDatabaseSync("db.db", { enableChangeListener: true });
export const db = drizzle(expo);expo-sqlite를 사용해 SQLite 데이터베이스를 엽니다.drizzle-orm/expo-sqlite의 drizzle 함수를 사용해 Drizzle 클라이언트를 생성합니다.다음은 _layout.tsx 파일에서 SQLiteProvider로 앱을 감싸야 합니다.
import { SQLiteProvider } from "drizzle-orm/expo-sqlite/react";
import { Stack } from "expo-router";
import { db, dbName } from "@/db";
import { migrations } from "@/db/migrations";
import { useMigrations } from "drizzle-orm/expo-sqlite/migrator";
import migrations from "@/db/migrations/migrations";
export default function RootLayout() {
// 마이그레이션 실행
const { success, error: dbError } = useMigrations(db, migrations);
// ... 기타 에러 처리 및 로딩 처리
return (
<SQLiteProvider db={dbName}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Stack>
</SQLiteProvider>
);
}useMigrations 훅을 사용해 앱 시작 시 마이그레이션을 실행합니다.SQLiteProvider로 앱을 감싸 Drizzle ORM이 데이터베이스에 접근할 수 있도록 합니다.dbName과 options를 설정합니다.이제 앱에서 Drizzle ORM을 사용해 데이터베이스에 접근할 준비가 되었습니다.
다음은 데이터를 추가해 봅시다.
데이터를 추가하기위해 BookmarkButton.tsx 컴포넌트를 생성합니다.
import { Ionicons } from "@expo/vector-icons";
import { StyleSheet, TouchableOpacity } from "react-native";
const BookmarkButton = () => {
return (
<TouchableOpacity style={styles.bookmarkButton} onPress={() => {}}>
<Ionicons name={"bookmark"} size={24} color={"#FF3B30"} />
</TouchableOpacity>
);
};
export default BookmarkButton;
const styles = StyleSheet.create({
bookmarkButton: {
position: "absolute",
top: 8,
right: 8,
backgroundColor: "rgba(0, 0, 0, 0.3)",
borderRadius: 20,
padding: 6,
},
});TouchableOpacity와 Ionicons를 사용해 즐겨찾기 버튼을 만듭니다.onPress 핸들러는 아직 구현하지 않았습니다.이제 onPress 핸들러에서 Drizzle ORM을 사용해 데이터를 추가하는 로직을 구현합니다.
데이터 저장 확인을 위해 더미 데이터를 사용합니다.
// ... import 생략
const BookmarkButton = () => {
const handlePress = async () => {
try {
await db.insert(bookmarkTable).values({
slug: "test-slug",
title: "Test Bookmark",
description: "테스트 즐겨찾기 항목입니다.",
id: "test-id",
});
} catch (err) {
console.error("Error toggling bookmark:", err);
}
};
return (
<TouchableOpacity style={styles.bookmarkButton} onPress={handlePress}>
<Ionicons name={"bookmark"} size={24} color={"#FF3B30"} />
</TouchableOpacity>
);
};
export default BookmarkButton;
// ... styles 생략handlePress 함수에서 db.insert를 사용해 bookmarkTable에 새 항목을 추가합니다.데이터를 추가하고 한번 더 누르면 에러가 발생하는 것을 확인할 수 있습니다. 이는 id가 유니크키로 설정되어 있어 중복된 값을 허용하지 않기 때문입니다.
이제 추가된 데이터를 조회 해봅시다.
useLiveQuery 훅을 사용해 실시간으로 데이터베이스의 변화를 감지하고 데이터를 조회할 수 있습니다.
// ... import 생략
const BookmarkButton = () => {
const queryId = "test-id"; // 조회할 항목의 ID
const bookmarkQuery = db
.select()
.from(bookmarkTable)
.where(eq(bookmarkTable.id, queryId));
const { data } = useLiveQuery(bookmarkQuery);
console.log("Bookmark data:", data);
// ... handlePress 및 JSX 생략
};
export default BookmarkButton;useLiveQuery 훅을 사용해 bookmarkTable에서 특정 id를 가진 항목을 조회합니다. (테스트용으로 고정된 queryId 사용했습니다. 실제는 props 등으로 동적으로 전달받아야 합니다.)data 변수에 저장되며, 데이터베이스가 변경되면 자동으로 업데이트됩니다.이제 즐겨찾기 버튼을 눌러 데이터를 추가하고, 콘솔에서 조회된 데이터를 확인할 수 있습니다.
다음은 active 상태에 따라 버튼 아이콘을 변경하고 전체 코드를 정리한 예시입니다.
import { db } from "@/db";
import { bookmarkTable } from "@/db/schema/bookmark.table";
import { Ionicons } from "@expo/vector-icons";
import { eq } from "drizzle-orm";
import { useLiveQuery } from "drizzle-orm/expo-sqlite";
import { startTransition, useOptimistic } from "react";
import { StyleSheet, TouchableOpacity } from "react-native";
interface BookmarkButtonProps {
id: string;
// 실제 앱에서는 아래 3개도 props로 받는 걸 추천
slug: string;
title: string;
domain: string;
description?: string;
}
const BookmarkButton = ({
id,
slug,
title,
domain,
description,
}: BookmarkButtonProps) => {
const bookmarkQuery = db
.select()
.from(bookmarkTable)
.where(eq(bookmarkTable.id, id));
const { data } = useLiveQuery(bookmarkQuery);
const isBookmark = (data?.length || 0) > 0;
const [optimisticBookmark, setOptimisticBookmark] = useOptimistic(
isBookmark,
(_current, next: boolean) => next,
);
const handlePress = async () => {
const next = !optimisticBookmark;
startTransition(() => {
// UI 즉시 반영
setOptimisticBookmark(next);
});
try {
if (next) {
// 없으면 추가
await db.insert(bookmarkTable).values({
id,
slug: slug,
title: title,
domain: domain,
description: description ?? null,
});
} else {
// 있으면 삭제
await db.delete(bookmarkTable).where(eq(bookmarkTable.id, id));
}
} catch (err) {
console.error("Error toggling bookmark:", err);
}
};
return (
<TouchableOpacity style={styles.bookmarkButton} onPress={handlePress}>
<Ionicons
name={optimisticBookmark ? "bookmark" : "bookmark-outline"}
size={24}
color={optimisticBookmark ? "#FF3B30" : "#FFFFFF"}
/>
</TouchableOpacity>
);
};
export default BookmarkButton;
const styles = StyleSheet.create({
bookmarkButton: {
position: "absolute",
top: 8,
right: 8,
backgroundColor: "rgba(0, 0, 0, 0.3)",
borderRadius: 20,
padding: 6,
},
});BookmarkButton 컴포넌트는 id, slug, title, description을 props로 받습니다.useLiveQuery 훅을 사용해 해당 id가 즐겨찾기에 있는지 조회합니다.useOptimistic 훅을 사용해 즐겨찾기 상태를 낙관적으로 업데이트합니다.handlePress 함수에서 즐겨찾기 추가/삭제 로직을 구현합니다.위 코드는 예시이며 다음으로 고려해봐야 할 사항은 각 데이터베이스 쿼리 함수를 별도의 훅이나 파일을 분리하여 관리하는 것입니다. 이렇게 하면 코드의 재사용성과 유지보수성이 향상됩니다.
이제 Expo와 Drizzle ORM을 사용해 React Native 앱에서 데이터베이스를 설정하고 사용하는 방법을 알아보았습니다.
마지막으로 Drizzle의 studio 기능을 사용해 데이터베이스를 시각적으로 관리하는 방법을 살펴보겠습니다.
Drizzle Studio는 데이터베이스 스키마를 시각적으로 관리하고, 데이터를 쉽게 조회 및 수정할 수 있는 도구입니다.
Drizzle Studio는 별도의 패키지로 제공되며, 다음 명령어로 설치할 수 있습니다.
bun add expo-drizzle-studio-plugindrizzle studio를 사용하기 위해선 메인 스크린에서 useDrizzleStudio 훅을 호출해야 합니다.
import { useDrizzleStudio } from "expo-drizzle-studio-plugin";
// ... 기타 import 생략
export default function HomeScreen() {
const dbContext = useSQLiteContext();
useDrizzleStudio(dbContext);
// ... 기타 코드 생략
}useSQLiteContext 훅을 사용해 현재 SQLite 데이터베이스 컨텍스트를 가져옵니다.useDrizzleStudio 훅을 호출해 Drizzle Studio를 활성화합니다.expo가 실행 중인 상태에서 shift + m 키를 누르면 옵션이 나타납니다.
여기서 Open expo-drizzle-studio-plugin을 선택하면 Drizzle Studio가 실행됩니다.
지금까지 생성된 테이블과 데이터를 시각적으로 확인하고 관리할 수 있습니다.
이번 글에서는 Expo와 Drizzle ORM을 사용해 React Native 앱에서 데이터베이스를 설정하고 사용하는 방법을 단계별로 알아보았습니다.
Drizzle ORM의 타입 안전성과 간편한 설정 덕분에 모바일 앱 개발에서도 데이터베이스 관리를 효율적으로 할 수 있음을 확인했습니다.
또한 Drizzle Studio를 활용해 데이터베이스를 시각적으로 관리하는 방법도 살펴보았습니다.
앞으로도 Drizzle ORM과 Expo를 활용한 다양한 기능들을 탐구해보시길 바랍니다. Re coding! 🚀