SQLite와 함께 TanStack Start를 자체 서버에 배포하기

발행: (2025년 12월 15일 오전 06:33 GMT+9)
6 min read
원문: Dev.to

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와 함께 서비스를 노출하세요. 즐거운 코딩 되세요!

Back to Blog

관련 글

더 보기 »