설치 중 의존성 해결 문제가 발생하는 경우:
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 콘솔에 로그인하고 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 텍스트를 표시하고 편집하는 기능, 체크박스로 완료 표시하는 기능, 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