본문으로 건너뛰기 Convex 완벽 가이드 | 실시간 백엔드·타입 안전성·React·Serverless·실전 활용

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

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

이 글의 핵심

Convex로 실시간 백엔드를 구축하는 완벽 가이드. 타입 안전한 API, 실시간 구독, 파일 스토리지, 인증까지 실전 예제로 정리. Convex·Backend·Realtime 중심으로 설명합니다. Start now.

이 글의 핵심

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

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

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

실무에서 마주치는 문제들

타입 안전성이 부족해요

Firebase는 타입이 약합니다. Convex는 완벽한 타입 안전성을 제공합니다. 실시간 구독이 어려워요

WebSocket 설정이 복잡합니다. Convex는 자동으로 처리합니다. 백엔드 로직이 필요해요

클라이언트에서 처리하기 어렵습니다. 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: 'noreply@example.com' },
        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 구현
  • 파일 스토리지 구현

같이 보면 좋은 글


이 글에서 다루는 키워드

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. 네, 많은 스타트업에서 사용하고 있습니다.