Slack을 업무 커뮤니케이션 도구로 쓰는 팀이라면, 자동화된 알림을 단순히 채널에 뿌리는 것보다 기존 메시지에 스레드로 답장하는 게 훨씬 깔끔하다. 하지만 대부분의 개발자들은 Slack API 문서를 읽어도 thread_ts라는 개념이 명확하지 않아서, 스레드 기능을 포기하고 그냥 일반 메시지만 보낸다. 이번에는 Slack API에서 스레드를 다루는 정확한 원리와 실전 구현 방법을 완벽하게 정리해서 소개하겠다.
Slack의 메시지는 크게 두 가지 유형으로 나뉜다. 하나는 채널에 직접 올라오는 일반 메시지이고, 다른 하나는 기존 메시지에 딸려 있는 스레드 메시지다. 스레드 메시지는 채널의 메인 스트림을 지저분하게 만들지 않으면서도 관련된 대화를 한데 묶을 수 있다.
Slack API에서 스레드를 구분하는 핵심은 바로 thread_ts(스레드 타임스탬프)다. 채널에 새 메시지를 올리면 그 메시지가 타임스탬프(ts)를 받는다. 이 타임스탬프를 thread_ts로 사용하면, 그 이후로 보내는 모든 메시지들이 그 메시지 아래 스레드로 달린다. 만약 thread_ts를 지정하지 않으면 새로운 최상위 메시지가 되는 것이다.
일반 메시지를 먼저 보내서 ts를 얻은 후, 그 ts를 thread_ts로 사용해서 답장을 보내는 구조다. 예를 들어 주문 완료 알림 메시지를 보낸 후, 결제 완료, 배송 시작 같은 업데이트들을 모두 그 메시지 아래 스레드로 달 수 있다.
스레드 메시지를 보내려면 먼저 Slack 앱에 올바른 권한이 있어야 한다. Slack API 페이지에서 앱을 만들거나 기존 앱을 열고, OAuth & Permissions 섹션으로 가자.
Bot Token Scopes에 다음 권한들을 추가해야 한다.
- chat:write: 메시지 전송 권한
- files:write: 파일 업로드 권한(나중에 스레드에서 파일을 보낼 때 필요)
- channels:read: 채널 정보 조회 권한
권한을 추가한 후 워크스페이스에 앱을 재설치하고, 생성된 Bot User OAuth Token을 복사해서 안전한 곳에 저장하자. 이 토큰이 없으면 API 요청이 인증되지 않는다.
스레드 기능을 쓰려면 먼저 채널에 최상위 메시지를 보내야 한다. 이 메시지의 타임스탬프를 받아야 그것을 thread_ts로 사용할 수 있다.
<?php
$channel = 'C123456789';
$threadTs = null;
// 최상위 메시지 발송
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => 'https://slack.com/api/chat.postMessage',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(array(
'channel' => $channel,
'text' => '주문 #12345 완료되었습니다.'
)),
CURLOPT_HTTPHEADER => array(
'Authorization: Bearer xoxb-YOUR-TOKEN',
'Content-Type: application/json'
)
));
$response = curl_exec($curl);
// $response를 처리하지 않고 바로 스레드 메시지 발송 시도
// 스레드 메시지 발송 (실패: thread_ts가 null)
curl_setopt_array($curl, array(
CURLOPT_POSTFIELDS => json_encode(array(
'channel' => $channel,
'thread_ts' => $threadTs, // null이므로 작동 안 함
'text' => '결제가 완료되었습니다.'
))
));
curl_exec($curl);
curl_close($curl);
?>
이 코드는 thread_ts가 null이므로 스레드가 만들어지지 않는다. 응답을 처리해야 한다.
<?php
$slackToken = 'xoxb-YOUR-BOT-TOKEN';
$channel = 'C123456789';
// 함수: Slack API 호출
function sendSlackMessage($token, $data) {
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => 'https://slack.com/api/chat.postMessage',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_HTTPHEADER => array(
'Authorization: Bearer ' . $token,
'Content-Type: application/json'
),
CURLOPT_TIMEOUT => 10
));
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
$result = json_decode($response, true);
if ($httpCode !== 200 || !$result['ok']) {
throw new Exception('Slack API Error: ' . ($result['error'] ?? 'Unknown error'));
}
return $result;
}
try {
// Step 1: 최상위 메시지 발송
$mainMessage = sendSlackMessage($slackToken, array(
'channel' => $channel,
'text' => '주문 #12345 완료되었습니다.',
'blocks' => array(
array(
'type' => 'section',
'text' => array(
'type' => 'mrkdwn',
'text' => '*주문 #12345*\n상태: 완료\n금액: 50,000원'
)
)
)
));
// 타임스탬프 추출
$threadTs = $mainMessage['ts'];
echo "최상위 메시지 전송 완료. TS: " . $threadTs . "\n";
// Step 2: 스레드에 답장 메시지 발송
$threadMessage = sendSlackMessage($slackToken, array(
'channel' => $channel,
'thread_ts' => $threadTs,
'text' => '결제가 완료되었습니다.'
));
echo "스레드 메시지 전송 완료\n";
// Step 3: 같은 스레드에 또 다른 메시지
$threadUpdate = sendSlackMessage($slackToken, array(
'channel' => $channel,
'thread_ts' => $threadTs,
'text' => '배송이 준비되고 있습니다.'
));
echo "스레드 업데이트 전송 완료\n";
} catch (Exception $e) {
echo "오류: " . $e->getMessage();
}
?>
이제 응답에서 ts를 추출해서 thread_ts로 사용한다. 같은 thread_ts를 계속 사용하면 모든 메시지가 같은 스레드 아래에 달린다.
메시지만 보내는 것도 유용하지만, 영수증, 송장, 보고서 같은 파일을 스레드에 첨부해야 할 때도 있다. 이 경우 files.upload API를 사용한다.
<?php
$slackToken = 'xoxb-YOUR-BOT-TOKEN';
$channel = 'C123456789';
$threadTs = '1234567890.123456'; // 이전 단계에서 얻은 타임스탬프
// 함수: 스레드에 파일 업로드
function uploadFileToSlackThread($token, $channel, $threadTs, $filePath, $comment) {
$curl = curl_init();
$cfile = curl_file_create(
$filePath,
mime_content_type($filePath),
basename($filePath)
);
curl_setopt_array($curl, array(
CURLOPT_URL => 'https://slack.com/api/files.upload',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => array(
'file' => $cfile,
'channels' => $channel,
'thread_ts' => $threadTs,
'initial_comment' => $comment
),
CURLOPT_HTTPHEADER => array(
'Authorization: Bearer ' . $token
),
CURLOPT_TIMEOUT => 30
));
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
$result = json_decode($response, true);
if ($httpCode !== 200 || !$result['ok']) {
throw new Exception('File upload failed: ' . ($result['error'] ?? 'Unknown error'));
}
return $result;
}
try {
// 로컬에 있는 파일을 스레드에 업로드
uploadFileToSlackThread(
$slackToken,
$channel,
$threadTs,
'/tmp/invoice_12345.pdf',
'주문 영수증입니다.'
);
echo "파일 업로드 완료\n";
} catch (Exception $e) {
echo "오류: " . $e->getMessage();
}
?>
files.upload를 사용할 때는 Content-Type을 multipart/form-data로 자동 설정되고, thread_ts를 포함시키면 파일이 스레드 안에 올라온다.
thread_ts는 정확히 최상위 메시지의 ts여야 한다. 다른 스레드 메시지의 ts를 사용하면 작동하지 않는다. 또한 ts는 "1234567890.123456" 형식의 문자열이므로 숫자로 변환하면 안 된다.
기본적으로 스레드 메시지는 스레드 안에만 표시된다. 하지만 reply_broadcast: true를 추가하면 메시지가 스레드에도 표시되고 채널 메인 스트림에도 표시된다. 중요한 업데이트를 모두가 볼 수 있게 하려면 이 옵션을 사용하자.
<?php
// 스레드에도 보이고 채널 메인에도 보이는 메시지
$message = sendSlackMessage($slackToken, array(
'channel' => $channel,
'thread_ts' => $threadTs,
'text' => '배송 완료되었습니다.',
'reply_broadcast' => true
));
?>
Slack API는 느릴 수 있다. curl_setopt에서 CURLOPT_TIMEOUT을 최소 10초 이상으로 설정해야 한다. 기본값 30초면 충분하지만, 파일 업로드 시에는 30~60초로 늘리자.
<?php
function sendSlackMessageWithRetry($token, $data, $maxRetries = 3) {
$retryCount = 0;
while ($retryCount < $maxRetries) {
try {
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => 'https://slack.com/api/chat.postMessage',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_HTTPHEADER => array(
'Authorization: Bearer ' . $token,
'Content-Type: application/json'
),
CURLOPT_TIMEOUT => 15,
CURLOPT_CONNECTTIMEOUT => 10
));
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
$curlError = curl_error($curl);
curl_close($curl);
if ($curlError) {
throw new Exception('cURL error: ' . $curlError);
}
$result = json_decode($response, true);
if ($httpCode === 200 && $result['ok']) {
return $result;
}
// 429 (Rate Limited): 재시도
if ($httpCode === 429) {
$retryCount++;
sleep(1);
continue;
}
// 그 외 에러
throw new Exception('Slack API Error: ' . ($result['error'] ?? 'HTTP ' . $httpCode));
} catch (Exception $e) {
$retryCount++;
if ($retryCount >= $maxRetries) {
throw $e;
}
sleep(pow(2, $retryCount - 1)); // 지수 백오프
}
}
}
?>
이 함수는 429 Rate Limited 에러나 네트워크 오류 시 자동으로 재시도하고, 지수 백오프를 사용해서 Slack 서버에 무리를 주지 않는다.
Slack 스레드는 메시지 구조를 정리하고 채널을 깔끔하게 유지하는 핵심 도구다. 주문 상태, 배포 로그, 알림 등 연관된 업데이트들을 스레드로 묶으면 팀원들이 맥락을 파악하기 훨씬 쉬워진다. thread_ts 개념만 제대로 이해하고, 응답에서 타임스탬프를 정확히 추출해서 다음 메시지에 사용하면 복잡한 자동화도 간단하게 구현할 수 있다. 이 글의 재시도 로직과 에러 처리 부분을 참고해서 안정적인 Slack 연동 시스템을 구축하면, 자동 알림의 신뢰성을 크게 높일 수 있을 것이다.