CRUD 操作은 모든 응용 프로그램의 기반입니다. 따라서 새로운 기술을 배우는 과정에서 이를 훌륭하게 manipalize 할 수 있는 것은 필수입니다.

이 튜토리얼에서는 React와 Convex를 사용하여 CRUD 응용 프로그램을 만들어 보실 수 있습니다. 이 작업을 위해 Book Collections라는 프로젝트를 빌딩하고 있습니다. 이 프로젝트에서는 사용자는 도서를 추가하고 도서를 완료 시켰다면 그 상태를 更新할 수 있습니다.

목차

Convex이란 무엇인가?

Convex은 Baas 플랫폼으로, 백 엔드 開発을 간단하게 해줍니다. Convex는 실시간 데이터베이스를 갖추고 있으며, 서버 测의 로직을 따로 書かない 경우도 있습니다.

전사적 요구사항

이 튜토리얼을 따라가려면 React의 기초를 알아야 합니다. 이 프로젝트에서는 TypeScript를 사용하겠지만, 옵션입니다. JavaScript로 따라가는 것도 가능합니다.

프로젝트 세팅 방법

프로젝트 용 따로의 폴더를 만들고 이름을 freely 지정하십시오 – 저는 Books. 이 폴더에서 Convex과 React를 세팅할 것입니다.

React 앱을 이렇게 만들 수 있습니다:

npm create vite@latest my-app -- --template react-ts

JavaScript로 작업하고자 하면 ts를 끝에서 제거하면 됩니다:

npm create vite@latest my-app -- --template react

Convex 세팅 방법

Convex을 같은 폴더에 설치하는 명령어를 사용할 수 있습니다:

npm install convex

그다음으로 npx convex dev를 실행합니다. 처음이라면 인증을 할 것입니다. 그렇지 않으면 프로젝트 이름을 요구할 것입니다.

Convex 대시보드를 방문하여 생성한 프로젝트를 확인할 수 있습니다.

Convex와 React 앱을 설정했으므로, Convex 백엔드를 React 앱에 연결해야 합니다.

src/main.tsx에서 App 컴포넌트를 ConvexReactClient로 감싸세요:

import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import "./index.css"

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);

createRoot(document.getElementById("root")!).render(
  <ConvexProvider client={convex}>
    <App />
  </ConvexProvider>
);

Convex를 설정할 때 .env.local 파일이 생성되었습니다. 해당 파일에서 백엔드 URL을 확인할 수 있습니다.

아래 줄에서는 URL로 React Convex 클라이언트를 인스턴스화했습니다.

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);

스키마 생성 방법

주요 프로젝트 디렉토리에서 convex 디렉토리를 볼 수 있어야 합니다. 여기서 데이터베이스 쿼리와 변형을 처리할 것입니다.

schema.ts 파일을 convex 폴더에 생성하세요:

import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  books: defineTable({
    title: v.string(),
    author: v.string(),
    isCompleted: v.boolean(),
  }),
});

문서에 대한 스키마는 defineSchema로 정의하고, defineTable로 테이블을 생성할 수 있습니다. Convex는 스키마를 정의하고 테이블을 생성하기 위한 이러한 함수를 제공합니다.

v는 타입 검증기이며, 테이블에 추가하는 각 데이터에 대한 타입을 제공하는 데 사용됩니다.

이 프로젝트는 도서 수집 애플리케이션이므로, 구조는 title, author, isCompleted를 포함합니다. 더 많은 필드를 추가할 수 있습니다.

스키마를 정의했으니, React에서 기본 UI를 설정해봅시다.

UI 생성 방법

src 폴더에 component이라는 폴더와 Home.tsx이라는 文件을 생성합니다. 여기에 UI를 정의할 수 있습니다.

import { useState } from "react";
import "../styles/home.css";

const Home = () => {
  const [title, setTitle] = useState("");
  const [author, setAuthor] = useState("");
  return (
    <div className="main-container">
      <h1>Book Collections</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          name="title"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="book title"
        />
        <br />
        <input
          type="text"
          name="author"
          value={author}
          onChange={(e) => setAuthor(e.target.value)}
          placeholder="book author"
        />
        <br />
        <input type="submit" />
      </form>
      {books ? <Books books={books} /> : "Loading..."}
    </div>
  );
};

export default Home;

이 곳에 자신의 컴포넌트를 원하는 것대로 생성할 수 있습니다. 저는 두 개의 입력 필드 title, author를 추가하고 submit button를 생성했습니다. 이것은 기본적인 구조입니다. 이제 백端에서 CRUD 메서드를 생성할 수 있습니다.

CRUD 함수 생성 방법

convex 폴더에 CRUD 함수를 사용하는 独立 queries.ts 文件을 생성할 수 있습니다.

함수 생성

convex/queries.ts 中:

createBooks 함수를 정의할 수 있습니다. Convex의 mutation 함수를 사용하여 데이터를 생성, 수정, 및 삭제할 수 있습니다. 데이터를 읽는 것은 query에 해당합니다.

mutation 함수는 다음과 같은 인자를 기대합니다:

  • agrs: 데이터베이스에 저장해야 하는 데이터.

  • handler: 데이터를 데이터베이스에 저장하는 로직을 처리하는 함수입니다. handler는 비동기 함수이며, ctxargs를 두 인자로 가지고 있습니다. 여기서 ctx는 데이터베이스 operatioins를 처리하기 위한 컨텍스트 오브젝트입니다.

insert 方法을 사용하여 새로운 데이터를 삽입할 것입니다. insert 方法的 첫 번째 매개 변수는 테이블 이름이고, 두 번째 매개 변수는 삽입해야 하는 데이터입니다.

결국, 데이터베이스から 데이터를 돌려 받을 수 있습니다.

아래는 코드입니다:

import { mutation} from "./_generated/server";
import { v } from "convex/values";

export const createBooks = mutation({
  args: { title: v.string(), author: v.string() },
  handler: async (ctx, args) => {
    const newBookId = await ctx.db.insert("books", {
      title: args.title,
      author: args.author,
      isCompleted: false,
    });
    return newBookId;
  },
});

읽기 기능

In convex/queries.ts:

import { query } from "./_generated/server";
import { v } from "convex/values";

//read
export const getBooks = query({
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query("books").collect();
  },
});

이 읽기 操作에서, Convex의 내장 query 함수를 사용하였습니다. 여기에서 args는 사용자에서 데이터를 받지 않기 때문에 비어있을 것입니다. 마찬가지로, handler 함수는 비동기적이며 ctx 객체를 사용하여 데이터베이스를 쿼리하고 데이터를 돌려 주는 것입니다.

업데이트 기능

In convex/queries.ts:

一个 updateStatus 함수를 생성합니다. 우리는 仅仅 isCompleted 상태를 업데이트 할 것입니다.

여기에서, 사용자から 문서 ID와 상태를 얻어야 합니다. args에서는 idisCompleted를 정의하고, 이는 사용자에서 来고 있습니다.

handler 에서, patch 方法을 사용하여 데이터를 更新할 것입니다. patch 方法은 두 가지 인자를 기대하고 있습니다: 첫 번째 인자는 문서의 id이고, 두 번째 인자는 更新된 데이터입니다.

import { mutation } from "./_generated/server";
import { v } from "convex/values";

//update
export const updateStatus = mutation({
  args: { id: v.id("books"), isCompleted: v.boolean() },
  handler: async (ctx, args) => {
    const { id } = args;
    await ctx.db.patch(id, { isCompleted: args.isCompleted });
    return "updated"
  },
});

삭제 기능

In convex/queries.ts:

deleteBooks 함수를 생성하고 mutation 함수를 사용합니다. 지울 文档의 ID가 필요합니다. args 에 ID를 정의하고 handler 에서 ctx 객체의 delete 方法的을 사용하고 ID를 전달합니다. 이렇게 文档을 지웁니다.

import { mutation } from "./_generated/server";
import { v } from "convex/values";

//delete
export const deleteBooks = mutation({
  args: { id: v.id("books") },
  handler: async (ctx, args) => {
    await ctx.db.delete(args.id);
    return "deleted";
  },
});

지금까지 CRUD 함수를 백端에 완성했습니다. 이제 UI에서 동작시키기 위해 작업해야 합니다. React로 돌아가겠습니다.

UI를 更新

React 应用程序에 기본적인 UI와 某些 input fields를 이미 생성했습니다. 이를 更新하겠습니다.

src/component/Home.tsx 에서:

import { useQuery, useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
import { Books } from "./Books";
import { useState } from "react";
import "../styles/home.css";

const Home = () => {
  const [title, setTitle] = useState("");
  const [author, setAuthor] = useState("");
  const books = useQuery(api.queries.getBooks);
  const createBooks = useMutation(api.queries.createBooks);

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
    e.preventDefault();
    createBooks({ title, author })
      .then(() => {
        console.log("created");
        setTitle("");
        setAuthor("");
      })
      .catch((err) => console.log(err));
  };
  return (
    <div className="main-container">
      <h1>Book Collections</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          name="title"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="book title"
        />
        <br />
        <input
          type="text"
          name="author"
          value={author}
          onChange={(e) => setAuthor(e.target.value)}
          placeholder="book author"
        />
        <br />
        <input type="submit" />
      </form>
      {books ? <Books books={books} /> : "Loading..."}
    </div>
  );
};

export default Home;

Convex의 api 를 사용하여 백端 API 함수를 이용할 수 있습니다. 正如你所见, 우리는 두 가지 API 함수를 호출했습니다: 데이터 읽기를 하고자 할 때 useQuery를 사용하고, 데이터를 변경하고자 할 때는 useMutation를 사용할 수 있습니다. 이 파일에서, 우리는 생성과 읽기 두 가지 operaion을 行って 있습니다.

이 方法을 사용하여 모든 데이터를 얻었습니다:

 const books = useQuery(api.queries.getBooks);

객체의 배열은 books 변수에 저장됩니다.

backend에서 생성 함수를 이 行의 코드로 얻었습니다:

const createBooks = useMutation(api.queries.createBooks);

UI에서 생성 함수를 사용하는 方法

UI에서 생성 함수를 사용하겠습니다.

input fields가 form 태그에 있으므로, form 제출을 처리하기 위해 onSubmit 속성을 사용할 것입니다.

//In the Home.tsx

const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
    e.preventDefault();
    createBooks({ title, author })
      .then(() => {
        console.log("created");
        setTitle("");
        setAuthor("");
      })
      .catch((err) => console.log(err));
  };

제출 按钮을 클릭하면 handleSubmit 함수가 동작합니다.

我们는 createBooks 를 사용하여 状態から titleauthor 를 전달하였습니다. 엔DPoint 함수는 비동기로 동작하므로, 비동기 데이터를 处理하기 위해 handleSubmit 를 비동기로 使用하거나 .then 를 使用할 수 있습니다. 저는 .then 方法을 사용하여 异步적인 데이터를 처리했습니다.

データ베이스에서 가져온 데이터를 보여줄 수 있는 별도의 コンポーネン트를 생성할 수 있습니다. 리턴된 데이터는 Home.tsx 에 있으므로, 我們는 데이터를 Books.tsx コンポーネン트に props 形式으로 전달할 것입니다.

Books.tsx 中:

import { useState } from "react";
import { book } from "../types/book.type";
import { useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
import { Id } from "../../convex/_generated/dataModel";
import "../styles/book.css";

export const Books = ({ books }: { books: book[] }) => {
  const [update, setUpdate] = useState(false);
  const [id, setId] = useState("");

  const deleteBooks = useMutation(api.queries.deleteBooks);
  const updateStatus = useMutation(api.queries.updateStatus);

  const handleClick = (id: string) => {
    setId(id);
    setUpdate(!update);
  };

  const handleDelete = (id: string) => {
    deleteBooks({ id: id as Id<"books"> })
      .then((mess) => console.log(mess))
      .catch((err) => console.log(err));
  };

  const handleUpdate = (e: React.FormEvent<HTMLFormElement>, id: string) => {
    e.preventDefault();
    const formdata = new FormData(e.currentTarget);
    const isCompleted: boolean =
      (formdata.get("completed") as string) === "true";
    updateStatus({ id: id as Id<"books">, isCompleted })
      .then((mess) => console.log(mess))
      .catch((err) => console.log(err));
    setUpdate(false);
  };

  return (
    <div>
      {books.map((data: book, index: number) => {
        return (
          <div
            key={data._id}
            className={`book-container ${data.isCompleted ? "completed" : "not-completed"}`}
          >
            <h3>Book no: {index + 1}</h3>
            <p>Book title: {data.title}</p>
            <p>Book Author: {data.author}</p>
            <p>
              Completed Status:{" "}
              {data.isCompleted ? "Completed" : "Not Completed"}
            </p>
            <button onClick={() => handleClick(data._id)}>Update</button>
            {id === data._id && update && (
              <>
                <form onSubmit={(e) => handleUpdate(e, data._id)}>
                  <select name="completed">
                    <option value="true">Completed</option>
                    <option value="false">Not Completed</option>
                  </select>
                  <input type="submit" />
                </form>
              </>
            )}
            <button onClick={() => handleDelete(data._id)}>delete</button>
          </div>
        );
      })}
    </div>
  );
};

Books.jsx 组件中,您可以显示来自数据库的数据并处理更新和删除记录的功能。

让我们逐一介绍这些功能的实现步骤。

数据如何显示

您可以在 Home.tsx 组件中获取作为 props 传递的数据。如果您使用 TypeScript,我已经为从查询返回的对象定义了一个类型。如果您使用 JavaScript,可以忽略这个。

创建 `books.types.ts`:

export type book = {
    _id: string,
    title: string,
    author: string,
    isCompleted: boolean
}

您可以使用 map 函数来显示数据。

import { useState } from "react";
import { book } from "../types/book.type";
import { useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
import { Id } from "../../convex/_generated/dataModel";
import "../styles/book.css";

export const Books = ({ books }: { books: book[] }) => {
  const [update, setUpdate] = useState(false);

  return (
    <div>
      {books.map((data: book, index: number) => {
        return (
          <div
            key={data._id}
            className={`book-container ${data.isCompleted ? "completed" : "not-completed"}`}
          >
            <h3>Book no: {index + 1}</h3>
            <p>Book title: {data.title}</p>
            <p>Book Author: {data.author}</p>
            <p>
              Completed Status:{" "}
              {data.isCompleted ? "Completed" : "Not Completed"}
            </p>
            <button onClick={() => handleClick(data._id)}>Update</button>
            {id === data._id && update && (
              <>
                <form onSubmit={(e) => handleUpdate(e, data._id)}>
                  <select name="completed">
                    <option value="true">Completed</option>
                    <option value="false">Not Completed</option>
                  </select>
                  <input type="submit" />
                </form>
              </>
            )}
            <button onClick={() => handleDelete(data._id)}>delete</button>
          </div>
        );
      })}
    </div>
  );
};

这是基本的结构。我们显示了标题、作者和状态,以及更新和删除按钮。

现在,让我们添加功能。

import { useState } from "react";
import { book } from "../types/book.type";
import { useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
import { Id } from "../../convex/_generated/dataModel";
import "../styles/book.css";

export const Books = ({ books }: { books: book[] }) => {
  const [update, setUpdate] = useState(false);
  const [id, setId] = useState("");

  const deleteBooks = useMutation(api.queries.deleteBooks);
  const updateStatus = useMutation(api.queries.updateStatus);

  const handleClick = (id: string) => {
    setId(id);
    setUpdate(!update);
  };

  const handleDelete = (id: string) => {
    deleteBooks({ id: id as Id<"books"> })
      .then((mess) => console.log(mess))
      .catch((err) => console.log(err));
  };

  const handleUpdate = (e: React.FormEvent<HTMLFormElement>, id: string) => {
    e.preventDefault();
    const formdata = new FormData(e.currentTarget);
    const isCompleted: boolean =
      (formdata.get("completed") as string) === "true";
    updateStatus({ id: id as Id<"books">, isCompleted })
      .then((mess) => console.log(mess))
      .catch((err) => console.log(err));
    setUpdate(false);
  };

  return (
    <div>
      {books.map((data: book, index: number) => {
        return (
          <div
            key={data._id}
            className={`book-container ${data.isCompleted ? "completed" : "not-completed"}`}
          >
            <h3>Book no: {index + 1}</h3>
            <p>Book title: {data.title}</p>
            <p>Book Author: {data.author}</p>
            <p>
              Completed Status:{" "}
              {data.isCompleted ? "Completed" : "Not Completed"}
            </p>
            <button onClick={() => handleClick(data._id)}>Update</button>
            {id === data._id && update && (
              <>
                <form onSubmit={(e) => handleUpdate(e, data._id)}>
                  <select name="completed">
                    <option value="true">Completed</option>
                    <option value="false">Not Completed</option>
                  </select>
                  <input type="submit" />
                </form>
              </>
            )}
            <button onClick={() => handleDelete(data._id)}>delete</button>
          </div>
        );
      })}
    </div>
  );
};

这是整个组件的代码。让我解释一下我们做了什么。

우선, 업데이트를 토글하는 것이 필요하므로 handleClick 함수를 정의하고 document ID를 그에 전달했습니다.

//handleClick
 const handleClick = (id: string) => {
    setId(id);
    setUpdate(!update);
  };

handleClick 안에는 ID 상태를 更新하고 업데이트 상태를 토글할 수 있어야 합니다. 이를 통해 클릭할 때 업데이트 입력을 토글하고, 다른 클릭 시에는 닫힙니다.

次に、handleUpdate가 있습니다. 데이터를 更新하기 위해서는 document ID가 필요하므로 이벤트 오브젝트와 document ID를 그에 전달합니다. 입력을 얻기 위해서는 FormData를 사용할 수 있습니다.

const updateStatus = useMutation(api.queries.updateStatus);

const handleUpdate = (e: React.FormEvent<HTMLFormElement>, id: string) => {
    e.preventDefault();
    const formdata = new FormData(e.currentTarget);
    const isCompleted: boolean =
      (formdata.get("completed") as string) === "true";
    updateStatus({ id: id as Id<"books">, isCompleted })
      .then((mess) => console.log(mess))
      .catch((err) => console.log(err));
    setUpdate(false);
  };

useMutation를 사용하여 updateStatus 함수를 얻어야 합니다. ID와 완료 상태를 함수에 전달하고, .then를 사용하여 异synchronous 부분을 처리합니다.

삭제 함수에서는 document ID만큼이 충분합니다. 이전의 것과 마찬가지로 useMutation를 사용하여 delete function을 호출하고 ID를 그에 전달합니다.

그 다음에 document ID를 전달하고 프로미스를 처리합니다.

const deleteBooks = useMutation(api.queries.deleteBooks);

const handleDelete = (id: string) => {
    deleteBooks({ id: id as Id<"books"> })
      .then((mess) => console.log(mess))
      .catch((err) => console.log(err));
 };

스타일링

이제 남은 것은 일부 스타일링을 추가하는 것입니다. 기본적인 스타일링을 추가했습니다. If the book has not been completed, it will be in red, and if the book has been completed, it will be in green.

여기는 스크린샷입니다:

Guys, that’s it!!

My repository에서 전체 코드를 확인할 수 있습니다: convex-curd

Summary

이 글에서는 도서 컬렉션 应用程序을 만들고 CRUD(Create, Read, Update, Delete) 操作用於를 구현했습니다. 이를 시작하기 전에 Convex와 React를 설정하고 CRUD 로직을 書い었습니다.

이 튜토리얼은 frontend와 backend를 모두 涵蓋하고 서버가 없는 응용 프로그램을 어떻게 빌 수 있는지 보여주는 것입니다.

전체 코드는 여기에서 찾을 수 있습니다: convex-curd

오류가 있거나 의문이 있으면 LinkedIn, Instagram에서 연락 주세요.

읽어주셔서 감사합니다!