Trigger.dev 완벽 가이드 — 코드 우선 백그라운드 작업·큐·Next.js 실전 파이프라인
이 글의 핵심
Trigger.dev는 백그라운드 작업을 애플리케이션 코드(TypeScript) 안에서 선언하고, 클라우드에서 실행·관측하는 코드 우선 작업 플랫폼입니다. Task·트리거·재시도·스케줄·웹훅·Next.js 통합과 실무 파이프라인 패턴을 한 번에 정리합니다.
이 글의 핵심
Trigger.dev는 API 요청 스레드에서 분리해 오래 걸리는 작업, 재시도가 필요한 작업, 스케줄·외부 이벤트에 반응하는 작업을 타입스크립트 코드로 선언하고, 관리형 인프라에서 실행·로그·재시도까지 다루게 해 주는 코드 우선(code-first) 백그라운드 작업 플랫폼입니다.
이 글에서는 다음을 실무 관점에서 연결해 설명합니다.
- 핵심 개념: 태스크, 트리거, 런(run), 페이로드, 대시보드 관측
- Jobs와 Triggers: v3 SDK 기준으로 Task가 “잡에 해당하는 실행 단위”이며, 트리거는 그 태스크를 언제·어떤 입력으로 돌릴지 결정하는 진입점
- 재시도·타임아웃:
retry,maxDuration,ttl, 큐·머신 옵션 - 스케줄링: 크론·예약 실행 패턴
- 웹훅·이벤트: HTTP 웹훅 수신 후 태스크 실행, 애플리케이션 이벤트와의 연결
- Next.js 통합: App Router Route Handler, Server Actions, 환경 변수, 배포 시 주의점
- 실전 데이터 파이프라인: 수집·변환·적재(ETL)에 가까운 단계형 작업 예시
참고: CLI·대시보드 UI·플랜 제한은 시기에 따라 달라질 수 있습니다. 운영 전 공식 문서와 프로젝트 설정 화면을 함께 확인하시기 바랍니다.
1. Trigger.dev의 핵심 개념
1-1. “코드 우선”이 의미하는 것
전통적인 백그라운드 작업 구축은 큐 브로커(Redis, SQS 등), 워커 프로세스, 배포 파이프라인, 재시도 정책을 각각 조합해야 합니다. Trigger.dev는 이런 조합을 제품화하여, 개발자가 주로 태스크 코드와 설정(trigger.config.ts)에 집중하도록 합니다.
- 선언적 태스크:
task({ id, run })형태로 실행 로직을 한곳에 모읍니다. - 관측 가능성: 실행 단위(런) 단위로 상태·로그를 추적하기 쉬운 UX를 지향합니다.
- 타입스크립트 친화: 페이로드·결과를 JSON 직렬화 가능한 형태로 다루며, SDK 타입을 활용할 수 있습니다.
1-2. 런(Run)과 페이로드(Payload)
태스크가 한 번 실행될 때마다 런이 생성됩니다. 런은 입력 페이로드, 상태, 시도(attempt), 로그의 묶음으로 이해하면 됩니다. API 라우트에서 tasks.trigger(...)를 호출하면 비동기로 실행이 예약되고, HTTP 응답은 보통 핸들(id 등)을 빠르게 반환합니다. 즉 사용자 요청 스레드와 실제 작업 실행이 분리됩니다.
1-3. 서버리스 함수와의 역할 분담
Vercel Functions, AWS Lambda 같은 짧은 요청-응답 모델은 사용자 인터랙션 처리에 적합합니다. 반면 대용량 파일 처리, 외부 API의 속도 제한 대응, 여러 단계로 나뉜 파이프라인은 실행 시간·재시도·동시성 제어가 핵심입니다. Trigger.dev는 이런 “요청 밖에서 돌아가야 하는 코드”를 견고하게 옮기기 위한 선택지입니다.
2. Jobs와 Triggers: 용어 정리
2-1. Job에 해당하는 것: Task
문서와 커뮤니티에서는 “백그라운드 잡”이라는 말을 흔히 쓰지만, Trigger.dev v3 SDK에서는 실행 단위를 task로 정의합니다. 따라서 이 글에서는 다음처럼 대응합니다.
| 일반적인 표현 | Trigger.dev v3에서의 대응 |
|---|---|
| 백그라운드 잡(job) | task({ ... })로 정의한 태스크 |
| 잡 실행 1회 | 런(run) |
| 잡 정의(이름/종류) | 태스크 id |
태스크 id는 프로젝트 안에서 고유해야 하며, 대시보드 검색·tasks.trigger("task-id", payload) 호출에 그대로 쓰입니다.
2-2. Trigger: 태스크를 “시동”하는 방법
트리거는 태스크를 시작하는 경로를 의미합니다. 대표적으로 다음이 있습니다.
- 애플리케이션 코드에서 트리거:
helloWorld.trigger({ ... })또는tasks.trigger("hello-world", payload) - 스케줄 트리거: 크론 등 시간 기반 실행(스케줄 태스크)
- 외부 HTTP 웹훅: Stripe, GitHub 등에서 오는 요청을 Next.js API가 받고 태스크를 트리거
- 대시보드 테스트 실행: 개발 중 페이로드를 넣어 수동 실행
핵심은 “태스크는 무엇을 할지”, “트리거는 언제·어떤 입력으로 돌릴지”를 분리해 사고하는 것입니다.
3. 태스크 정의와 기본 실행 흐름
3-1. 최소 예시: Hello World 태스크
아래는 공식 문서 흐름을 바탕으로 한 구조적 최소 예시입니다. 실제 프로젝트에서는 /trigger 디렉터리와 trigger.config.ts가 함께 생성됩니다.
import { task } from "@trigger.dev/sdk";
export const helloWorld = task({
id: "hello-world",
run: async (payload: { message: string }) => {
console.log(payload.message);
return { ok: true };
},
});
run 함수는 비동기로 작성합니다. 반환값은 JSON 직렬화 가능해야 하므로, Date 같은 값은 문자열로 바꾸는 식으로 다룹니다.
3-2. 다른 코드에서 트리거하기
태스크를 import해 타입 안전하게 트리거할 수 있습니다.
import { helloWorld } from "@/trigger/hello-world";
export async function enqueueGreeting() {
const handle = await helloWorld.trigger({ message: "Hello from app code" });
return handle.id;
}
handle은 실행 추적에 사용할 수 있는 식별 정보를 담습니다. 운영 환경에서는 이 id를 로그 상관관계(correlation id)로 남기면 디버깅이 쉬워집니다.
3-3. CLI 개발 루프
공식 Next.js 가이드에 따르면, 초기 설정은 아래와 같이 시작하는 것이 일반적입니다.
npx trigger.dev@latest init
npx trigger.dev@latest dev
dev 명령은 /trigger 변경을 감시하고, 태스크를 등록·실행하며 대시보드와 상태를 동기화합니다. Next.js 앱과 동시에 띄우는 편이 개발 경험에 유리하며, concurrently로 스크립트를 묶는 예시도 문서에 소개되어 있습니다.
4. 재시도와 타임아웃
4-1. 재시도(retry): 실패를 “정책”으로 다루기
태스크는 run에서 예외가 throw되면 실패로 처리되며, 기본적으로 재시도가 적용됩니다. 공식 개요에 따르면 기본 재시도는 3회 수준이며, 태스크 단위에서 세부 정책을 덮어쓸 수 있습니다.
import { task } from "@trigger.dev/sdk";
export const flakyExternalApi = task({
id: "flaky-external-api",
retry: {
maxAttempts: 10,
factor: 1.8,
minTimeoutInMs: 500,
maxTimeoutInMs: 30_000,
randomize: true,
},
run: async (payload: { orderId: string }) => {
// 외부 API 호출: 일시적 오류는 재시도로 흡수
return { orderId: payload.orderId, synced: true };
},
});
지수 백오프(exponential backoff)는 동일한 실패가 동시에 재시도되며 외부 시스템을 두드리는 폭주를 만드는 것을 완화합니다. randomize(지터)는 그 효과를 더합니다.
주의: 비재현적이지 않은 오류(잘못된 API 키, 권한 부족, 잘못된 페이로드)는 재시도해도 성공하지 않습니다. 이런 경우는 도메인 예외로 분류하고 재시도 대상에서 제외하는 편이 안전합니다.
4-2. maxDuration: 실행 시간 상한
기본적으로 긴 실행을 허용하는 설계를 강조하지만, 운영에서는 무한 실행이 오히려 리스크가 됩니다. maxDuration(초 단위)으로 상한을 둘 수 있습니다.
export const boundedTask = task({
id: "bounded-task",
maxDuration: 300,
run: async () => {
return { finished: true };
},
});
4-3. ttl: “늦게 실행되면 의미 없는” 작업
TTL(time-to-live)는 실행이 너무 늦게 디큐되면 버려야 하는 작업에 유용합니다. 예를 들어 실시간성이 강한 알림, 짧은 유효기간을 가진 토큰 처리 등입니다.
export const timeSensitive = task({
id: "time-sensitive",
ttl: "10m",
run: async () => {
return { done: true };
},
});
트리거 시점에 TTL을 덮어쓰거나, 프로젝트 전역 설정과 조합할 수 있습니다(공식 문서의 TTL 가이드 참고).
4-4. 큐(queue)와 동시성
queue 옵션은 동시에 몇 개의 런을 실행할지를 제어합니다. 외부 API 한도, DB 커넥션 수, CPU 사용량을 고려해 동시성 상한을 걸면 운영 사고를 줄일 수 있습니다.
export const oneAtATime = task({
id: "one-at-a-time",
queue: { concurrencyLimit: 1 },
run: async () => {
return {};
},
});
5. 스케줄링
5-1. 크론 기반 배치
정기 집계, 리포트 생성, 데이터 정리 같은 작업은 스케줄 트리거가 자연스럽습니다. Trigger.dev v3에서는 scheduled tasks로 문서화되어 있으며, 크론 패턴을 코드 또는 대시보드에서 관리하는 흐름을 지원합니다.
실무 팁은 다음과 같습니다.
- 멱등성(idempotency): 같은 스케줄이 겹치거나 재실행되어도 안전한지를 먼저 설계합니다.
- 실행 시간 창: 배치가 길어질수록 다음 스케줄과 겹칠 수 있으므로
maxDuration·큐·락 전략을 함께 봅니다. - 외부 시스템 타임존: 크론은 “서버/플랫폼 기준 시간” 이슈가 생기기 쉬워, 비즈니스 기준(예:
Asia/Seoul)을 명확히 합니다.
5-2. 동적 스케줄(사용자별)
“사용자가 알림 주기를 설정한다” 같은 요구는 사용자마다 다른 스케줄이 필요합니다. Trigger.dev 측 기능(대시보드/SDK/REST)을 활용해 외부 ID(externalId)로 엔티티와 연결하는 패턴이 공식적으로 다뤄집니다. 구현 세부는 계정·플랜·API 변화에 영향을 받으므로, 최신 스케줄 가이드를 기준으로 설계하시기 바랍니다.
6. 웹훅과 이벤트
6-1. 웹훅 수신 → 태스크 트리거
대표 패턴은 다음과 같습니다.
- 결제·배송·GitHub 등 외부 서비스가 HTTP POST로 이벤트를 보냄
- Next.js Route Handler(
app/api/.../route.ts)가 서명 검증·페이로드 파싱 수행 - 검증이 끝나면 태스크를 트리거하고 HTTP는 빠르게 200을 반환
이 패턴의 이점은 웹훅 처리 시간을 짧게 유지하면서, 실제 비즈니스 로직은 재시도·관측이 강한 태스크로 옮길 수 있다는 점입니다.
6-2. “이벤트”를 코드로 모델링하기
플랫폼마다 이벤트 버스 제품명은 다르지만, 애플리케이션 설계에서는 보통 다음을 분리합니다.
- 도메인 이벤트:
OrderPaid,UserSignedUp같은 내부 의미 단위 - 통합 이벤트: Stripe
invoice.paid같은 외부 스키마
Trigger.dev 태스크의 페이로드는 내부 도메인 이벤트에 맞게 정규화하는 것이 유지보수에 유리합니다. 외부 웹훅 스키마가 바뀌어도 어댑터 레이어만 수정하면 됩니다.
7. Next.js 통합
7-1. 초기화와 환경 변수
Next.js 프로젝트 루트에서 CLI로 초기화하면 trigger.config.ts와 /trigger 예제가 생성됩니다. 앱 코드에서 태스크를 트리거하려면 TRIGGER_SECRET_KEY를 로컬(.env.local)과 배포 환경에 설정해야 합니다. 키는 대시보드의 API Keys 화면에서 확인합니다.
7-2. App Router: Route Handler에서 트리거
공식 예시는 다음과 같은 형태입니다(태스크 id 문자열과 페이로드).
import type { helloWorldTask } from "@/trigger/example";
import { tasks } from "@trigger.dev/sdk";
import { NextResponse } from "next/server";
export async function GET() {
const handle = await tasks.trigger<typeof helloWorldTask>("hello-world", "James");
return NextResponse.json(handle);
}
문서에서는 Edge 런타임에서도 tasks.trigger가 동작할 수 있음을 언급합니다. 다만 Edge에서 가능한 네트워크·환경 제약은 프로젝트마다 다르므로, 운영 전 검증이 필요합니다.
7-3. Server Actions
서버 액션에서 "use server"로 태스크를 트리거할 수 있습니다. UI 버튼 클릭으로 긴 작업을 넣을 때 유용하지만, 권한 검증(세션·역할)을 서버 액션 안에서 반드시 수행해야 합니다. 그렇지 않으면 누구나 태스크를 남발할 수 있습니다.
7-4. 빌드/CI에서의 흔한 이슈
문서에는 GitHub CI에서 Next.js 빌드가 정적 생성 과정에서 SDK가 키를 요구해 실패하는 사례가 소개되어 있습니다. 이때 Route Handler에 다음을 추가해 동적 라우트로 만드는 방법이 안내됩니다.
export const dynamic = "force-dynamic";
7-5. Vercel 환경 변수 동기화(선택)
trigger.config.ts의 빌드 확장으로 syncVercelEnvVars를 사용하면 Vercel 프로젝트 환경 변수를 Trigger.dev 쪽으로 맞추는 흐름을 자동화할 수 있습니다. 팀·토큰·프로젝트 ID 설정이 필요합니다.
7-6. 프런트엔드 실시간 UI(선택)
@trigger.dev/react-hooks로 런 상태를 구독해 진행률 UI를 만들 수 있습니다. AI 스트리밍처럼 장시간 작업의 사용자 경험을 개선할 때 특히 유용합니다.
8. 실전 데이터 처리 파이프라인
이 절에서는 파일 수집 → 검증 → 변환 → 적재로 이어지는 흐름을 “태스크 관점”으로 모델링합니다. 코드는 교육용 의사 코드에 가깝고, 실제 프로젝트의 스키마·저장소에 맞게 조정해야 합니다.
8-1. 파이프라인을 태스크로 쪼개는 기준
- 단계마다 실패 패턴이 다름: 네트워크 오류 vs 스키마 오류 vs DB 제약 위반
- 재시도 정책이 달라야 함: 다운로드는 공격적으로, DB 쓰기는 보수적으로
- 관측 포인트가 필요함: 어느 단계에서 멈췄는지 알아야 운영이 가능
8-2. 오케스트레이션 예시: 부모 태스크가 자식 태스크를 트리거
import { task } from "@trigger.dev/sdk";
export const ingestFile = task({
id: "ingest-file",
retry: { maxAttempts: 5, minTimeoutInMs: 1_000, maxTimeoutInMs: 60_000, randomize: true },
run: async (payload: { url: string; tenantId: string }) => {
// 1) 다운로드(의사 코드)
const raw = await downloadText(payload.url);
// 2) 검증 + 정규화
const rows = parseAndValidate(raw);
// 3) 변환 태스크로 위임(대용량이면 청크 단위로 쪼개는 편이 안전)
await transformRows.trigger({ tenantId: payload.tenantId, rows });
return { imported: rows.length };
},
});
export const transformRows = task({
id: "transform-rows",
queue: { concurrencyLimit: 4 },
run: async (payload: { tenantId: string; rows: unknown[] }) => {
const transformed = payload.rows.map(normalizeRow);
await loadToWarehouse.trigger({ tenantId: payload.tenantId, rows: transformed });
return { count: transformed.length };
},
});
export const loadToWarehouse = task({
id: "load-to-warehouse",
retry: { maxAttempts: 8 },
maxDuration: 900,
run: async (payload: { tenantId: string; rows: unknown[] }) => {
// DWH/DB 적재(배치 upsert 등)
return { loaded: payload.rows.length };
},
});
async function downloadText(url: string): Promise<string> {
return "raw";
}
function parseAndValidate(raw: string): unknown[] {
return [];
}
function normalizeRow(row: unknown): unknown {
return row;
}
위 패턴의 핵심은 각 단계가 독립적으로 재시도될 수 있게 경계를 나눈다는 점입니다. 다만 태스크를 많이 쪼갤수록 운영 복잡도도 올라가므로, 팀 규모와 데이터 규모에 맞게 조정해야 합니다.
8-3. 파이프라인 다이어그램
flowchart LR A[외부 파일/이벤트] --> B[ingest-file] B --> C[transform-rows] C --> D[load-to-warehouse] B -. 실패 시 재시도 .-> B C -. 큐 동시성 제한 .-> C D -. maxDuration 상한 .-> D
8-4. 데이터 정합성: 멱등 키와 중복 방지
파이프라인에서 가장 흔한 사고는 같은 입력을 두 번 적재하는 것입니다. 해결책은 저장소 특성에 따라 달라지지만, 일반적으로는 다음을 조합합니다.
- 비즈니스 키 기반 upsert
- 수입(import) 배치 id를 테이블에 기록
- 원본 파일 해시로 중복 탐지
태스크 재시도는 “같은 런의 재시도”인지 “새 런”인지에 따라 의미가 달라지므로, 저장소 제약과 태스크 의미를 함께 설계해야 합니다.
9. 모범 사례와 설계 체크리스트
- 페이로드 최소화: 민감 정보·대용량 바이너리를 그대로 넣지 말고, 스토리지 키·서명 URL로 치환합니다.
- 시크릿은 태스크 실행 환경에: Trigger.dev 대시보드의 환경 변수에 등록하고, 코드에는 노출하지 않습니다.
- 관측 필드 표준화:
tenantId,requestId,userId같은 상관관계 키를 페이로드에 일관되게 넣습니다. - 머신 프리셋: 대용량 메모리가 필요하면
machine옵션으로 리소스를 올립니다.
10. 트러블슈팅
- 로컬에서 태스크가 실행되지 않음:
trigger.devdev프로세스가 떠 있는지,/trigger가 갱신되는지 확인합니다. - Next.js 빌드가 키를 요구함: 해당 라우트를 동적 처리하거나, CI에 안전한 더미 키 전략을 검토합니다(보안 정책에 맞게).
- 재시도가 무한히 돌 것 같음: 예외 유형을 나누고, 비재현 오류만 재시도합니다.
- 스케줄이 기대와 다름: 크론의 시간 기준(UTC vs 로컬)과 서머타임 이슈를 점검합니다.
11. 정리
Trigger.dev는 “백그라운드 작업을 코드로 표현하고, 플랫폼에서 실행·재시도·관측한다”는 점에서 현대적인 백엔드 개발자 경험을 제공합니다. v3에서는 Task가 실행의 중심이고, 트리거는 앱 코드·스케줄·웹훅·대시보드 등 다양한 진입점을 포괄합니다.
Next.js와 결합하면 Route Handler/Server Actions는 얇게, 태스크는 두껍게 가져가는 구조가 자연스럽습니다. 마지막으로, 데이터 파이프라인을 태스크로 나눌 때는 재시도·동시성·멱등성을 한 세트로 설계해야 프로덕션에서 안전하게 굴러갑니다.
부록: 공식 문서로 이어지는 학습 경로
배포 전에는 git add, commit, push 후 프로젝트의 배포 스크립트(예: npm run deploy)를 실행하는 흐름을 따르시기 바랍니다.