OPEN HYPER STEP
← 목록으로 (stack-analysis)
STACK-ANALYSIS · 35 / 90
stack-analysis
CHAPTER 35 / 90
읽기 약 2
FUNCTION

작업 큐: BullMQ + Redis


핵심 개념

백그라운드 잡·재시도·우선순위·스케줄링 — 이메일·이미지 처리·웹훅.

본문

BullMQ 셋업

BASH📋 코드 (1줄)
pnpm add bullmq ioredis
TYPESCRIPT📋 코드 (22줄)
// queues/email.queue.ts
import { Queue } from 'bullmq';
import IORedis from 'ioredis';

const connection = new IORedis(process.env.REDIS_URL!, {
  maxRetriesPerRequest: null,
});

export const emailQueue = new Queue('email', { connection });

export async function sendWelcomeEmail(userId: string, email: string) {
  await emailQueue.add(
    'welcome',
    { userId, email },
    {
      attempts: 3,
      backoff: { type: 'exponential', delay: 5000 },
      removeOnComplete: 100,    // 완료된 잡 100개만 보관
      removeOnFail: 500,
    }
  );
}

Worker — 잡 처리

TYPESCRIPT📋 코드 (30줄)
// workers/email.worker.ts
import { Worker } from 'bullmq';
import { sendEmail } from '@/lib/email';

const worker = new Worker(
  'email',
  async (job) => {
    const { type, userId, email } = job.data;
    await job.log(`Processing ${type} for ${email}`);

    if (type === 'welcome') {
      await sendEmail(email, '환영합니다', `<h1>가입을 환영합니다</h1>`);
    } else if (type === 'reset-password') {
      await sendResetEmail(job.data);
    }

    return { sent: true, at: new Date() };
  },
  {
    connection,
    concurrency: 5,  // 동시 5개 처리
    limiter: {
      max: 100,        // 분당 최대 100개 (외부 API 한도)
      duration: 60000,
    },
  }
);

worker.on('completed', (job) => console.log(`Job ${job.id} done`));
worker.on('failed', (job, err) => console.error(`Job ${job?.id} failed:`, err));

우선순위 + 지연 잡

TYPESCRIPT📋 코드 (22줄)
// 즉시 (긴급)
await queue.add('urgent', data, { priority: 1 });

// 5분 후
await queue.add('reminder', data, { delay: 5 * 60 * 1000 });

// 매일 오전 9시 (cron)
import { JobScheduler } from 'bullmq';
const scheduler = new JobScheduler('email');
await scheduler.upsertJobScheduler(
  'daily-digest',
  { pattern: '0 9 * * *' },
  { name: 'digest', data: {} }
);


// 1시간 후 (지연 알림)
await queue.add(
  'reminder',
  { userId, message: '결제 완료해주세요' },
  { delay: 60 * 60 * 1000 }
);

이미지 처리 큐

TYPESCRIPT📋 코드 (43줄)
import sharp from 'sharp';
import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';

const imageWorker = new Worker(
  'image',
  async (job) => {
    const { srcKey, sizes } = job.data;

    const original = await s3.send(new GetObjectCommand({
      Bucket: BUCKET, Key: srcKey,
    }));
    const buffer = Buffer.from(await original.Body!.transformToByteArray());

    const results = [];
    for (const size of sizes) {
      const resized = await sharp(buffer)
        .resize(size.width, size.height, { fit: 'cover' })
        .webp({ quality: 80 })
        .toBuffer();

      const dstKey = srcKey.replace(/\.\w+$/, `_${size.width}x${size.height}.webp`);
      await s3.send(new PutObjectCommand({
        Bucket: BUCKET, Key: dstKey, Body: resized, ContentType: 'image/webp',
      }));
      results.push(dstKey);

      await job.updateProgress((results.length / sizes.length) * 100);
    }

    return { resized: results };
  },
  { connection, concurrency: 3 }
);


// 사용
await imageQueue.add('resize', {
  srcKey: 'uploads/abc.jpg',
  sizes: [
    { width: 200, height: 200 },
    { width: 800, height: 600 },
  ],
});

웹훅 재시도

TYPESCRIPT📋 코드 (35줄)
const webhookWorker = new Worker(
  'webhook',
  async (job) => {
    const { url, payload, secret } = job.data;
    const signature = crypto.createHmac('sha256', secret)
      .update(JSON.stringify(payload))
      .digest('hex');

    const res = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Signature': signature,
      },
      body: JSON.stringify(payload),
      signal: AbortSignal.timeout(10000),
    });

    if (!res.ok) {
      throw new Error(`Webhook failed: ${res.status}`);
    }
    return { status: res.status };
  },
  {
    connection,
    concurrency: 10,
  }
);


// 발송 — 자동 재시도
await webhookQueue.add('order.created', payload, {
  attempts: 5,
  backoff: { type: 'exponential', delay: 2000 },  // 2s, 4s, 8s, 16s, 32s
});

모니터링 (Bull Board)

TYPESCRIPT📋 코드 (17줄)
import { ExpressAdapter } from '@bull-board/express';
import { createBullBoard } from '@bull-board/api';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';

const serverAdapter = new ExpressAdapter();
serverAdapter.setBasePath('/admin/queues');

createBullBoard({
  queues: [
    new BullMQAdapter(emailQueue),
    new BullMQAdapter(imageQueue),
    new BullMQAdapter(webhookQueue),
  ],
  serverAdapter,
});

app.use('/admin/queues', requireAdmin, serverAdapter.getRouter());

다음 챕터

CH.36 "Rate Limiting + CORS + Helmet 실전".


AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude

무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6

내 코드의 작업 큐 부분을 분석해서
실전 분석 + 개선 우선순위를 알려줘.
ChatGPT

무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro

작업 큐 관련 인기 라이브러리/패턴 5개를
비교 분석해서 패턴 추출를 알려줘.
Gemini

무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro

내 프로젝트 전체에서 작업 큐
최적화 가능 위치를 보고해줘.
Grok

무료: Grok 4.1 / SuperGrok $30/mo

2026년 한국 백엔드 시장의
작업 큐 트렌드를 솔직히 알려줘.

⭐ 이것만 기억하세요
작업 큐: BullMQ + Redis 이 3가지만 확실히 잡으세요
1.BullMQ = Redis 기반 잡 큐 — 재시도·우선순위·지연·cron 모두
2.Worker는 별도 프로세스 — API 서버와 분리해야 안정
3.Bull Board로 큐 상태 시각화 — 실패 잡 재처리 가능


공유하기
진행도 35 / 90