문자메시지 발송이 필요한데 복잡한 통신사 연동은 하고 싶지 않다면, Twilio SMS API가 답이다. 다만 대부분의 개발자들은 Twilio 계정 생성부터 API 인증, 실제 메시지 발송 코드 작성까지 어디서부터 시작해야 할지 헷갈린다. 이번에는 Twilio 계정 준비, API 키 발급, PHP에서의 실제 발송 방법, 그리고 발송 실패 시 대응까지 완벽하게 정리해서 소개하겠다.
먼저 Twilio 공식 사이트에 접속해서 계정을 만든다. 회원가입 후 대시보드에 로그인하면, 좌측 사이드바에서 Account > API keys & tokens로 이동한다. 여기서 Account SID와 Auth Token을 복사해둔다. 이 두 가지가 PHP에서 API 인증에 필요한 자격증명이다.
다음으로 발신 번호(Phone Number)를 등록해야 한다. 대시보드의 Phone Numbers > Manage Numbers > Buy a number에서 원하는 국가와 지역의 번호를 구매할 수 있다. 한국에서 발송할 계획이라면 한국 번호를 선택하면 된다.
Composer를 사용해서 Twilio PHP SDK를 설치한다.
composer require twilio/sdk
설치 후 vendor 폴더에 Twilio 라이브러리가 추가되고, autoload.php가 자동으로 로드된다.
이제 실제로 문자메시지를 보내는 코드를 작성해보자.
✗ 잘못된 방법 (API 키를 노출시키는 경우)
<?php
require_once 'vendor/autoload.php';
use Twilio\Rest\Client;
// 하드코딩된 자격증명 - 절대 금지
$sid = 'AC1234567890abcdef1234567890abcdef';
$token = 'your_auth_token_here';
$from = '+1234567890';
$client = new Client($sid, $token);
$message = $client->messages->create(
'+82101234567', // 수신자 번호 (국제 형식)
array('from' => $from, 'body' => '안녕하세요')
);
?>
위 코드는 여러 문제가 있다. 첫째, API 키를 소스 코드에 직접 작성하면 Git에 올렸을 때 노출된다. 둘째, 환경마다 다른 키를 사용할 수 없다. 셋째, 발송 결과를 제대로 처리하지 않는다.
✓ 올바른 방법 (환경변수를 사용하는 경우)
<?php
require_once 'vendor/autoload.php';
use Twilio\Rest\Client;
// 환경변수에서 읽기
$sid = getenv('TWILIO_ACCOUNT_SID');
$token = getenv('TWILIO_AUTH_TOKEN');
$from = getenv('TWILIO_PHONE_NUMBER');
if (!$sid || !$token || !$from) {
die('환경변수 설정이 필요합니다.');
}
try {
$client = new Client($sid, $token);
$message = $client->messages->create(
'+82101234567',
array(
'from' => $from,
'body' => '테스트 메시지입니다.'
)
);
echo '발송 성공. SID: ' . $message->sid;
} catch (Exception $e) {
echo '발송 실패: ' . $e->getMessage();
}
?>
.env 파일(Git 무시)에 자격증명을 저장하거나, 서버 환경변수로 설정한다. 그리고 try-catch로 발송 실패 시 에러를 캐치한다.
여러 사용자에게 동시에 메시지를 보낼 때는 API 요청 수를 고려해야 한다. Twilio는 요청당 비용이 발생하므로, 간단한 루프보다는 비동기 처리가 효율적이다.
✗ 잘못된 방법 (순차 발송으로 인한 시간 낭비)
<?php
require_once 'vendor/autoload.php';
use Twilio\Rest\Client;
$client = new Client(
getenv('TWILIO_ACCOUNT_SID'),
getenv('TWILIO_AUTH_TOKEN')
);
$phone_numbers = ['+82101234567', '+82101234568', '+82101234569'];
// 각 번호마다 차례대로 발송 (느림)
foreach ($phone_numbers as $number) {
$message = $client->messages->create(
$number,
array(
'from' => getenv('TWILIO_PHONE_NUMBER'),
'body' => '공지사항입니다.'
)
);
echo '발송됨: ' . $message->sid . PHP_EOL;
}
?>
이 방법은 각 메시지가 순차적으로 처리되므로, 100명에게 보낼 때 최소 100초 이상 걸린다.
✓ 올바른 방법 (발송 로그 저장 및 배치 처리)
<?php
require_once 'vendor/autoload.php';
use Twilio\Rest\Client;
$client = new Client(
getenv('TWILIO_ACCOUNT_SID'),
getenv('TWILIO_AUTH_TOKEN')
);
$phone_numbers = ['+82101234567', '+82101234568', '+82101234569'];
$from = getenv('TWILIO_PHONE_NUMBER');
$body = '공지사항입니다.';
// DB에 발송 기록 미리 저장
$send_log = [];
foreach ($phone_numbers as $number) {
try {
$message = $client->messages->create(
$number,
array('from' => $from, 'body' => $body)
);
// 발송 성공 로그
$send_log[] = [
'phone' => $number,
'twilio_sid' => $message->sid,
'status' => 'sent',
'sent_at' => date('Y-m-d H:i:s')
];
} catch (Exception $e) {
// 발송 실패 로그
$send_log[] = [
'phone' => $number,
'status' => 'failed',
'error' => $e->getMessage(),
'sent_at' => date('Y-m-d H:i:s')
];
}
}
// 로그를 DB에 저장
foreach ($send_log as $log) {
// INSERT INTO sms_logs (phone, twilio_sid, status, error, sent_at) VALUES (...);
echo 'Log: ' . json_encode($log) . PHP_EOL;
}
?>
이제 발송 결과를 데이터베이스에 저장하므로, 나중에 어떤 메시지가 성공했고 실패했는지 추적할 수 있다.
Twilio는 메시지 발송 후 상태 변화(delivered, failed, undelivered 등)를 Webhook으로 알려준다. 서버에서 이를 수신해서 실시간으로 상태를 업데이트할 수 있다.
Twilio 대시보드에서 Messaging > Settings > Webhook URL에 아래 주소를 등록한다.
https://yourdomain.com/twilio_webhook.php
twilio_webhook.php
<?php
// Twilio 서명 검증
$token = getenv('TWILIO_AUTH_TOKEN');
$twilio_signature = $_SERVER['HTTP_X_TWILIO_SIGNATURE'] ?? '';
$url = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
$data = $_POST;
ksort($data);
$body_hash = hash('sha1', $url . http_build_query($data), true);
$signature = base64_encode(hash_hmac('sha1', $url . http_build_query($data), $token, true));
if ($signature !== $twilio_signature) {
http_response_code(403);
die('Unauthorized');
}
// 발송 상태 업데이트
$message_sid = $_POST['MessageSid'] ?? '';
$status = $_POST['MessageStatus'] ?? ''; // delivered, failed, undelivered, queued, sending, sent
echo '<Response></Response>'; // Twilio가 200 OK를 기대함
// DB에 상태 업데이트
// UPDATE sms_logs SET status = '$status' WHERE twilio_sid = '$message_sid';
echo PHP_EOL . 'Status updated: ' . $message_sid . ' = ' . $status;
?>
| 문제 | 원인 | 해결책 |
|---|---|---|
| "Invalid phone format" 에러 | 수신자 번호가 국제 형식이 아님 | '+82' + '01012345678' 형태로 앞의 0 제거하고 국가코드 추가 |
| "Account suspended" 에러 | 잘못된 자격증명 또는 계정 정지 | Account SID, Auth Token 다시 확인. Twilio 대시보드에서 계정 상태 확인 |
| "The number is not registered" 에러 | 구매한 발신 번호가 활성화되지 않음 | Twilio 대시보드의 Phone Numbers에서 번호 활성 상태 확인 |
| 발송은 성공하는데 수신이 안 됨 | 수신자의 국가/통신사 정책 또는 스팸 필터 | 수신자에게 문자 수신 권한 확인 요청, Twilio 로그에서 상태 확인 |
| API 요청 타임아웃 | 네트워크 지연 또는 Twilio 서버 부하 | cURL 타임아웃 설정 증가, 재시도 로직 추가 |
네트워크 오류나 일시적 실패에 대응하기 위해 재시도 로직을 추가한다.
<?php
require_once 'vendor/autoload.php';
use Twilio\Rest\Client;
function sendSmsWithRetry($phone, $message, $max_retries = 3) {
$client = new Client(
getenv('TWILIO_ACCOUNT_SID'),
getenv('TWILIO_AUTH_TOKEN')
);
$from = getenv('TWILIO_PHONE_NUMBER');
$retry_count = 0;
while ($retry_count < $max_retries) {
try {
$result = $client->messages->create(
$phone,
array('from' => $from, 'body' => $message)
);
return ['success' => true, 'sid' => $result->sid];
} catch (Exception $e) {
$retry_count++;
$error = $e->getMessage();
// 재시도 가능 여부 판단
if (strpos($error, 'timeout') !== false ||
strpos($error, 'temporarily unavailable') !== false) {
// 지수 백오프 대기 (1초, 2초, 4초)
sleep(pow(2, $retry_count - 1));
} else {
// 복구 불가능한 에러는 즉시 반환
return ['success' => false, 'error' => $error];
}
}
}
return ['success' => false, 'error' => '최대 재시도 횟수 초과'];
}
$result = sendSmsWithRetry('+82101234567', '테스트 메시지');
echo json_encode($result);
?>
Twilio SMS API는 국내 SMS 서비스의 복잡함을 없애고 전 세계 발송을 간단하게 처리해준다. 계정 설정부터 시작해서 환경변수를 통한 안전한 자격증명 관리, 그리고 발송 결과 추적까지 이 글의 내용을 따라가면 프로덕션 환경에서 즉시 활용할 수 있다. 특히 국제 서비스나 다국가 사용자 지원이 필요할 때 Twilio는 국내 SMS 서비스보다 훨씬 유연하고 안정적이다. API 키 관리와 발송 실패 처리를 정확히 구현한다면, 안정적인 알림 시스템을 만들 수 있을 것이다.