대량 발송의 핵심은 **한 템플릿, 여러 값**입니다. `contacts` 배열의 각 항목이 자기 변수를 가지므로, 사람마다 다른 주문번호·금액·날짜를 같은 문안으로 보낼 수 있습니다.

## 기본형

```typescript
await sendgo.alimtalk.send({
  templateCode: 'ORDER_CONFIRM_001',
  contacts: [
    { contact: '01011111111', name: '홍길동', var1: 'ORD-001', var2: '29,000원' },
    { contact: '01022222222', name: '김철수', var1: 'ORD-002', var2: '15,000원' },
    { contact: '01033333333', name: '이영희', var1: 'ORD-003', var2: '52,000원' },
  ],
});
```

```python
client.alimtalk.send(
    template_code="ORDER_CONFIRM_001",
    contacts=[
        {"contact": "01011111111", "name": "홍길동", "var1": "ORD-001", "var2": "29,000원"},
        {"contact": "01022222222", "name": "김철수", "var1": "ORD-002", "var2": "15,000원"},
    ],
)
```

```php
<?php

$sendgo->alimtalk->send([
    'templateCode' => 'ORDER_CONFIRM_001',
    'contacts'     => [
        ['contact' => '01011111111', 'name' => '홍길동', 'var1' => 'ORD-001', 'var2' => '29,000원'],
        ['contact' => '01022222222', 'name' => '김철수', 'var1' => 'ORD-002', 'var2' => '15,000원'],
    ],
]);
```

## 배치로 나누기

요청 하나에 수만 건을 담으면 타임아웃이 났을 때 어디까지 처리됐는지 알 수 없고, 재시도 비용이 커집니다. 수백 건 단위로 나누세요.

### Node.js

```typescript
const BATCH = 500;

async function sendInBatches(recipients: Recipient[]) {
  const failures: Recipient[] = [];

  for (let i = 0; i < recipients.length; i += BATCH) {
    const chunk = recipients.slice(i, i + BATCH);

    try {
      await sendgo.alimtalk.send({
        templateCode: 'ORDER_CONFIRM_001',
        contacts: chunk.map((r) => ({
          contact: r.phone,
          name: r.name,
          var1: r.orderNo,
          var2: r.amount,
        })),
      });
    } catch (error) {
      // 배치 하나가 실패해도 나머지는 계속 보낸다.
      console.error(`배치 ${i / BATCH} 실패`, error);
      failures.push(...chunk);
    }
  }

  return failures;
}
```

### PHP · Laravel

```php
<?php

use Illuminate\Support\Collection;

collect($recipients)->chunk(500)->each(function (Collection $chunk) use ($sendgo) {
    try {
        $sendgo->alimtalk->send([
            'templateCode' => 'ORDER_CONFIRM_001',
            'contacts'     => $chunk->map(fn ($r) => [
                'contact' => $r->phone,
                'name'    => $r->name,
                'var1'    => $r->order_no,
                'var2'    => number_format($r->amount).'원',
            ])->values()->all(),
        ]);
    } catch (\Sendgo\Php\Exception\SendgoException $e) {
        Log::error('배치 발송 실패', ['message' => $e->getMessage()]);
    }
});
```

### Python

```python
BATCH = 500

for i in range(0, len(recipients), BATCH):
    chunk = recipients[i:i + BATCH]
    try:
        client.alimtalk.send(
            template_code="ORDER_CONFIRM_001",
            contacts=[
                {"contact": r.phone, "name": r.name, "var1": r.order_no}
                for r in chunk
            ],
        )
    except SendgoError as e:
        logger.error("배치 발송 실패: %s", e)
```

## 큐에서 처리하기

대량 발송은 웹 요청 안에서 하지 마세요. 사용자는 응답을 기다리고, 타임아웃이 나면 중간부터 다시 보낼 방법이 없습니다.

```php
<?php
// app/Jobs/SendOrderAlimtalk.php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Sendgo\Php\Sendgo;
use Sendgo\Php\Exception\SendgoException;

class SendOrderAlimtalk implements ShouldQueue
{
    use Queueable;

    // 대부분의 발송 실패는 재시도해도 똑같이 실패한다. 무한 재시도 금지.
    public int $tries = 3;
    public int $backoff = 30;

    public function __construct(private array $contacts) {}

    public function handle(Sendgo $sendgo): void
    {
        $sendgo->alimtalk->send([
            'templateCode' => 'ORDER_CONFIRM_001',
            'contacts'     => $this->contacts,
        ]);
    }

    public function failed(SendgoException $e): void
    {
        // 실패한 배치를 기록해 두면 나중에 실패분만 재발송할 수 있다.
        FailedDispatch::create(['contacts' => $this->contacts, 'reason' => $e->getMessage()]);
    }
}
```

## 부분 실패 다루기

요청이 200 으로 돌아와도 **개별 수신자는 실패할 수 있습니다.** 없는 번호, 차단된 수신자, 카카오톡 미사용자 등입니다.

- 전체를 재발송하지 마세요. 성공한 사람에게 같은 메시지가 두 번 갑니다.
- 실패분만 골라 다시 보내거나, [SMS 대체 발송](/ko/cookbook/sms-fallback)을 켜서 자동으로 문자로 넘기세요.
- 건별 결과는 콘솔의 발송 내역에서 확인합니다.

## 놓치기 쉬운 것

- **번호 정규화를 먼저 하세요.** DB 에 `010-1234-5678`, `+821012345678`, `01012345678` 이 섞여 있는 경우가 흔합니다. 하이픈과 국가번호를 제거해 숫자만 남기세요.
- **중복 번호를 제거하세요.** 같은 사람에게 두 번 나가고 크레딧도 두 번 빠집니다.
- **크레딧을 미리 확인하세요.** 1만 건 발송 도중 잔액이 떨어지면 `PAYMENT_REQUIRED` 가 나면서 나머지가 통째로 실패합니다.
- **광고성이라면 야간 발송 금지가 적용됩니다.** 배치가 21시를 넘겨 실행되지 않도록 하세요 → [광고성 메시지 규칙](/ko/cookbook/ad-message-rules)

## 다음 단계

- [예약 발송](/ko/cookbook/scheduled-send)
- [오류 코드와 재시도 전략](/ko/cookbook/error-handling)