Neon Postgres를 사용한 Todo 앱

이 튜토리얼은 Drizzle ORMNeon 데이터베이스, Next.js를 사용해 Todo 앱을 만드는 방법을 보여줍니다.

This guide assumes familiarity with:
  • 기존의 Next.js 프로젝트가 있거나, 다음 명령어로 새로운 프로젝트를 생성해야 합니다:
npx create-next-app@latest --typescript
  • Drizzle ORM과 Drizzle kit을 설치해야 합니다. 다음 명령어로 설치할 수 있습니다:
npm
yarn
pnpm
bun
npm i drizzle-orm
npm i -D drizzle-kit
npm
yarn
pnpm
bun
npm i @neondatabase/serverless
  • 환경 변수를 관리하기 위해 dotenv 패키지를 설치해야 합니다.
npm
yarn
pnpm
bun
npm i dotenv
IMPORTANT

설치 중 의존성 문제가 발생할 경우:

React Native를 사용하지 않는다면, --force--legacy-peer-deps 옵션을 사용해 강제로 설치하면 문제가 해결될 수 있습니다. React Native를 사용한다면, React Native 버전과 호환되는 정확한 React 버전을 사용해야 합니다.

새로운 Neon 프로젝트 생성하기

Neon Console에 로그인한 후, Projects 섹션으로 이동합니다. 기존 프로젝트를 선택하거나 New Project 버튼을 클릭해 새 프로젝트를 생성합니다.

Neon 프로젝트는 neondb라는 이름의 Postgres 데이터베이스를 기본으로 제공합니다. 이 튜토리얼에서는 이 데이터베이스를 사용할 예정입니다.

연결 문자열 변수 설정하기

프로젝트 콘솔에서 Connection Details 섹션으로 이동하여 데이터베이스 연결 문자열을 찾습니다. 다음과 비슷한 형태일 것입니다:

postgres://username:password@ep-cool-darkness-123456.us-east-2.aws.neon.tech/neondb

Neon 데이터베이스에 연결할 때 사용할 DATABASE_URL 환경 변수를 .env 또는 .env.local 파일에 추가합니다.

DATABASE_URL=NEON_DATABASE_CONNECTION_STRING

Drizzle ORM을 데이터베이스에 연결하기

src/db 폴더 안에 drizzle.ts 파일을 생성하고 데이터베이스 설정을 구성합니다:

import { config } from "dotenv";
import { drizzle } from 'drizzle-orm/neon-http';

// .env 또는 .env.local 파일에서 환경 변수를 로드
config({ path: ".env" });

// Drizzle ORM을 사용해 데이터베이스 연결 설정
export const db = drizzle(process.env.DATABASE_URL!);

이 코드는 .env 파일에서 환경 변수를 읽어와 Drizzle ORM을 통해 데이터베이스에 연결합니다. process.env.DATABASE_URL은 데이터베이스 연결 정보를 담고 있는 환경 변수입니다.

Todo 스키마 정의

src/db/schema.ts
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의 데이터 타입을 사용하여 todo 테이블을 정의합니다. 이 테이블에는 id, text, done 필드가 포함되어 있습니다.

Drizzle 설정 파일 설정하기

Drizzle 설정 파일Drizzle Kit에서 사용하는 구성 파일로, 데이터베이스 연결 정보, 마이그레이션 폴더, 스키마 파일 등에 대한 모든 정보를 포함합니다.

프로젝트 루트에 drizzle.config.ts 파일을 생성하고 다음 내용을 추가하세요:

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
IMPORTANT

push 명령어는 로컬 개발 환경에서 새로운 스키마 디자인이나 변경 사항을 빠르게 테스트해야 할 때 유용합니다. 마이그레이션 파일을 관리하는 오버헤드 없이 빠르게 반복 작업을 할 수 있습니다.

서버 사이드 함수 설정

이 단계에서는 src/actions/todoAction.ts 파일에 서버 사이드 함수를 설정하여 할 일 항목에 대한 주요 작업을 처리합니다.

  1. getData:
    • 데이터베이스에서 모든 기존 할 일 항목을 가져옵니다.
  2. addTodo:
    • 제공된 텍스트로 새로운 할 일 항목을 데이터베이스에 추가합니다.
    • **revalidatePath("/")**를 사용하여 홈 페이지의 재검증을 시작합니다.
  3. deleteTodo:
    • 고유 ID를 기반으로 할 일 항목을 데이터베이스에서 제거합니다.
    • 홈 페이지의 재검증을 트리거합니다.
  4. toggleTodo:
    • 할 일 항목의 완료 상태를 토글하고 데이터베이스를 업데이트합니다.
    • 작업 후 홈 페이지를 재검증합니다.
  5. editTodo:
    • 데이터베이스에서 ID로 식별된 할 일 항목의 텍스트를 수정합니다.
    • 홈 페이지의 재검증을 시작합니다.
src/actions/todoAction.ts
"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("/");
};
Expand

Next.js로 홈페이지 설정하기

  1. 프로젝트 생성

    • 터미널에서 다음 명령어를 실행하여 새로운 Next.js 프로젝트를 생성합니다:
      npx create-next-app@latest my-app
    • my-app은 여러분이 원하는 프로젝트 이름으로 변경할 수 있습니다.
  2. 프로젝트 디렉토리 이동

    • 프로젝트가 생성되면 해당 디렉토리로 이동합니다:
      cd my-app
  3. 개발 서버 실행

    • 다음 명령어를 실행하여 개발 서버를 시작합니다:
      npm run dev
    • 서버가 실행되면 브라우저에서 http://localhost:3000으로 접속하여 기본 홈페이지를 확인할 수 있습니다.
  4. 홈페이지 수정

    • pages/index.js 파일을 열어 홈페이지 내용을 수정합니다. 예를 들어, 다음과 같이 간단한 내용을 추가할 수 있습니다:
      export default function Home() {
        return (
          <div>
            <h1>환영합니다!</h1>
            <p>이것은 Next.js로 만든 홈페이지입니다.</p>
          </div>
        );
      }
  5. 스타일 추가

    • CSS 파일을 생성하거나 인라인 스타일을 사용하여 홈페이지에 스타일을 추가할 수 있습니다. 예를 들어, styles/Home.module.css 파일을 생성하고 다음과 같이 스타일을 정의합니다:
      .container {
        padding: 0 2rem;
      }
      
      .title {
        margin: 0;
        line-height: 1.15;
        font-size: 4rem;
      }
    • 그런 다음, index.js 파일에서 이 스타일을 적용합니다:
      import styles from '../styles/Home.module.css';
      
      export default function Home() {
        return (
          <div className={styles.container}>
            <h1 className={styles.title}>환영합니다!</h1>
            <p>이것은 Next.js로 만든 홈페이지입니다.</p>
          </div>
        );
      }
  6. 변경 사항 확인

    • 파일을 저장하면 개발 서버가 자동으로 리로드되어 변경 사항을 반영합니다. 브라우저에서 홈페이지를 새로고침하여 수정된 내용을 확인합니다.
  7. 배포 준비

    • 프로젝트가 완료되면 다음 명령어를 실행하여 프로덕션 빌드를 생성합니다:
      npm run build
    • 빌드가 완료되면 다음 명령어로 프로덕션 서버를 실행할 수 있습니다:
      npm start

이제 여러분은 Next.js를 사용하여 간단한 홈페이지를 설정하고 수정하는 방법을 알게 되었습니다. 추가적인 기능이나 페이지를 추가하려면 Next.js 공식 문서를 참고하세요.

TypeScript 타입 정의하기

src/types/todoType.ts 파일에서 **todoType**이라는 이름의 타입을 정의합니다. 이 타입은 일반적인 할 일 항목의 구조를 나타내며, 세 가지 속성을 가집니다: **id**는 number 타입, **text**는 string 타입, 그리고 **done**은 boolean 타입입니다.

src/types/todoType.ts
export type todoType = {
  id: number;
  text: string;
  done: boolean;
};

To-do 애플리케이션 홈페이지 만들기

  1. src/components/todo.tsx:
    Todo 컴포넌트를 생성합니다. 이 컴포넌트는 단일 할 일 항목을 나타내며, 할 일 텍스트를 표시하고 편집할 수 있는 기능, 체크박스를 통해 완료 상태를 표시하는 기능, 그리고 편집, 저장, 취소, 삭제 작업을 제공합니다.

  2. src/components/addTodo.tsx:
    AddTodo 컴포넌트는 새로운 할 일 항목을 추가하기 위한 간단한 폼을 제공합니다. 할 일 텍스트를 입력할 수 있는 입력 필드와 새로운 할 일을 추가하는 버튼이 포함됩니다.

  3. src/components/todos.tsx:
    Todos 컴포넌트는 To-do 애플리케이션의 메인 인터페이스를 나타냅니다. 할 일 항목의 상태를 관리하고, 할 일을 생성, 편집, 토글, 삭제하는 함수를 제공하며, Todo 컴포넌트를 사용하여 개별 할 일 항목을 렌더링합니다.

todo.tsx
addTodo.tsx
todos.tsx
"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 = ({
  todo,
  changeTodoText,
  toggleIsTodoDone,
  deleteTodoItem,
}) => {
  // 편집 모드를 관리하는 상태
  const [editing, setEditing] = useState(false);

  // 텍스트 입력을 관리하는 상태
  const [text, setText] = useState(todo.text);

  // 완료 상태를 관리하는 상태
  const [isDone, setIsDone] = useState(todo.done);

  // 텍스트 입력 변경 이벤트 핸들러
  const handleTextChange = (e: ChangeEvent) => {
    setText(e.target.value);
  };

  // 완료 상태 토글 이벤트 핸들러
  const handleIsDone = async () => {
    toggleIsTodoDone(todo.id, !isDone);
    setIsDone((prev) => !prev);
  };

  // 편집 모드 시작 이벤트 핸들러
  const handleEdit = () => {
    setEditing(true);
  };

  // 편집된 텍스트 저장 이벤트 핸들러
  const handleSave = async () => {
    changeTodoText(todo.id, text);
    setEditing(false);
  };

  // 편집 모드 취소 이벤트 핸들러
  const handleCancel = () => {
    setEditing(false);
    setText(todo.text);
  };

  // 할 일 항목 삭제 이벤트 핸들러
  const handleDelete = () => {
    if (confirm("이 할 일을 삭제하시겠습니까?")) {
      deleteTodoItem(todo.id);
    }
  };

  // Todo 컴포넌트 렌더링
  return (
    <div>
      {/* 완료 상태를 표시하는 체크박스 */}
      <input
        type="checkbox"
        className="text-blue-200 rounded-sm h-4 w-4"
        checked={isDone}
        onChange={handleIsDone}
      />
      {/* 할 일 텍스트 입력 필드 */}
      <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`}
      />
      {/* 편집, 저장, 취소, 삭제 버튼 */}
      <div>
        {editing ? (
          <button
            onClick={handleSave}
            className="bg-green-600 text-green-50 rounded px-2 w-14 py-1"
          >
            저장
          </button>
        ) : (
          <button
            onClick={handleEdit}
            className="bg-blue-400 text-blue-50 rounded w-14 px-2 py-1"
          >
            편집
          </button>
        )}
        {editing ? (
          <button
            onClick={handleCancel}
            className="bg-red-400 w-16 text-red-50 rounded px-2 py-1"
          >
            닫기
          </button>
        ) : (
          <button
            onClick={handleDelete}
            className="bg-red-400 w-16 text-red-50 rounded px-2 py-1"
          >
            삭제
          </button>
        )}
      </div>
    </div>
  );
};

export default Todo;
Expand

src/app 폴더의 page.tsx 파일을 업데이트하여 데이터베이스에서 할 일 항목을 가져오고 Todos 컴포넌트를 렌더링합니다:

src/app/page.tsx
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