설치 중 의존성 해결 문제가 발생하는 경우:
React Native를 사용하지 않는다면 --force 또는 --legacy-peer-deps 옵션으로 강제 설치하면 문제가 해결됩니다. React Native를 사용하는 경우에는 React Native 버전과 호환되는 정확한 React 버전을 사용해야 합니다.
이 튜토리얼은 Drizzle ORM, Neon 데이터베이스, Next.js를 사용하여 Todo 앱을 만드는 방법을 보여줍니다.
npx create-next-app@latest --typescriptnpm i drizzle-orm
npm i -D drizzle-kit
npm i @neondatabase/serverless
dotenv 패키지를 설치해야 합니다.npm i dotenv
설치 중 의존성 해결 문제가 발생하는 경우:
React Native를 사용하지 않는다면 --force 또는 --legacy-peer-deps 옵션으로 강제 설치하면 문제가 해결됩니다. React Native를 사용하는 경우에는 React Native 버전과 호환되는 정확한 React 버전을 사용해야 합니다.
Neon Console에 로그인하고 Projects 섹션으로 이동합니다. 프로젝트를 선택하거나 New Project 버튼을 클릭하여 새 프로젝트를 생성합니다.
Neon 프로젝트에는 neondb라는 이름의 바로 사용 가능한 Postgres 데이터베이스가 제공됩니다. 이 튜토리얼에서는 이 데이터베이스를 사용합니다.
프로젝트 콘솔의 Connection Details 섹션으로 이동하여 데이터베이스 연결 문자열을 확인합니다. 다음과 유사한 형태입니다:
postgres://username:password@ep-cool-darkness-123456.us-east-2.aws.neon.tech/neondb.env 또는 .env.local 파일에 DATABASE_URL 환경 변수를 추가합니다. 이 변수는 Neon 데이터베이스에 연결하는 데 사용됩니다.
DATABASE_URL=NEON_DATABASE_CONNECTION_STRINGsrc/db 폴더에 drizzle.ts 파일을 생성하고 데이터베이스 설정을 구성합니다:
import { config } from "dotenv";
import { drizzle } from 'drizzle-orm/neon-http';
config({ path: ".env" }); // or .env.local
export const db = drizzle(process.env.DATABASE_URL!);import { integer, text, boolean, pgTable } from "drizzle-orm/pg-core";
export const todo = pgTable("todo", {
id: integer("id").primaryKey(),
text: text("text").notNull(),
done: boolean("done").default(false).notNull(),
});여기서는 Drizzle ORM의 데이터 타입을 사용하여 id, text, done 필드를 가진 todo 테이블을 정의합니다.
Drizzle config - Drizzle Kit에서 사용하는 설정 파일로, 데이터베이스 연결, 마이그레이션 폴더, 스키마 파일에 대한 모든 정보를 포함합니다.
프로젝트 루트에 drizzle.config.ts 파일을 생성하고 다음 내용을 추가합니다:
import { config } from 'dotenv';
import { defineConfig } from "drizzle-kit";
config({ path: '.env' });
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./migrations",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});drizzle-kit generate 명령어로 마이그레이션을 생성하고 drizzle-kit migrate 명령어로 실행할 수 있습니다.
마이그레이션 생성:
npx drizzle-kit generate이 마이그레이션들은 drizzle.config.ts에 지정된 대로 drizzle/migrations 디렉토리에 저장됩니다. 이 디렉토리에는 데이터베이스 스키마를 업데이트하는 데 필요한 SQL 파일과 다양한 마이그레이션 단계의 스키마 스냅샷을 저장하는 meta 폴더가 포함됩니다.
생성된 마이그레이션 예시:
CREATE TABLE IF NOT EXISTS "todo" (
"id" integer PRIMARY KEY NOT NULL,
"text" text NOT NULL,
"done" boolean DEFAULT false NOT NULL
);마이그레이션 실행:
npx drizzle-kit migrate또는 Drizzle kit push 명령어를 사용하여 데이터베이스에 변경사항을 직접 푸시할 수 있습니다:
npx drizzle-kit push이 단계에서는 src/actions/todoAction.ts 파일에 todo 아이템에 대한 주요 작업을 처리하는 서버 사이드 함수를 구축합니다:
getData:
addTodo:
revalidatePath("/")**를 사용하여 홈 페이지의 재검증을 시작합니다.deleteTodo:
toggleTodo:
editTodo:
"use server";
import { eq, not } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { db } from "@/db/drizzle";
import { todo } from "@/db/schema";
export const getData = async () => {
const data = await db.select().from(todo);
return data;
};
export const addTodo = async (id: number, text: string) => {
await db.insert(todo).values({
id: id,
text: text,
});
};
export const deleteTodo = async (id: number) => {
await db.delete(todo).where(eq(todo.id, id));
revalidatePath("/");
};
export const toggleTodo = async (id: number) => {
await db
.update(todo)
.set({
done: not(todo.done),
})
.where(eq(todo.id, id));
revalidatePath("/");
};
export const editTodo = async (id: number, text: string) => {
await db
.update(todo)
.set({
text: text,
})
.where(eq(todo.id, id));
revalidatePath("/");
};src/types/todoType.ts에서 todo 아이템을 위한 TypeScript 타입을 정의합니다. 이 타입은 number 타입의 id, string 타입의 text, boolean 타입의 done 세 가지 속성을 가집니다. **todoType**이라는 이름의 이 타입은 애플리케이션 내 일반적인 todo 아이템의 구조를 나타냅니다.
export type todoType = {
id: number;
text: string;
done: boolean;
};src/components/todo.tsx:
단일 todo 아이템을 나타내는 Todo 컴포넌트를 생성합니다. todo 텍스트를 표시하고 편집하는 기능, 체크박스로 완료 표시하는 기능, 편집, 저장, 취소, 삭제 작업을 제공하는 기능을 포함합니다.src/components/addTodo.tsx:
AddTodo 컴포넌트는 Todo 앱에 새 todo 아이템을 추가하는 간단한 폼을 제공합니다. todo 텍스트를 입력하는 입력 필드와 새 todo 추가를 트리거하는 버튼이 포함됩니다.src/components/todos.tsx:
Todo 앱의 메인 인터페이스를 나타내는 Todos 컴포넌트를 생성합니다. todo 아이템의 상태를 관리하고, todo를 생성, 편집, 토글, 삭제하는 함수를 제공하며, Todo 컴포넌트를 사용하여 개별 todo 아이템을 렌더링합니다."use client";
import { ChangeEvent, FC, useState } from "react";
import { todoType } from "@/types/todoType";
interface Props {
todo: todoType;
changeTodoText: (id: number, text: string) => void;
toggleIsTodoDone: (id: number, done: boolean) => void;
deleteTodoItem: (id: number) => void;
}
const Todo: FC<Props> = ({
todo,
changeTodoText,
toggleIsTodoDone,
deleteTodoItem,
}) => {
// State for handling editing mode
const [editing, setEditing] = useState(false);
// State for handling text input
const [text, setText] = useState(todo.text);
// State for handling "done" status
const [isDone, setIsDone] = useState(todo.done);
// Event handler for text input change
const handleTextChange = (e: ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
};
// Event handler for toggling "done" status
const handleIsDone = async () => {
toggleIsTodoDone(todo.id, !isDone);
setIsDone((prev) => !prev);
};
// Event handler for initiating the edit mode
const handleEdit = () => {
setEditing(true);
};
// Event handler for saving the edited text
const handleSave = async () => {
changeTodoText(todo.id, text);
setEditing(false);
};
// Event handler for canceling the edit mode
const handleCancel = () => {
setEditing(false);
setText(todo.text);
};
// Event handler for deleting a todo item
const handleDelete = () => {
if (confirm("Are you sure you want to delete this todo?")) {
deleteTodoItem(todo.id);
}
};
// Rendering the Todo component
return (
<div className="flex items-center gap-2 p-4 border-gray-200 border-solid border rounded-lg">
{/* Checkbox for marking the todo as done */}
<input
type="checkbox"
className="text-blue-200 rounded-sm h-4 w-4"
checked={isDone}
onChange={handleIsDone}
/>
{/* Input field for todo text */}
<input
type="text"
value={text}
onChange={handleTextChange}
readOnly={!editing}
className={`${
todo.done ? "line-through" : ""
} outline-none read-only:border-transparent focus:border border-gray-200 rounded px-2 py-1 w-full`}
/>
{/* Action buttons for editing, saving, canceling, and deleting */}
<div className="flex gap-1 ml-auto">
{editing ? (
<button
onClick={handleSave}
className="bg-green-600 text-green-50 rounded px-2 w-14 py-1"
>
Save
</button>
) : (
<button
onClick={handleEdit}
className="bg-blue-400 text-blue-50 rounded w-14 px-2 py-1"
>
Edit
</button>
)}
{editing ? (
<button
onClick={handleCancel}
className="bg-red-400 w-16 text-red-50 rounded px-2 py-1"
>
Close
</button>
) : (
<button
onClick={handleDelete}
className="bg-red-400 w-16 text-red-50 rounded px-2 py-1"
>
Delete
</button>
)}
</div>
</div>
);
};
export default Todo;src/app 폴더의 page.tsx 파일을 업데이트하여 데이터베이스에서 todo 아이템을 가져오고 Todos 컴포넌트를 렌더링합니다:
import { getData } from "@/actions/todoAction";
import Todos from "@/components/todos";
export default async function Home() {
const data = await getData();
return <Todos todos={data} />;
}이 가이드는 다음 파일 구조를 사용합니다:
📦 <project root>
├ 📂 migrations
│ ├ 📂 meta
│ └ 📜 0000_heavy_doctor_doom.sql
├ 📂 public
├ 📂 src
│ ├ 📂 actions
│ │ └ 📜 todoActions.ts
│ ├ 📂 app
│ │ ├ 📜 favicon.ico
│ │ ├ 📜 globals.css
│ │ ├ 📜 layout.tsx
│ │ └ 📜 page.tsx
│ ├ 📂 components
│ │ ├ 📜 addTodo.tsx
│ │ ├ 📜 todo.tsx
│ │ └ 📜 todos.tsx
│ └ 📂 db
│ │ ├ 📜 drizzle.ts
│ │ └ 📜 schema.ts
│ └ 📂 types
│ └ 📜 todoType.ts
├ 📜 .env
├ 📜 .eslintrc.json
├ 📜 .gitignore
├ 📜 drizzle.config.ts
├ 📜 next-env.d.ts
├ 📜 next.config.mjs
├ 📜 package-lock.json
├ 📜 package.json
├ 📜 postcss.config.mjs
├ 📜 README.md
├ 📜 tailwind.config.ts
└ 📜 tsconfig.json