spinny:~/writing $ vim trigger-dev-background-jobs-guide.md
1~2대부분의 프로덕션 애플리케이션은 요청/응답 사이클에 맞지 않는 작업이 필요합니다: 이메일 보내기, 업로드 처리, AI 파이프라인 실행, 타사 데이터 동기화, 보고서 생성. 전통적인 답은 큐(Redis, SQS, RabbitMQ), 작업자 함대, 스케줄러, 그리고 모든 배포에서 깨지는 글루 코드의 약한 더미입니다.3~4[Trigger.dev](https://trigger.dev)는 그 스택을 단일 TypeScript SDK로 압축합니다. 함수를 작성하고, 어디서나 호출하면, 플랫폼이 큐잉, 재시도, 가시성, 일정 잡기 및 내구성 있는 실행을 처리합니다. 작업은 필요한 만큼 오래 실행됩니다 - 10초 서버리스 타임아웃 없음, 재배포 시 작업 손실 없음.5~6## 왜 Trigger.dev인가7~82026년의 변화는 내구성 있는 실행입니다. 워크플로우는 재시작, 충돌, 배포 및 속도 제한에서 살아남아야 합니다. 또한 진행 상황을 실시간으로 UI에 스트리밍하고 사람의 입력을 위해 일시 중지해야 합니다. Trigger.dev는 버전 3에서 이러한 요구 사항을 중심으로 재구축되었으며 AI 인프라 표면을 계속 확장하고 있습니다.9~10```mermaid11graph LR12 App[당신의 App] -->|trigger| API[Trigger.dev API]13 API --> Queue[내구성 있는 Queue]14 Queue --> Worker[Worker 컨테이너]15 Worker -->|run task| Task[당신의 Task 코드]16 Task -->|metadata| Realtime[실시간 스트림]17 Realtime --> UI[React UI]18 Worker --> Storage[Run 상태 저장소]19```20~21모델은 단순합니다: 작업을 export로 정의하고, SDK가 이를 가져오고, 플랫폼이 격리된 컨테이너에서 일정을 잡고 실행하며, 실행 상태가 유지되어 재개, 재시도 및 관찰할 수 있습니다.22~23## 시작하기24~25### 프로젝트 초기화26~27```bash28npx trigger.dev@latest login29npx trigger.dev@latest init30```31~32이는 `trigger.config.ts` 파일과 예제 작업이 있는 `trigger/` 디렉토리를 생성합니다. config 파일은 프로젝트의 진실의 원천입니다: 어떤 디렉토리가 작업을 포함하는지, 빌드 설정, 라이프사이클 훅 및 런타임 옵션.33~34```typescript35// trigger.config.ts36import { defineConfig } from "@trigger.dev/sdk";37~38export default defineConfig({39 project: "proj_abc123",40 runtime: "node",41 logLevel: "log",42 maxDuration: 3600,43 retries: {44 enabledInDev: true,45 default: {46 maxAttempts: 3,47 factor: 2,48 minTimeoutInMs: 1000,49 maxTimeoutInMs: 30_000,50 },51 },52 dirs: ["./trigger"],53});54```55~56### 작업을 로컬에서 실행57~58```bash59npx trigger.dev@latest dev60```61~62dev 서버는 클라우드에 연결하고, 작업을 등록하고, 로컬 코드를 통해 실행을 스트리밍합니다. 에디터에 중단점을 설정하고 실제 트리거에서 적중합니다 - 일반 Node.js 프로젝트에서 사용하는 것과 동일한 루프입니다.63~64## 작업 정의65~66작업은 고유한 `id`와 `run` 함수로 export된 객체입니다. SDK는 `dirs` 전체에서 export를 검사하고 자동으로 등록합니다.67~68```typescript69// trigger/send-welcome-email.ts70import { task } from "@trigger.dev/sdk";71import { Resend } from "resend";72~73const resend = new Resend(process.env.RESEND_API_KEY);74~75export const sendWelcomeEmail = task({76 id: "send-welcome-email",77 retry: {78 maxAttempts: 5,79 factor: 1.8,80 minTimeoutInMs: 500,81 maxTimeoutInMs: 30_000,82 },83 run: async (payload: { email: string; name: string }) => {84 const { data, error } = await resend.emails.send({85 from: "hello@spinny.dev",86 to: payload.email,87 subject: `Welcome, ${payload.name}`,88 html: `<p>Glad you are here, ${payload.name}.</p>`,89 });90~91 if (error) throw error;92 return { messageId: data?.id };93 },94});95```96~97세 가지 주의 사항:98~991. **run 본문에 타임아웃 없음.** 플랫폼은 런타임이 아닌 config의 `maxDuration`을 통해 실행 시간을 관리합니다.1002. **Throw는 재시도입니다.** SDK는 예외를 잡고 `retry` 정책에 따라 지수 백오프로 다시 실행합니다.1013. **반환 값은 유지됩니다.** 다른 작업과 프론트엔드는 어디서나 `run.output`을 읽을 수 있습니다.102~103## 작업 트리거104~105백엔드, API 라우트 또는 다른 작업에서 작업을 호출합니다.106~107```typescript108import { sendWelcomeEmail } from "@/trigger/send-welcome-email";109~110const handle = await sendWelcomeEmail.trigger(111 { email: "user@example.com", name: "Alex" },112 {113 idempotencyKey: `welcome-${userId}`,114 concurrencyKey: `tenant-${tenantId}`,115 queue: { name: "emails", concurrencyLimit: 50 },116 delay: "30s",117 ttl: "10m",118 }119);120~121console.log(handle.id); // run_xyz - 진행 상황을 추적하거나 표시하는 데 사용122```123~124옵션은 한 번의 호출로 많은 동작을 잠금 해제합니다:125~126- **`idempotencyKey`** - 같은 키의 실행이 이미 존재하면, SDK는 작업을 복제하는 대신 기존 핸들을 반환합니다.127- **`concurrencyKey`** - 키를 공유하는 실행을 직렬화하여 테넌트당 속도 제한을 초과하지 않도록 합니다.128- **`queue.concurrencyLimit`** - 모든 키에서 큐의 글로벌 캡.129- **`delay`** - 실행을 미래의 시간으로 일정을 잡습니다.130- **`ttl`** - 그때까지 실행이 시작되지 않으면 자동으로 만료시킵니다.131~132### Batch trigger133~134팬아웃 워크로드의 경우, `batchTrigger`는 호출당 최대 500개 항목을 받아들이고 항목당 하나의 실행을 생성합니다.135~136```typescript137await sendWelcomeEmail.batchTrigger(138 newUsers.map((u) => ({139 payload: { email: u.email, name: u.name },140 options: { idempotencyKey: `welcome-${u.id}` },141 }))142);143```144~145## 일정이 잡힌 작업146~147Cron 작업은 일급 선언이 됩니다. 일정 자체는 작업에 여러 번 첨부할 수 있는 별도의 객체입니다.148~149```typescript150// trigger/daily-digest.ts151import { schedules } from "@trigger.dev/sdk";152~153export const dailyDigest = schedules.task({154 id: "daily-digest",155 cron: "0 9 * * *",156 run: async (payload) => {157 console.log("Scheduled at:", payload.timestamp);158 console.log("Last run:", payload.lastTimestamp);159 console.log("Timezone:", payload.timezone);160 console.log("Next 5 runs:", payload.upcoming);161~162 await sendDigestForDate(payload.timestamp);163 },164});165```166~167테넌트별 일정 - 예를 들어 고객당 하나의 cron - 의 경우, 관리 API를 통해 동적으로 생성합니다.168~169```typescript170import { schedules } from "@trigger.dev/sdk";171~172await schedules.create({173 task: "daily-digest",174 cron: "0 9 * * *",175 timezone: "America/New_York",176 externalId: `customer_${customerId}`,177 deduplicationKey: `digest-${customerId}`,178});179```180~181`deduplicationKey`는 호출을 멱등하게 만듭니다: 배포 시 같은 코드를 다시 실행해도 중복된 일정을 쌓지 않습니다.182~183## 큐, 동시성 및 멱등성184~185세 가지 기본 요소가 대부분의 속도 제한 및 순서 지정 요구 사항을 다룹니다.186~187```mermaid188graph TB189 Trigger[trigger payload] --> IK{idempotencyKey<br/>본 적 있나?}190 IK -->|예| Reuse[기존 run 반환]191 IK -->|아니오| CK[concurrencyKey 버킷]192 CK --> Q[concurrencyLimit이 있는<br/>큐]193 Q -->|슬롯 사용 가능| Run[task 실행]194 Q -->|슬롯 가득| Wait[큐에서 대기]195```196~197일반적인 패턴: 벤더의 속도 제한을 존중하기 위해 테넌트당 하나의 큐와 키별 작은 동시성, 재시도를 안전하게 만들기 위한 멱등성 키.198~199```typescript200await syncShopifyOrders.trigger(201 { shopId },202 {203 queue: { name: `shopify-${shopId}`, concurrencyLimit: 2 },204 concurrencyKey: shopId,205 idempotencyKey: `sync-${shopId}-${Date.now() / 60_000 | 0}`,206 }207);208```209~210## 대기 및 장기 실행 작업211~212작업은 연결을 유지하거나 컴퓨팅을 태우지 않고 일시 중지할 수 있습니다. 플랫폼은 상태를 유지하고 대기가 완료되면 함수를 재개합니다.213~214```typescript215import { wait } from "@trigger.dev/sdk";216~217export const onboarding = task({218 id: "onboarding",219 run: async (payload: { userId: string }) => {220 await sendWelcomeEmail.triggerAndWait({ userId: payload.userId });221 await wait.for({ days: 1 });222 await sendTipsEmail.trigger({ userId: payload.userId });223 await wait.until({ date: oneWeekFromSignup(payload.userId) });224 await sendUpgradeOffer.trigger({ userId: payload.userId });225 },226});227```228~229`triggerAndWait`는 킬러 기능입니다: 자식 작업을 트리거하고 자식이 완료될 때까지 부모를 일시 중지합니다. async 함수처럼 작업을 구성하지만, 오케스트레이션은 며칠 또는 몇 주에 걸쳐 내구성 있게 실행됩니다.230~231### `wait.forToken`을 사용한 Human-in-the-loop232~233승인 흐름 및 AI 게이트의 경우, `wait.forToken`은 애플리케이션이 결과로 콜백할 때까지 일시 중지합니다.234~235```typescript236import { task, wait } from "@trigger.dev/sdk";237~238export const publishPost = task({239 id: "publish-post",240 run: async (payload: { draftId: string }) => {241 const draft = await generateAIContent(payload.draftId);242~243 const token = await wait.createToken({ timeout: "7d" });244 await notifyEditor({ draftId: draft.id, token: token.id });245~246 const decision = await wait.forToken<{ approved: boolean; notes?: string }>(247 token.id248 );249~250 if (decision.approved) {251 return await publish(draft);252 }253 return await applyFeedback(draft, decision.notes);254 },255});256```257~258편집자는 UI를 열고, 초안을 검토하고, 승인을 클릭하고, 백엔드는 토큰을 완료합니다. 작업은 중단된 곳에서 재개됩니다 - 몇 시간 또는 며칠이 지났더라도.259~260## 라이프사이클 훅261~262`init`, `onStart`, `onSuccess` 및 `onFailure`를 작업에 또는 `trigger.config.ts`에서 전역적으로 첨부할 수 있습니다. 추적, 오류 보고 및 공유 설정에 사용합니다.263~264```typescript265// trigger.config.ts266export default defineConfig({267 // ...268 init: async () => {269 Sentry.init({ dsn: process.env.SENTRY_DSN });270 },271 onFailure: async ({ error, ctx }) => {272 Sentry.captureException(error, {273 tags: { taskId: ctx.task.id, runId: ctx.run.id },274 });275 },276});277```278~279`init`은 부팅 시 작업자 컨테이너당 한 번 실행되며, 실행당이 아니므로 클라이언트와 풀을 설정하기에 적합한 곳입니다.280~281## 프론트엔드의 실시간282~283Trigger.dev는 실행 상태 변경 - 상태, 메타데이터, 출력 - 을 스트리밍 API로 게시합니다. React 훅은 해당 스트림을 구독하고 자동으로 다시 렌더링합니다.284~285```typescript286// trigger/process-video.ts287import { task, metadata } from "@trigger.dev/sdk";288~289export const processVideo = task({290 id: "process-video",291 run: async (payload: { videoId: string }) => {292 metadata.set("stage", "transcoding");293 await transcode(payload.videoId);294~295 metadata.set("stage", "thumbnails");296 await generateThumbnails(payload.videoId);297~298 metadata.set("stage", "uploading");299 const url = await uploadToCDN(payload.videoId);300~301 return { url };302 },303});304```305~306```tsx307// components/VideoStatus.tsx308"use client";309import { useRealtimeRun } from "@trigger.dev/react-hooks";310import type { processVideo } from "@/trigger/process-video";311~312export function VideoStatus({313 runId,314 publicAccessToken,315}: {316 runId: string;317 publicAccessToken: string;318}) {319 const { run, error } = useRealtimeRun<typeof processVideo>(runId, {320 accessToken: publicAccessToken,321 });322~323 if (error) return <p>Error: {error.message}</p>;324 if (!run) return <p>Loading...</p>;325~326 return (327 <div>328 <p>Status: {run.status}</p>329 <p>Stage: {String(run.metadata?.stage ?? "queued")}</p>330 {run.output?.url && <video src={run.output.url} controls />}331 </div>332 );333}334```335~336특정 실행으로 범위가 지정된 공개 액세스 토큰을 서버 측에서 생성하고 클라이언트에 전송합니다. 훅은 인증, 재연결 및 증분 업데이트를 처리합니다.337~338원샷 트리거 및 구독:339~340```tsx341import { useRealtimeTaskTrigger } from "@trigger.dev/react-hooks";342~343const { submit, run, isLoading } = useRealtimeTaskTrigger<typeof processVideo>(344 "process-video",345 { accessToken: publicAccessToken }346);347~348<button onClick={() => submit({ videoId })} disabled={isLoading}>349 Process video350</button>;351```352~353## AI 에이전트 및 스트리밍354~355Trigger.dev는 AI 에이전트를 위한 인기 있는 런타임이 되었습니다. 같은 기본 요소들 - 내구성 있는 실행, 재시도, 대기, 실시간 메타데이터, human-in-the-loop - 이 정확히 에이전트가 필요로 하는 것이기 때문입니다. 실행이 진행되는 동안 모델 제공자의 토큰을 `metadata`로 스트리밍하면, 프론트엔드가 이를 라이브로 렌더링하고, 실행은 서버리스 타임아웃을 태우지 않고도 장기 실행 도구 호출을 살아남습니다.356~357```typescript358import { task, metadata } from "@trigger.dev/sdk";359import { streamText } from "ai";360import { anthropic } from "@ai-sdk/anthropic";361~362export const researchAgent = task({363 id: "research-agent",364 maxDuration: 1800,365 run: async (payload: { question: string }) => {366 const result = streamText({367 model: anthropic("claude-opus-4-7"),368 system: "You are a research assistant. Use the web.",369 prompt: payload.question,370 tools: { webSearch },371 });372~373 let fullText = "";374 for await (const chunk of result.textStream) {375 fullText += chunk;376 metadata.set("partial", fullText);377 }378~379 return { answer: fullText, usage: await result.usage };380 },381});382```383~384프론트엔드는 `useRealtimeRun`을 사용하고 `run.metadata.partial`을 읽어 스트리밍 응답을 렌더링합니다. 채팅 완료를 렌더링하는 것과 같은 방식이지만, 이것은 전체 페이지 새로고침에서도 살아남습니다.385~386## 배포387~388배포는 작업을 버전이 지정된 번들로 컴파일하고, 컨테이너를 빌드하고, 트래픽을 원자적으로 교체합니다. 이전의 진행 중인 실행은 이전 버전을 계속 사용합니다.389~390```bash391npx trigger.dev@latest deploy --env prod392```393~394CI에서는 일반적으로 이를 앱을 배포하는 동일한 워크플로우에 연결합니다:395~396```yaml397# .github/workflows/deploy.yml398- name: Deploy Trigger.dev399 env:400 TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}401 run: npx trigger.dev@latest deploy --env prod402```403~404미리보기 환경의 경우, `--env preview --branch ${{ github.head_ref }}`를 전달하면 Trigger.dev가 분기당 격리된 환경을 생성하여 Vercel이 미리보기 배포를 처리하는 방식을 반영합니다.405~406## 셀프 호스팅 vs 클라우드407~408Trigger.dev는 Apache 2.0 라이선스 하에 오픈 소스입니다. 모든 컨테이너 플랫폼(Docker Compose, Kubernetes, Fly.io)에서 셀프 호스팅하거나 trigger.dev에서 관리되는 클라우드를 사용할 수 있습니다.409~410| 측면 | 클라우드 | 셀프 호스팅 |411|--------|-------|-------------|412| **설정** | 가입, `init` 실행 | docker-compose 또는 Helm chart 실행 |413| **확장** | 자동 | 당신의 책임 |414| **가격** | 실행당 + 컴퓨팅당 | 인프라 비용만 |415| **준수** | SOC 2 | 환경이 제공하는 것 |416| **최적** | 대부분의 팀 | 엄격한 데이터 거주, 사용자 정의 인프라 |417~418SDK와 CLI는 모드 간에 동일합니다 - 프로필 플래그를 변경하고 자신의 인스턴스를 가리킵니다.419~420## 모범 사례421~422### 1. 페이로드를 작고 직렬화 가능하게 유지423~424전체 객체가 아닌 ID와 참조를 전달하세요. 작업 내부에서 데이터를 가져오세요. 이렇게 하면 큐가 작게 유지되고, 페이로드가 로깅하기 저렴하며, 다시 트리거하지 않고도 데이터 소스를 변경할 수 있습니다.425~426### 2. 모든 외부 호출에 멱등성 키427~428작업 트리거의 `idempotencyKey`를 벤더 API(Stripe, OpenAI 등)의 멱등성 키와 결합하세요. 재시도는 종단 간 안전합니다.429~430### 3. 오케스트레이션에 `triggerAndWait`를 사용, 트리거의 `Promise.all`이 아님431~432`triggerAndWait`를 호출하는 부모는 자식 작업을 내구성 있게 구성합니다. 트리거하고 즉시 해결하는 부모는 체인의 가시성을 잃습니다.433~434### 4. 실행에 태그 지정435~436트리거에 `tags`를 추가하세요(`tags: ["user:123", "feature:onboarding"]`). 그러면 비즈니스 차원으로 대시보드와 관리 API를 필터링할 수 있습니다.437~438### 5. `init`을 멱등하게 유지439~440모든 콜드 스타트에서 실행됩니다. 마이그레이션이나 일회성 부작용을 거기에 두지 마세요.441~442## 결론443~444Trigger.dev는 이전에 처음부터 작업 시스템을 구축해야 했던 작업 카테고리를 제거합니다. async TypeScript를 작성하고, 어디서나 호출하면, 플랫폼은 내구성 있는 실행, 일정 잡기, 큐, 재시도, 실시간 업데이트 및 human-in-the-loop 패턴을 즉시 제공합니다.445~446야간 cron을 구동하는 동일한 표면이 프론트엔드로 스트리밍하고 검토를 위해 일시 중지되는 다단계 AI 에이전트를 구동하는 표면입니다. 이러한 융합이 신뢰할 수 있는 백그라운드 작업이 필요한 SaaS를 운영하든, 서버리스 타임아웃을 넘어 살아남는 AI 기능을 출시하든, 프레임워크를 2026년에 진지하게 살펴볼 가치가 있게 만드는 것입니다.447~448> **시작하기 체크리스트:**449>450> - [x] trigger.dev에 가입하거나 셀프 호스팅 Docker 스택 실행451> - [x] 프로젝트에서 `npx trigger.dev@latest init`452> - [x] `task({ id, run })`로 첫 번째 작업 정의453> - [x] API에서 트리거하고 대시보드에서 실행 보기454> - [x] 프로덕션 안전을 위해 `idempotencyKey` 및 `concurrencyKey` 추가455> - [x] `useRealtimeRun`을 상태 컴포넌트에 연결456> - [x] CI에서 `trigger.dev deploy --env prod`로 배포457~
NORMAL · trigger-dev-background-jobs-guide.md [readonly]457 lines · :q to close