Convex 완벽 가이드 | 실시간 백엔드·타입 안전성·React·Serverless·실전 활용

Convex 완벽 가이드 | 실시간 백엔드·타입 안전성·React·Serverless·실전 활용

이 글의 핵심

Convex로 실시간 백엔드를 구축하는 완벽 가이드입니다. 타입 안전한 API, 실시간 구독, 파일 스토리지, 인증까지 실전 예제로 정리했습니다.

실무 경험 공유: Firebase에서 Convex로 전환하면서, 타입 안전성이 향상되고 실시간 기능이 더 강력해진 경험을 공유합니다.

들어가며: “실시간 백엔드가 복잡해요”

실무 문제 시나리오

시나리오 1: 타입 안전성이 부족해요
Firebase는 타입이 약합니다. Convex는 완벽한 타입 안전성을 제공합니다.

시나리오 2: 실시간 구독이 어려워요
WebSocket 설정이 복잡합니다. Convex는 자동으로 처리합니다.

시나리오 3: 백엔드 로직이 필요해요
클라이언트에서 처리하기 어렵습니다. Convex는 서버 함수를 제공합니다.


1. Convex란?

핵심 특징

Convex는 실시간 백엔드 플랫폼입니다.

주요 장점:

  • 타입 안전성: End-to-End TypeScript
  • 실시간: 자동 구독
  • 서버 함수: Query, Mutation, Action
  • 파일 스토리지: 내장
  • 인증: Clerk 통합

2. 프로젝트 설정

설치

npm create convex@latest

프로젝트 구조

my-convex-app/
├── convex/
│   ├── schema.ts
│   ├── users.ts
│   └── posts.ts
├── src/
│   └── app/
└── convex.json

3. Schema 정의

// convex/schema.ts
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';

export default defineSchema({
  users: defineTable({
    email: v.string(),
    name: v.string(),
    createdAt: v.number(),
  }).index('by_email', ['email']),

  posts: defineTable({
    title: v.string(),
    content: v.string(),
    authorId: v.id('users'),
    published: v.boolean(),
    createdAt: v.number(),
  })
    .index('by_author', ['authorId'])
    .index('by_published', ['published']),
});

4. Query & Mutation

Query

// convex/posts.ts
import { query } from './_generated/server';
import { v } from 'convex/values';

export const list = query({
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query('posts').collect();
  },
});

export const get = query({
  args: { id: v.id('posts') },
  handler: async (ctx, args) => {
    return await ctx.db.get(args.id);
  },
});

Mutation

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

export const create = mutation({
  args: {
    title: v.string(),
    content: v.string(),
    authorId: v.id('users'),
  },
  handler: async (ctx, args) => {
    const postId = await ctx.db.insert('posts', {
      ...args,
      published: false,
      createdAt: Date.now(),
    });

    return postId;
  },
});

export const update = mutation({
  args: {
    id: v.id('posts'),
    title: v.string(),
  },
  handler: async (ctx, args) => {
    await ctx.db.patch(args.id, { title: args.title });
  },
});

5. React 통합

Provider

// app/ConvexClientProvider.tsx
'use client';

import { ConvexProvider, ConvexReactClient } from 'convex/react';

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

export default function ConvexClientProvider({ children }) {
  return <ConvexProvider client={convex}>{children}</ConvexProvider>;
}

useQuery

'use client';

import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';

export default function Posts() {
  const posts = useQuery(api.posts.list);

  if (posts === undefined) return <div>Loading...</div>;

  return (
    <ul>
      {posts.map((post) => (
        <li key={post._id}>{post.title}</li>
      ))}
    </ul>
  );
}

useMutation

'use client';

import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';

export default function CreatePost() {
  const createPost = useMutation(api.posts.create);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);

    await createPost({
      title: formData.get('title') as string,
      content: formData.get('content') as string,
      authorId: 'user-id',
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="title" required />
      <textarea name="content" />
      <button type="submit">Create Post</button>
    </form>
  );
}

6. Action (외부 API)

// convex/actions.ts
import { action } from './_generated/server';
import { v } from 'convex/values';

export const sendEmail = action({
  args: {
    to: v.string(),
    subject: v.string(),
    body: v.string(),
  },
  handler: async (ctx, args) => {
    const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        personalizations: [{ to: [{ email: args.to }] }],
        from: { email: '[email protected]' },
        subject: args.subject,
        content: [{ type: 'text/plain', value: args.body }],
      }),
    });

    return response.ok;
  },
});

7. 파일 스토리지

// convex/files.ts
import { mutation } from './_generated/server';

export const generateUploadUrl = mutation({
  args: {},
  handler: async (ctx) => {
    return await ctx.storage.generateUploadUrl();
  },
});

export const saveFile = mutation({
  args: { storageId: v.string() },
  handler: async (ctx, args) => {
    await ctx.db.insert('files', {
      storageId: args.storageId,
      createdAt: Date.now(),
    });
  },
});

// 클라이언트
const generateUploadUrl = useMutation(api.files.generateUploadUrl);
const saveFile = useMutation(api.files.saveFile);

const handleUpload = async (file: File) => {
  const uploadUrl = await generateUploadUrl();

  const response = await fetch(uploadUrl, {
    method: 'POST',
    body: file,
  });

  const { storageId } = await response.json();
  await saveFile({ storageId });
};

정리 및 체크리스트

핵심 요약

  • Convex: 실시간 백엔드
  • 타입 안전성: End-to-End TypeScript
  • 실시간: 자동 구독
  • 서버 함수: Query, Mutation, Action
  • 파일 스토리지: 내장
  • 인증: Clerk 통합

구현 체크리스트

  • Convex 프로젝트 생성
  • Schema 정의
  • Query 구현
  • Mutation 구현
  • React 통합
  • Action 구현
  • 파일 스토리지 구현

같이 보면 좋은 글

  • Supabase 완벽 가이드
  • tRPC 완벽 가이드
  • Firebase 완벽 가이드

이 글에서 다루는 키워드

Convex, Backend, Realtime, TypeScript, React, Serverless, Database

자주 묻는 질문 (FAQ)

Q. Firebase와 비교하면 어떤가요?

A. Convex가 타입 안전성이 훨씬 좋습니다. Firebase는 더 성숙하고 기능이 많습니다.

Q. Supabase와 비교하면 어떤가요?

A. Convex가 실시간 기능이 더 강력합니다. Supabase는 PostgreSQL 기반으로 더 유연합니다.

Q. 무료로 사용할 수 있나요?

A. 네, 무료 플랜이 있습니다. 1GB 데이터, 1GB 파일 스토리지까지 무료입니다.

Q. 프로덕션에서 사용해도 되나요?

A. 네, 많은 스타트업에서 사용하고 있습니다.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3