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