SQLite와 함께 TanStack Start를 자체 서버에 배포하기
Source: Dev.to
Why TanStack Start + SQLite?
- Simplicity – SQLite는 애플리케이션과 같은 프로세스에서 실행되어 네트워크 지연과 복잡한 데이터베이스 클러스터를 없앱니다. 개발은
pnpm install하고 바로 코딩을 시작하는 것만큼 쉽습니다. - Performance – 읽기와 쓰기가 로컬 파일 시스템에서 이루어져, 일반적인 웹 요청에 대해 네트워크에 연결된 데이터베이스보다 더 빠른 속도를 제공하는 경우가 많습니다.
- Easy Deployment – 앱 서버와 데이터베이스 파일(영구 볼륨으로 마운트)을 Haloy와 함께 단일 리전으로 배포합니다.
What You’ll Build
다음과 같은 기술을 사용한 풀스택 React 애플리케이션을 만들게 됩니다:
- TanStack Start – 파일 기반 라우팅과 서버 함수를 제공하는 React 메타 프레임워크
- SQLite – 가볍고 파일 기반인 데이터베이스
- Drizzle ORM – 타입 안전한 데이터베이스 쿼리를 위한 TypeScript ORM
- Haloy – 자체 서버에 간단히 배포할 수 있는 도구
Prerequisites
- Node.js 20+ 설치
- Haloy 설치 (Haloy Quickstart 참고)
- Linux 서버(VPS 또는 전용)
- 도메인 또는 서브도메인
- React와 TypeScript에 대한 기본 지식
Setup
mkdir my-tanstack-app
cd my-tanstack-app
pnpm init
tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"moduleResolution": "Bundler",
"module": "ESNext",
"target": "ES2022",
"skipLibCheck": true,
"strictNullChecks": true
}
}
Install core dependencies
pnpm add @tanstack/react-start @tanstack/react-router react react-dom nitro
Install development dependencies
pnpm add -D vite @vitejs/plugin-react typescript @types/react @types/react-dom @types/node vite-tsconfig-paths
Install Drizzle and SQLite client
pnpm add drizzle-orm @libsql/client dotenv drizzle-kit
Note:
drizzle-kit은 프로덕션 의존성으로 설치됩니다(-D가 아님). 이는 Docker 컨테이너에서 시작 시 마이그레이션을 실행할 수 있도록 하기 위함입니다.
package.json scripts
{
// ...
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"start": "node .output/server/index.mjs",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate"
}
}
Configuration
vite.config.ts
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import tsConfigPaths from "vite-tsconfig-paths";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
export default defineConfig({
server: {
port: 3000,
},
plugins: [
tsConfigPaths(),
tanstackStart(),
nitro(),
// React's Vite plugin must come after Start's Vite plugin
viteReact(),
],
nitro: {},
});
TanStack Start는 Nitro를 서버 엔진으로 사용합니다. 이번 배포에서는 Haloy와 완벽히 호환되는 기본 Node.js 프리셋을 사용합니다. 추가적인 Nitro 설정은 필요하지 않으며, 빈 nitro: {} 객체만 있으면 충분합니다.
drizzle.config.ts
import { config } from "dotenv";
import { defineConfig } from "drizzle-kit";
config();
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error("DATABASE_URL is not set");
}
export default defineConfig({
out: "./drizzle",
schema: "./src/db/schema.ts",
dialect: "sqlite",
dbCredentials: {
url: databaseUrl,
},
});
Database client (src/db/index.ts)
import "dotenv/config";
import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error("DATABASE_URL is not set");
}
const client = createClient({ url: databaseUrl });
const db = drizzle({ client });
export { client, db };
Schema (src/db/schema.ts)
import { sql } from "drizzle-orm";
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const todos = sqliteTable("todos", {
id: integer("id", { mode: "number" }).primaryKey({
autoIncrement: true,
}),
title: text("title").notNull(),
createdAt: integer("created_at", { mode: "timestamp" }).default(
sql`(unixepoch())`
),
});
Local development environment (.env)
DATABASE_URL=file:local.db
마이그레이션을 생성하고 실행합니다:
pnpm db:generate
pnpm db:migrate
이 명령은 프로덕션에서 사용할 마이그레이션 파일을 drizzle/ 디렉터리에 생성합니다.
Routing
src/router.tsx
import { createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
export function getRouter() {
const router = createRouter({
routeTree,
scrollRestoration: true,
defaultNotFoundComponent: () => 404 - not found,
});
return router;
}
Note:
./routeTree.gen파일을 찾을 수 없다는 TypeScript 오류가 표시될 수 있습니다. 이는 정상적인 현상이며, TanStack Start가 개발 서버를 실행할 때 자동으로 해당 파일을 생성합니다.
Root route (src/routes/__root.tsx)
///
import {
createRootRoute,
HeadContent,
Outlet,
Scripts,
} from "@tanstack/react-router";
import type { ReactNode } from "react";
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{ title: "TanStack Start Starter" },
],
}),
component: RootComponent,
});
function RootComponent() {
return (
);
}
function RootDocument({ children }: Readonly) {
return (
{children}
);
}
Todo route (src/routes/index.tsx)
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { eq } from "drizzle-orm";
import { db } from "../db";
import { todos } from "../db/schema";
const getTodos = createServerFn({
method: "GET",
}).handler(async () => await db.select().from(todos));
const addTodo = createServerFn({ method: "POST" })
.inputValidator((data: FormData) => {
if (!(data instanceof FormData)) {
throw new Error("Expected FormData");
}
return {
title: data.get("title")?.toString() || "",
};
})
.handler(async ({ data }) => {
await db.insert(todos).values({ title: data.title });
});
const deleteTodo = createServerFn({ method: "POST" })
.inputValidator((data: number) => data)
.handler(async ({ data }) => {
await db.delete(todos).where(eq(todos.id, data));
});
export const Route = createFileRoute("/")({
component: RouteComponent,
loader: async () => await getTodos(),
});
function RouteComponent() {
const router = useRouter();
const todos = Route.useLoaderData();
return (
{todos.map((todo) => (
{todo.title}
{
await deleteTodo({ data: todo.id });
router.invalidate();
}}
>
X
))}
## Add todo
{
e.preventDefault();
const form = e.currentTarget;
const data = new FormData(form);
await addTodo({ data });
router.invalidate();
form.reset();
}}
>
Add
);
}
이제 Haloy와 함께 로컬 개발 및 프로덕션 배포가 가능한 완전한 TanStack Start + SQLite 스타터가 준비되었습니다. Haloy 문서를 참고하여 Docker 이미지를 빌드하고, local.db용 영구 볼륨을 설정한 뒤 자동 HTTPS와 함께 서비스를 노출하세요. 즐거운 코딩 되세요!