스마트폰 시대에 앱 푸시 알림만큼 중요한 게 바로 SMS(문자 메시지)다. 특히 인증번호, 주문 확인, 긴급 공지 같은 중요한 정보는 이메일보다 SMS로 전달할 때 사용자 확인율이 훨씬 높다. 다만 대부분의 개발자들은 직접 통신사와 계약하거나 복잡한 API 문서를 읽고도 어디서부터 시작해야 할지 몰라 시간을 낭비한다.
이번에는 세계적으로 가장 널리 쓰이는 Twilio SMS API를 PHP에서 어떻게 연동하는지, 계정 생성부터 실제 문자 발송, 대량 발송 처리까지 완벽하게 정리해서 소개하겠다.
Twilio를 쓰는 이유는 단순하다:
| 항목 | 직접 구축 | Twilio |
|---|---|---|
| 초기 비용 | 높음 (통신사 계약) | 낮음 (1문자 약 0.5센트) |
| 개발 기간 | 길음 (수개월) | 짧음 (1시간) |
| 글로벌 대응 | 어려움 | 자동 지원 |
| 유지보수 | 복잡함 | 간단함 |
계정 생성하기
먼저 Twilio 공식 사이트에 접속한다. 우측 상단 Sign Up 버튼을 클릭하면 아래와 같은 가입 폼이 나타난다:
✓ 이메일 입력 → 비밀번호 설정 → "약관 동의"에 체크 → "Create account" 클릭
가입 후 인증 이메일을 확인하고, 휴대폰 번호로 본인 인증을 완료하면 대시보드 접속이 가능해진다. 처음 가입하면 무료 크레딧 $15를 받는데, 이 정도면 테스트용으로 충분하다(SMS 한 건에 약 0.5센트).
API 키와 전화번호 확인하기
Twilio 대시보드에 접속하면 Account SID와 Auth Token이 보인다. 이 두 값은 PHP에서 API를 호출할 때 필수다:
// Twilio 대시보드에서 확인할 값
Account SID: ACxxxxxxxxxxxxxxxxxxxxxxxxxx
Auth Token: your_auth_token_here
Twilio Phone: +1xxxxxxxxxx (SMS를 보낼 때 발신 번호)
"Phone Numbers" 메뉴에서 이미 할당된 전화번호를 확인할 수 있다. 이 번호가 SMS 발신자 번호가 된다. 새 번호를 추가하려면 "Buy Numbers"를 클릭하고 국가(대한민국)를 선택한 뒤 번호를 구매하면 된다.
Composer를 이용해서 Twilio의 공식 PHP SDK를 설치한다:
composer require twilio/sdk
설치가 완료되면 프로젝트의 vendor 폴더에 Twilio 라이브러리가 자동으로 다운로드된다. 기존 프로젝트에서 Composer를 안 쓰고 있다면, 먼저 composer init으로 composer.json을 생성한 후 위 명령어를 실행하면 된다.
가장 간단한 SMS 발송 코드
✗ 잘못된 코드 (보안 문제)
<?php
require_once 'vendor/autoload.php';
// API 키를 코드에 직접 삽입 (절대 금지!)
$sid = 'ACxxxxxxxxxxxxxxxxxxxxxxxxxx';
$token = 'your_auth_token_here';
$twilioNumber = '+1xxxxxxxxxx';
// Twilio 클라이언트 생성
$client = new TwilioRestClient($sid, $token);
// SMS 발송
$message = $client->messages->create(
'+821012345678', // 수신 번호
array(
'from' => $twilioNumber,
'body' => '안녕하세요. 테스트 메시지입니다.'
)
);
echo "메시지 SID: " . $message->sid;
?>
✓ 올바른 코드 (환경변수 사용)
<?php
require_once 'vendor/autoload.php';
// 환경변수 또는 설정 파일에서 가져오기
$sid = getenv('TWILIO_ACCOUNT_SID');
$token = getenv('TWILIO_AUTH_TOKEN');
$twilioNumber = getenv('TWILIO_PHONE_NUMBER');
if (!$sid || !$token || !$twilioNumber) {
die('Twilio 환경변수가 설정되지 않았습니다.');
}
try {
$client = new TwilioRestClient($sid, $token);
// 수신 번호 유효성 검사
$toNumber = '+821012345678';
if (!preg_match('/^+d{1,15}$/', $toNumber)) {
throw new Exception('유효하지 않은 전화번호 형식');
}
// SMS 발송
$message = $client->messages->create(
$toNumber,
array(
'from' => $twilioNumber,
'body' => '안녕하세요. 테스트 메시지입니다.'
)
);
echo "✓ 메시지 발송 성공n";
echo "메시지 SID: " . $message->sid . "n";
echo "발송 상태: " . $message->status . "n";
} catch (Exception $e) {
echo "✗ 에러: " . $e->getMessage();
}
?>
결과 출력
✓ 메시지 발송 성공
메시지 SID: SM1234567890abcdef1234567890abcdef
발송 상태: queued
상태가 "queued"면 발송 대기 중이고, Twilio의 서버에서 처리한 후 "sent" 또는 "failed"로 변경된다.
API 키를 안전하게 관리하려면 .env 파일을 사용해야 한다. 먼저 dotenv 라이브러리를 설치한다:
composer require vlucas/phpdotenv
프로젝트 루트에 .env 파일을 생성한다:
# .env
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token_here
TWILIO_PHONE_NUMBER=+1xxxxxxxxxx
중요: .env 파일은 .gitignore에 추가해서 절대 Git에 커밋하면 안 된다:
# .gitignore에 추가
.env
.env.local
이제 PHP 코드에서 .env 파일을 로드한다:
<?php
require_once 'vendor/autoload.php';
$dotenv = DotenvDotenv::createImmutable(__DIR__);
$dotenv->load();
$sid = $_ENV['TWILIO_ACCOUNT_SID'];
$token = $_ENV['TWILIO_AUTH_TOKEN'];
$twilioNumber = $_ENV['TWILIO_PHONE_NUMBER'];
echo "✓ 환경변수 로드 완료";
?>
대부분의 웹 서비스에서 필요한 인증번호 발송 시나리오를 구현해보자:
<?php
require_once 'vendor/autoload.php';
use TwilioRestClient;
class TwilioSmsService {
private $client;
private $twilioNumber;
public function __construct() {
$dotenv = DotenvDotenv::createImmutable(__DIR__);
$dotenv->load();
$this->client = new Client(
$_ENV['TWILIO_ACCOUNT_SID'],
$_ENV['TWILIO_AUTH_TOKEN']
);
$this->twilioNumber = $_ENV['TWILIO_PHONE_NUMBER'];
}
// 인증번호 발송
public function sendVerificationCode($phoneNumber, $code) {
// 전화번호 포맷 정규화 (01012345678 → +821012345678)
$phoneNumber = $this->normalizePhoneNumber($phoneNumber);
if (!$phoneNumber) {
return ['success' => false, 'message' => '유효하지 않은 전화번호'];
}
try {
$message = $this->client->messages->create(
$phoneNumber,
array(
'from' => $this->twilioNumber,
'body' => "[인증번호] {$code} (유효시간 5분)"
)
);
return [
'success' => true,
'message' => '인증번호 발송 완료',
'sid' => $message->sid,
'status' => $message->status
];
} catch (Exception $e) {
return [
'success' => false,
'message' => 'SMS 발송 실패: ' . $e->getMessage()
];
}
}
// 주문 확인 메시지 발송
public function sendOrderConfirmation($phoneNumber, $orderId, $totalAmount) {
$phoneNumber = $this->normalizePhoneNumber($phoneNumber);
if (!$phoneNumber) {
return ['success' => false, 'message' => '유효하지 않은 전화번호'];
}
try {
$body = "[주문확인] 주문번호: {$orderId}n합계: {$totalAmount}원n마이페이지에서 배송상태를 확인하세요.";
$message = $this->client->messages->create(
$phoneNumber,
array(
'from' => $this->twilioNumber,
'body' => $body
)
);
return ['success' => true, 'sid' => $message->sid];
} catch (Exception $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}
// 전화번호 정규화 함수
private function normalizePhoneNumber($phone) {
// 숫자만 추출
$phone = preg_replace('/[^0-9]/', '', $phone);
// 한국 번호 (01012345678 형태)
if (strlen($phone) === 11 && substr($phone, 0, 2) === '01') {
return '+82' . substr($phone, 1); // +821012345678
}
// 이미 +로 시작하는 국제형식
if (substr($phone, 0, 1) === '+') {
return $phone;
}
// 유효하지 않은 형식
return null;
}
}
// 사용 예제
$sms = new TwilioSmsService();
// 인증번호 발송
$result = $sms->sendVerificationCode('01012345678', '123456');
echo json_encode($result, JSON_UNESCAPED_UNICODE);
// 주문 확인 발송
$result = $sms->sendOrderConfirmation('01012345678', 'ORD20240101001', '29900');
echo json_encode($result, JSON_UNESCAPED_UNICODE);
?>
결과 출력
{"success":true,"message":"인증번호 발송 완료","sid":"SMabcdef1234567890","status":"queued"}
여러 사용자에게 한 번에 문자를 보낼 때는 반복문을 사용하되, 속도 제한(Rate Limiting)을 고려해야 한다:
<?php
require_once 'vendor/autoload.php';
class BulkSmsService {
private $sms;
private $maxPerSecond = 20; // 초당 최대 20건
private $delayMs = 50; // 건마다 50ms 딜레이
public function __construct(TwilioSmsService $sms) {
$this->sms = $sms;
}
// 대량 발송
public function sendBulk($recipients, $messageTemplate) {
$results = [
'total' => count($recipients),
'success' => 0,
'failed' => 0,
'details' => []
];
$startTime = time();
foreach ($recipients as $index => $recipient) {
// 속도 제한: 초당 최대 20건 유지
if ($index > 0 && $index % $this->maxPerSecond === 0) {
$elapsed = time() - $startTime;
if ($elapsed < 1) {
sleep(1 - $elapsed);
}
$startTime = time();
}
// 수신자별 메시지 커스터마이징
$body = str_replace('{name}', $recipient['name'], $messageTemplate);
try {
// 실제 발송 전 데이터베이스에 미리 기록
$result = $this->sms->sendDirect(
$recipient['phone'],
$body
);
if ($result['success']) {
$results['success']++;
} else {
$results['failed']++;
}
$results['details'][] = [
'phone' => $recipient['phone'],
'status' => $result['success'] ? 'sent' : 'failed',
'message' => $result['message'] ?? ''
];
} catch (Exception $e) {
$results['failed']++;
$results['details'][] = [
'phone' => $recipient['phone'],
'status' => 'error',
'message' => $e->getMessage()
];
}
// 마이크로초 단위 딜레이
usleep($this->delayMs * 1000);
}
return $results;
}
}
// 사용 예제
$recipients = [
['name' => '김철수', 'phone' => '01012345678'],
['name' => '이영희', 'phone' => '01087654321'],
['name' => '박민준', 'phone' => '01099998888']
];
$bulk = new BulkSmsService($sms);
$results = $bulk->sendBulk(
$recipients,
"{name}님 안녕하세요. 이벤트 참여 이벤트 안내입니다."
);
echo "발송 완료: " . $results['success'] . "/{$results['total']}건n";
?>
✗ API 키를 코드에 직접 삽입하기
// 절대 금지!
$sid = 'ACxxxxxxxxxxxxxxxxxxxxxxxxxx';
$token = 'your_auth_token_here';
✓ 대신 환경변수 사용:
$sid = $_ENV['TWILIO_ACCOUNT_SID'];
$token = $_ENV['TWILIO_AUTH_TOKEN'];
✗ 전화번호 포맷을 확인 없이 발송하기
// 국가별, 형식별 혼재 → 발송 실패
$client->messages->create('01012345678', ...); // 실패
$client->messages->create('+821012345678', ...); // 성공
✓ 정규화 함수로 일관되게 변환:
$normalizedPhone = normalizePhoneNumber($phone); // 항상 +82... 형식
✗ 대량 발송 시 속도 제한을 무시하기
// 초당 100건 이상 발송 → 429 Too Many Requests 에러
foreach ($recipients as $phone) {
$client->messages->create($phone, ...);
}
✓ 속도 제한 적용:
if ($index % 20 === 0) {
sleep(1); // 초당 20건 유지
}
Twilio SMS API는 복잡한 통신 인프라 없이도 몇 줄의 코드로 글로벌 문자 발송 시스템을 구축할 수 있다. 작은 최적화 하나(전화번호 정규화, 속도 제한, 환경변수 관리)가 모여서 안정적인 서비스를 만든다는 점을 잊지 말자.
이 글의 "3단계 기본 코드"를 먼저 테스트해보고, "5단계 실전 함수"를 프로젝트에 맞게 수정해서 적용하면, 바로 오늘 안에 인증번호 발송 기능을 배포할 수 있을 것이다.