화상회의 기능을 직접 구현해본 개발자라면 얼마나 복잡한지 알 것이다. WebRTC 시그널링, 미디어 스트림 관리, 참여자 제어까지 손으로 하나하나 만들려면 몇 달이 걸린다. 다만 대부분의 백엔드 개발자는 Twilio Video 같은 전문 서비스가 있다는 걸 모르고 처음부터 바닥부터 만들려다가 프로젝트를 포기한다. 이번에는 Twilio Video API를 PHP에서 제대로 연동하는 방법을 정확히 정리해서 소개하겠다. 접근 토큰 생성부터 실제 화상 룸 생성, 참여자 관리까지 실전 코드로 보여줄 테니 참고하면 된다.
Twilio Video는 클라우드 기반 실시간 통신 플랫폼이다. 서버에서 토큰만 발급해주면, 클라이언트 측 JavaScript SDK가 알아서 피어 연결을 맺고 음성/비디오 스트림을 주고받는다. 너가 인프라를 구축할 필요가 없다. 대신 토큰 발급, 참여자 추적, 녹화 설정 같은 백엔드 로직은 직접 만들어야 한다.
Twilio Video의 핵심은 '접근 토큰'이다. 이 토큰 없이는 클라이언트가 룸에 접속할 수 없다. 토큰은 시간 제한이 있고(기본 1시간), 어떤 룸에, 어떤 권한으로 접속할지 정보가 포함되어 있다.
먼저 Twilio 계정을 만들고 Video API를 활성화해야 한다. https://www.twilio.com/console에 접속해서 API 키(API SID)와 API Secret을 얻는다. 대시보드의 '프로젝트 설정' 섹션에서 확인할 수 있다.
PHP에서 Twilio를 쓰려면 Composer로 공식 SDK를 설치한다.
composer require twilio/sdk
설치 후 autoload.php를 require하면 Twilio\\Rest\\Client 클래스를 쓸 수 있다.
화상회의에 참여하려면 먼저 클라이언트에게 토큰을 발급해줘야 한다. 이 토큰은 참여자 신원, 참여할 룸 이름, 권한(음성 송수신, 비디오 송수신 여부)을 포함한다.
많은 개발자가 사용자 인증 없이 토큰을 발급한다. 이건 보안 위험이다. 누구나 화상회의에 접속할 수 있게 된다.
<?php
require 'vendor/autoload.php';
use Twilio\\Jwt\\AccessToken;
use Twilio\\Jwt\\Grants\\VideoGrant;
// 잘못된 예: 어떤 검증도 없음
$roomName = $_GET['room'];
$participantName = $_GET['name'];
$token = new AccessToken(
getenv('TWILIO_ACCOUNT_SID'),
getenv('TWILIO_API_KEY'),
getenv('TWILIO_API_SECRET')
);
$videoGrant = new VideoGrant();
$videoGrant->setRoom($roomName);
$token->addGrant($videoGrant);
$token->setIdentity($participantName);
echo $token->toJWT();
?>
문제점: 누구든 roomName과 name을 전달하면 토큰을 받을 수 있다. 이건 무단 접속을 막을 수 없다.
사용자를 먼저 인증하고, 그 사용자가 해당 룸에 접속할 권한이 있는지 확인한 후 토큰을 발급한다.
<?php
require 'vendor/autoload.php';
use Twilio\\Jwt\\AccessToken;
use Twilio\\Jwt\\Grants\\VideoGrant;
session_start();
// 사용자 인증 확인
if (empty($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
$roomName = $_POST['room_name'] ?? null;
$participantName = $_POST['participant_name'] ?? null;
$userId = $_SESSION['user_id'];
// 입력값 검증
if (!$roomName || !$participantName) {
http_response_code(400);
echo json_encode(['error' => 'room_name and participant_name required']);
exit;
}
// 룸 접속 권한 확인 (데이터베이스에서 확인)
$db = new PDO('mysql:host=localhost;dbname=myapp', 'user', 'password');
$stmt = $db->prepare('SELECT id FROM conference_rooms WHERE room_name = ? AND user_id = ?');
$stmt->execute([$roomName, $userId]);
if (!$stmt->fetch()) {
http_response_code(403);
echo json_encode(['error' => 'Access denied to this room']);
exit;
}
// 토큰 생성
$token = new AccessToken(
getenv('TWILIO_ACCOUNT_SID'),
getenv('TWILIO_API_KEY'),
getenv('TWILIO_API_SECRET')
);
$videoGrant = new VideoGrant();
$videoGrant->setRoom($roomName);
$token->addGrant($videoGrant);
$token->setIdentity($userId . ':' . $participantName);
$token->setTimeToLive(3600); // 1시간
header('Content-Type: application/json');
echo json_encode([
'token' => $token->toJWT(),
'room_name' => $roomName
]);
?>
개선점: 세션에서 user_id를 확인하고, 데이터베이스에서 해당 사용자가 룸 접속 권한이 있는지 검증한다. 권한이 없으면 403 Forbidden을 반환한다.
서버에서 발급한 토큰을 받으면, 클라이언트는 Twilio Video JavaScript SDK를 사용해서 룸에 접속한다. 백엔드에서 할 일은 아니지만, 전체 흐름을 이해하려면 클라이언트 코드도 알아야 한다.
<!DOCTYPE html>
<html>
<head>
<title>Twilio Video Room</title>
</head>
<body>
<div id="participants"></div>
<script src="https://media.twiliocdn.com/sdk/js/video/releases/2.26.0/twilio-video.min.js"></script>
<script>
async function joinRoom() {
// 서버에서 토큰 요청
const response = await fetch('/get_token.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
room_name: 'my-room',
participant_name: 'John Doe'
})
});
const data = await response.json();
const { token, room_name } = data;
// 룸에 접속
const room = await Twilio.Video.connect(token, {
name: room_name,
audio: true,
video: { width: 640 }
});
console.log('Room joined: ' + room.name);
displayParticipants(room);
}
function displayParticipants(room) {
room.participants.forEach(participantConnected);
room.on('participantConnected', participantConnected);
room.on('participantDisconnected', participantDisconnected);
}
function participantConnected(participant) {
console.log('Participant joined: ' + participant.identity);
}
function participantDisconnected(participant) {
console.log('Participant left: ' + participant.identity);
}
joinRoom();
</script>
</body>
</html>
Twilio 대시보드에서만 룸을 볼 수 있는 게 아니다. PHP 백엔드에서 Twilio REST API를 직접 호출해서 룸 정보를 조회하거나, 참여자를 강제 퇴장시키거나, 녹화를 시작할 수 있다.
현재 진행 중인 화상회의 룸을 확인할 수 있다.
<?php
require 'vendor/autoload.php';
use Twilio\\Rest\\Client;
$client = new Client(
getenv('TWILIO_ACCOUNT_SID'),
getenv('TWILIO_AUTH_TOKEN')
);
// 활성 룸 목록
$rooms = $client->video->v1->rooms->stream();
foreach ($rooms as $room) {
echo "Room: " . $room->uniqueName . "\n";
echo "Duration: " . $room->duration . " seconds\n";
echo "Max Participants: " . $room->maxParticipants . "\n";
echo "---\n";
}
?>
어떤 사용자들이 지금 룸에 있는지 확인한다.
<?php
require 'vendor/autoload.php';
use Twilio\\Rest\\Client;
$client = new Client(
getenv('TWILIO_ACCOUNT_SID'),
getenv('TWILIO_AUTH_TOKEN')
);
$roomSid = 'RM0123456789abcdef';
$participants = $client->video->v1->rooms($roomSid)
->participants
->stream();
foreach ($participants as $participant) {
echo "Participant: " . $participant->identity . "\n";
echo "Status: " . $participant->status . "\n";
echo "Duration: " . $participant->duration . " seconds\n";
}
?>
금지된 사용자나 행동 문제가 있는 참여자를 강제 퇴장시킬 수 있다.
<?php
require 'vendor/autoload.php';
use Twilio\\Rest\\Client;
$client = new Client(
getenv('TWILIO_ACCOUNT_SID'),
getenv('TWILIO_AUTH_TOKEN')
);
$roomSid = 'RM0123456789abcdef';
$participantSid = 'PA0123456789abcdef';
// 참여자 제거
$client->video->v1->rooms($roomSid)
->participants($participantSid)
->update(array('status' => 'disconnected'));
echo "Participant removed from room";
?>
화상회의를 자동 녹화할 수 있다. 룸을 만들 때 녹화 옵션을 설정하거나, 나중에 녹화를 시작/중지할 수 있다.
새 룸을 만들 때 자동 녹화를 활성화하면, 참여자들이 들어오는 순간부터 녹화가 시작된다.
<?php
require 'vendor/autoload.php';
use Twilio\\Rest\\Client;
$client = new Client(
getenv('TWILIO_ACCOUNT_SID'),
getenv('TWILIO_AUTH_TOKEN')
);
$room = $client->video->v1->rooms->create(array(
'uniqueName' => 'important-meeting-' . time(),
'type' => 'go',
'maxParticipants' => 10,
'recordParticipantsOnConnect' => true, // 자동 녹화
'mediaRegion' => 'us1' // 지역 설정(한국은 ap1, 미국은 us1)
));
echo json_encode([
'room_sid' => $room->sid,
'room_name' => $room->uniqueName,
'recording_enabled' => true
]);
?>
이미 녹화된 파일을 조회하고 다운로드할 수 있다.
<?php
require 'vendor/autoload.php';
use Twilio\\Rest\\Client;
$client = new Client(
getenv('TWILIO_ACCOUNT_SID'),
getenv('TWILIO_AUTH_TOKEN')
);
$recordings = $client->video->v1->recordings->stream();
foreach ($recordings as $recording) {
echo "Recording SID: " . $recording->sid . "\n";
echo "Status: " . $recording->status . "\n";
echo "Size: " . $recording->size . " bytes\n";
if ($recording->status === 'completed') {
echo "Download URL: " . $recording->links['download'] . "\n";
}
echo "---\n";
}
?>
1. 토큰 만료 시간을 너무 길게 설정하지 마라
✗ 틀린 것: 24시간 유효한 토큰
$token->setTimeToLive(86400); // 24시간은 너무 길다
✓ 올바른 것: 1시간 또는 필요한 만큼만
$token->setTimeToLive(3600); // 1시간
토큰이 오래 유효하면, 누군가 토큰을 탈취했을 때 오랫동안 불법 접속할 수 있다. 화상회의 시간이 1시간을 초과하면, 중간에 새 토큰을 발급받게 설계하는 게 낫다.
2. API Key와 Auth Token을 헷갈리지 마라
Twilio에는 두 가지 인증 방식이 있다. 토큰 생성할 때는 API Key와 API Secret을 쓰고, REST API 호출할 때는 Account SID와 Auth Token을 쓴다. 섞어서 쓰면 인증 실패가 난다.
| 용도 | 필요한 것 |
|---|---|
| 클라이언트 접근 토큰 생성 | Account SID + API Key + API Secret |
| REST API 호출(룸 관리 등) | Account SID + Auth Token |
3. 룸 SID와 룸 이름을 혼동하지 마라
룸을 만들면 uniqueName(사람이 읽을 수 있는 이름)과 SID(Twilio 시스템 ID)가 생긴다. 클라이언트 접속할 때는 uniqueName을 쓰고, REST API에서 룸을 조회하거나 참여자를 제거할 때는 SID를 써야 한다.
4. 활성 룸이 자동 삭제되지 않는다
마지막 참여자가 나가도 룸이 남아있을 수 있다. 시간이 지나면 자동 삭제되지만, 수동으로 삭제하려면 REST API를 호출해야 한다.
$client->video->v1->rooms($roomSid)->update(array('status' => 'completed'));
Twilio Video API를 쓰면, 수개월이 걸릴 화상회의 시스템을 며칠 안에 만들 수 있다. 접근 토큰을 올바르게 생성하고, 클라이언트에 전달하고, 서버에서 룸과 참여자를 관리하는 세 단계만 잘 이해하면 된다. 이 글의 인증 검증 코드와 룸 관리 예제를 참고해서 구현하면, 실제 서비스에 바로 적용할 수 있을 것이다.