Appwrite Patterns은(는) 무엇을 하나요?
빠른 가이드: Appwrite를 오픈소스 BaaS로 사용하여 인증, 구조화된 데이터(TablesDB), 파일 스토리지, 서버리스 함수, 실시간 구독을 구현하세요. 항상 공유
Client인스턴스에서 서비스 클래스를 초기화하고, 모든 행에 명시적으로 권한을 설정하며(기본적으로 아무것도 접근 불가), 백엔드에서는 API 키 인증을 사용하는 서버 SDK(node-appwrite)만 사용하세요.
<critical_requirements>
중요: 이 스킬을 사용하기 전에
모든 코드는 CLAUDE.md의 프로젝트 규칙을 따라야 합니다(kebab-case, 명명된 내보내기, import 순서,
import type, 명명된 상수).
(모든 행과 파일에 명시적으로 권한을 설정해야 합니다. Appwrite는 기본적으로 접근을 허용하지 않으므로, 권한을 생략하면 데이터에 접근할 수 없습니다)
(서버에서는 node-appwrite와 API 키 인증을, 클라이언트에서는 appwrite와 세션 인증을 사용해야 합니다. 클라이언트 번들에 API 키를 절대 노출하지 마세요)
(모든 SDK 호출에서 항상 AppwriteException을 확인해야 합니다. Appwrite는 예외를 던지며, { data, error } 튜플을 반환하지 않습니다)
(자동 생성 ID에는 항상 ID.unique()를 사용하세요. 원시 문자열을 전달하면 사용자 정의 ID가 생성되며, 자동 생성되지 않습니다)
(새로운 프로젝트에는 TablesDB API를 사용해야 합니다. 레거시 Databases/collections/documents API는 더 이상 사용되지 않으며 보안 패치만 받습니다)
</critical_requirements>
자동 감지 키워드: Appwrite, appwrite, node-appwrite, TablesDB, Account, Databases, Permission, Role, ID.unique, Query.equal, createEmailPasswordSession, realtime.subscribe, Channel.files, Channel.tablesdb, APPWRITE_FUNCTION
사용 시기:
- TypeScript로 Appwrite 클라이언트 또는 서버 SDK 설정
- 인증 구현 (이메일/비밀번호, OAuth, 매직 URL, 전화 OTP, 익명 세션)
- TablesDB 테이블 CRUD (행, 쿼리, 권한)
- 접근 제어가 포함된 Storage 버킷 파일 업로드 및 제공
- 이벤트, 스케줄 또는 HTTP로 트리거되는 서버리스 함수 구축
- 행, 파일 또는 계정 이벤트에 대한 실시간 변경 구독
- 팀 멤버십 및 역할 기반 권한 관리
주요 패턴:
- 타입화된 서비스 클래스를 사용한 클라이언트 및 서버 SDK 초기화
- 인증 흐름: 회원가입, 로그인, OAuth, 매직 URL, 세션 관리
Query클래스를 사용한 TablesDB 작업 (필터링, 페이지네이션, 정렬)- 권한 모델:
Permission.read(Role.any()),Role.user(),Role.team() - Storage: 업로드, 다운로드, 이미지 변환이 포함된 미리보기, 버킷 권한
- 서버리스 함수:
export default async ({ req, res, log, error }) => {} Channel헬퍼와realtime.subscribe()를 통한 실시간 구독
사용하지 말아야 할 경우:
- 직접 데이터베이스 연결 또는 SQL 쿼리 (Appwrite는 API 전용, 원시 SQL 없음)
- 여러 테이블에 걸친 복잡한 관계형 조인 (Appwrite는 관계를 지원하지만 임의 SQL 조인은 지원하지 않음)
- 서버 측 실시간이 필요한 애플리케이션 (Appwrite Realtime은 클라이언트 SDK 전용)
상세 리소스:
- 의사 결정 프레임워크 및 안티패턴은 reference.md 참조
클라이언트 및 쿼리:
- examples/core.md — 클라이언트 설정, TablesDB CRUD, 에러 처리, 권한
인증:
- examples/auth.md — 전체 인증 흐름, OAuth, 매직 URL, 세션, 팀
Storage 및 Functions:
- examples/storage.md — 파일 업로드, 다운로드, 미리보기, 버킷 권한
- examples/functions.md — 서버리스 함수, 트리거, 함수 내 SDK 사용
실시간:
- examples/realtime.md — 채널 구독, 이벤트 필터링, 정리
철학
Appwrite는 인증, 데이터베이스(TablesDB), 파일 스토리지, 서버리스 함수, 실시간 기능을 통합 SDK로 제공하는 오픈소스 백엔드 서비스입니다. 셀프 호스팅하거나 Appwrite Cloud를 통해 사용할 수 있습니다.
핵심 원칙:
- 기본 보안 — 명시적인 권한 없이는 어떤 것에도 접근할 수 없습니다. 모든 행과 파일에 권한을 설정해야 하며, 그렇지 않으면 모든 사용자에게 보이지 않습니다. 이는 "기본적으로 열려 있음"과 반대되는 개념입니다. 필수 단계로 취급하세요.
- 서비스 클래스 아키텍처 — 모든 SDK 상호작용은 공유
Client에서 인스턴스화된 서비스 클래스(Account,TablesDB,Storage,Functions,Teams,Realtime)를 통해 이루어집니다. 이 패턴은 클라이언트와 서버 SDK에서 일관됩니다. - 두 개의 SDK 분리 —
appwrite(클라이언트)는 브라우저용 세션 기반 인증을 사용합니다.node-appwrite(서버)는 백엔드용 API 키 인증을 사용합니다. 동일한 API 모양을 공유하지만 인증 메커니즘이 다릅니다. 절대 혼용하지 마세요. - 튜플이 아닌 예외 — Appwrite는 실패 시
{ data, error }튜플이 아닌AppwriteException을 던집니다. 항상 try/catch로 호출을 감싸세요. - TablesDB가 미래 — Appwrite 1.8은 TablesDB(테이블/행/열)를 최신 API로 도입했습니다. 레거시 Databases API(컬렉션/문서/속성)는 여전히 작동하지만 더 이상 사용되지 않습니다. 새로운 기능은 TablesDB에만 추가됩니다.
- ID 생성 — 서버 생성 고유 ID에는
ID.unique()를 사용하세요. 문자열을 직접 전달하면 사용자 정의 ID가 되며(테이블 내에서 전역적으로 고유해야 함).
Appwrite 사용 시기:
- 데이터를 완전히 제어할 수 있는 오픈소스, 셀프 호스팅 BaaS
- 하나의 플랫폼에서 인증, 데이터베이스, 스토리지, 함수가 필요한 프로젝트
- 공급업체 종속 없이 셀프 호스팅 가능한 BaaS를 원하는 팀
- 세분화된 문서/행 수준 권한이 필요한 애플리케이션
사용하지 말아야 할 경우:
- 조인, 뷰, 저장 프로시저가 필요한 복잡한 SQL 중심 애플리케이션
- 서버 측 실시간 소비자 (Appwrite Realtime은 클라이언트 SDK 전용, 서버 SDK 지원 없음)
- 로컬 우선 동기화가 필요한 오프라인 우선 앱 (Appwrite에는 내장 오프라인 동기화 없음)
핵심 패턴
패턴 1: 클라이언트 SDK 설정 (브라우저)
엔드포인트와 프로젝트 ID로 Appwrite 클라이언트를 초기화합니다. 모든 서비스 클래스는 단일 Client 인스턴스를 공유합니다.
// lib/appwrite.ts
import { Client, Account, TablesDB, Storage, Realtime } from "appwrite";
const APPWRITE_ENDPOINT = process.env.APPWRITE_ENDPOINT!;
const APPWRITE_PROJECT_ID = process.env.APPWRITE_PROJECT_ID!;
const client = new Client()
.setEndpoint(APPWRITE_ENDPOINT)
.setProject(APPWRITE_PROJECT_ID);
export const account = new Account(client);
export const tablesDB = new TablesDB(client);
export const storage = new Storage(client);
export const realtime = new Realtime(client);
좋은 이유: 단일 클라이언트 인스턴스를 서비스 전체에서 공유, 설정을 위한 명명된 상수, 각 서비스를 명명된 내보내기로 제공, 환경 변수로 코드에서 설정 분리
// 잘못된 예: 하드코딩된 설정, 클라이언트 재생성
import { Client, Account } from "appwrite";
const account = new Account(
new Client()
.setEndpoint("https://cloud.appwrite.io/v1") // 하드코딩
.setProject("abc123"), // 하드코딩된 프로젝트 ID
);
잘못된 이유: 하드코딩된 엔드포인트와 프로젝트 ID, 서비스당 새 Client 인스턴스는 리소스 낭비, 공유 설정 없음
패턴 2: 서버 SDK 설정 (Node.js)
서버 SDK는 API 키 인증을 사용합니다. 클라이언트 코드에 API 키를 절대 노출하지 마세요.
// lib/appwrite-server.ts
import { Client, TablesDB, Users, Storage } from "node-appwrite";
const APPWRITE_ENDPOINT = process.env.APPWRITE_ENDPOINT!;
const APPWRITE_PROJECT_ID = process.env.APPWRITE_PROJECT_ID!;
const APPWRITE_API_KEY = process.env.APPWRITE_API_KEY!;
const client = new Client()
.setEndpoint(APPWRITE_ENDPOINT)
.setProject(APPWRITE_PROJECT_ID)
.setKey(APPWRITE_API_KEY);
export const tablesDB = new TablesDB(client);
export const users = new Users(client);
export const storage = new Storage(client);
좋은 이유: .setKey()로 API 키 인증(서버 전용), Users 서비스로 관리자 사용자 관리(클라이언트 SDK에서는 사용 불가), 명명된 상수
사용 시기: API 라우트, 서버리스 함수, 관리 스크립트, 웹훅. 이 모듈을 클라이언트 측 코드에서 절대 import하지 마세요.
패턴 3: 에러 처리 (AppwriteException)
Appwrite는 AppwriteException을 던집니다. { data, error } 튜플을 반환하지 않습니다. 항상 try/catch를 사용하세요.
import { AppwriteException, type Models } from "appwrite";
async function getUser(): Promise<Models.User<Models.Preferences>> {
try {
return await account.get();
} catch (error) {
if (error instanceof AppwriteException) {
// error.message — 사람이 읽을 수 있는 메시지
// error.code — HTTP 상태 코드 (401, 404 등)
// error.type — 기계가 읽을 수 있는 에러 유형 문자열
throw new Error(`Appwrite 오류 ${error.code}: ${error.message}`);
}
throw error;
}
}
좋은 이유: 타입이 좁혀진 AppwriteException은 code, message, type을 제공, 알 수 없는 오류는 다시 던짐, Models 네임스페이스를 사용한 명시적 반환 타입
// 잘못된 예: { data, error } 튜플 반환 가정
const { data, error } = await account.get(); // 잘못됨 — Appwrite는 튜플을 반환하지 않고 던짐
if (error) {
/* ... */
}
잘못된 이유: Appwrite는 에러 튜플을 반환하지 않음. account.get()은 데이터를 반환하거나 예외를 던지므로 이 코드는 조용히 실패함
패턴 4: TablesDB 연산 (CRUD)
모든 데이터베이스 연산에 TablesDB를 사용하세요. 생성 시 행 ID가 필요하며, 자동 생성에는 ID.unique()를 사용하세요.
행 생성
import { ID, Permission, Role } from "appwrite";
interface Todo {
title: string;
completed: boolean;
}
const DATABASE_ID = "main";
const TABLE_ID = "todos";
const row = await tablesDB.createRow<Todo>({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
rowId: ID.unique(),
data: {
title: "장보기",
completed: false,
},
permissions: [
Permission.read(Role.user(userId)),
Permission.update(Role.user(userId)),
Permission.delete(Role.user(userId)),
],
});
쿼리로 행 목록 조회
import { Query } from "appwrite";
const PAGE_SIZE = 25;
const result = await tablesDB.listRows<Todo>({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
queries: [
Query.equal("completed", false),
Query.orderDesc("$createdAt"),
Query.limit(PAGE_SIZE),
],
});
// result.rows — Todo 행 배열
// result.total — 일치하는 총 개수
행 업데이트
const updated = await tablesDB.updateRow<Todo>({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
rowId: row.$id,
data: { completed: true },
});
행 삭제
await tablesDB.deleteRow({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
rowId: row.$id,
});
좋은 이유: 데이터베이스/테이블 ID를 위한 명명된 상수, 자동 생성 ID를 위한 ID.unique(), 생성 시 명시적 권한 설정, 타입 세이프 필터링을 위한 Query 클래스, 행 형태를 위한 제네릭 타입 매개변수
패턴 5: 권한 모델
Appwrite는 기본적으로 접근을 허용하지 않습니다. 모든 행과 파일에 권한을 설정해야 합니다.
Permission + Role 구문
import { Permission, Role } from "appwrite";
// 공개 읽기, 소유자 전체 접근
const publicReadPermissions = [
Permission.read(Role.any()),
Permission.update(Role.user(userId)),
Permission.delete(Role.user(userId)),
];
// 팀 접근
const teamPermissions = [
Permission.read(Role.team(teamId)),
Permission.update(Role.team(teamId, "admin")),
Permission.delete(Role.team(teamId, "owner")),
];
// 인증된 사용자만
const verifiedOnlyPermissions = [Permission.read(Role.users("verified"))];
일반적인 Role 헬퍼
Role.any(); //任何人 (게스트 포함)
Role.guests(); // 인증되지 않은 사용자만
Role.users(); // 모든 인증된 사용자
Role.users("verified"); // 인증된 사용자만
Role.user(userId); // 특정 사용자 ID
Role.team(teamId); // 팀의 모든 구성원
Role.team(teamId, "admin"); // "admin" 역할의 팀 구성원
Role.member(membershipId); // 특정 팀 멤버십
Role.label("premium"); // "premium" 레이블이 있는 사용자
좋은 이유: 모든 리소스에 명시적 권한, 역할 헬퍼는 타입 세이프, 팀 역할로 세분화된 접근 제어 가능
// 잘못된 예: 권한 없이 행 생성
const row = await tablesDB.createRow({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
rowId: ID.unique(),
data: { title: "비밀" },
// 권한이 설정되지 않음 — 이 행은 모든 사람에게 보이지 않습니다!
});
잘못된 이유: 권한이 없으면 행은 데이터베이스에 존재하지만 어떤 사용자도 읽기, 업데이트, 삭제할 수 없습니다(생성자조차도). 이것은 가장 흔한 Appwrite 실수입니다.
패턴 6: 인증 흐름
모든 인증 작업에는 Account 서비스를 사용하세요.
이메일/비밀번호 회원가입 및 로그인
import { ID, type Models } from "appwrite";
// 회원가입: 계정 생성 + 세션 생성 (두 단계)
const user = await account.create({
userId: ID.unique(),
email,
password,
name,
});
const session = await account.createEmailPasswordSession({ email, password });
// 로그인
const session = await account.createEmailPasswordSession({ email, password });
// 로그아웃 (현재 기기)
await account.deleteSession({ sessionId: "current" });
좋은 이유: 두 단계 회원가입 (계정 생성 + 세션 생성), 모든 SDK 호출에 객체 매개변수, 현재 기기만 로그아웃하는 deleteSession({ sessionId: "current" })
전체 인증 흐름 (OAuth, 매직 URL, 익명 세션, 팀, 비밀번호 재설정 포함)은 examples/auth.md를 참조하세요.
패턴 7: Storage 연산
Storage 서비스를 사용하여 파일을 업로드, 다운로드, 미리보기하세요.
import { ID, Permission, Role } from "appwrite";
const BUCKET_ID = "user-uploads";
const PREVIEW_WIDTH = 400;
const PREVIEW_HEIGHT = 300;
// 권한과 함께 업로드
await storage.createFile({
bucketId: BUCKET_ID,
fileId: ID.unique(),
file,
permissions: [
Permission.read(Role.user(userId)),
Permission.update(Role.user(userId)),
],
});
// 변환이 포함된 이미지 미리보기
const url = storage.getFilePreview({
bucketId: BUCKET_ID,
fileId: file.$id,
width: PREVIEW_WIDTH,
height: PREVIEW_HEIGHT,
quality: 80,
});
// 파일 다운로드
const blob = await storage.getFileDownload({
bucketId: BUCKET_ID,
fileId: file.$id,
});
자세한 내용은 examples/storage.md를 참조하세요.