Slack을 업무 커뮤니케이션 도구로 쓰는 팀이라면, 자동화된 알림을 단순히 채널에 뿌리는 것보다 기존 메시지에 스레드로 답장하는 게 훨씬 깔끔하다. 하지만 대부분의 개발자들은 Slack API 문서를 읽어도 thread_ts라는 개념이 명확하지 않아서, 스레드 기능을 포기하고 그냥 일반 메시지만 보낸다. 이번에는 Slack API에서 스레드를 다루는 정확한 원리와 실전 구현 방법을 완벽하게 정리해서 소개하겠다.

 

1단계. Slack 스레드의 기초 개념 이해하기

Slack의 메시지는 크게 두 가지 유형으로 나뉜다. 하나는 채널에 직접 올라오는 일반 메시지이고, 다른 하나는 기존 메시지에 딸려 있는 스레드 메시지다. 스레드 메시지는 채널의 메인 스트림을 지저분하게 만들지 않으면서도 관련된 대화를 한데 묶을 수 있다.

Slack API에서 스레드를 구분하는 핵심은 바로 thread_ts(스레드 타임스탬프)다. 채널에 새 메시지를 올리면 그 메시지가 타임스탬프(ts)를 받는다. 이 타임스탬프를 thread_ts로 사용하면, 그 이후로 보내는 모든 메시지들이 그 메시지 아래 스레드로 달린다. 만약 thread_ts를 지정하지 않으면 새로운 최상위 메시지가 되는 것이다.

 

스레드 vs 일반 메시지의 메커니즘

일반 메시지를 먼저 보내서 ts를 얻은 후, 그 ts를 thread_ts로 사용해서 답장을 보내는 구조다. 예를 들어 주문 완료 알림 메시지를 보낸 후, 결제 완료, 배송 시작 같은 업데이트들을 모두 그 메시지 아래 스레드로 달 수 있다.

 

2단계. Slack Bot 토큰 설정 및 권한 확인

스레드 메시지를 보내려면 먼저 Slack 앱에 올바른 권한이 있어야 한다. Slack API 페이지에서 앱을 만들거나 기존 앱을 열고, OAuth & Permissions 섹션으로 가자.

Bot Token Scopes에 다음 권한들을 추가해야 한다.

  • chat:write: 메시지 전송 권한
  • files:write: 파일 업로드 권한(나중에 스레드에서 파일을 보낼 때 필요)
  • channels:read: 채널 정보 조회 권한

권한을 추가한 후 워크스페이스에 앱을 재설치하고, 생성된 Bot User OAuth Token을 복사해서 안전한 곳에 저장하자. 이 토큰이 없으면 API 요청이 인증되지 않는다.

 

3단계. PHP에서 첫 메시지 보내고 타임스탬프 얻기

스레드 기능을 쓰려면 먼저 채널에 최상위 메시지를 보내야 한다. 이 메시지의 타임스탬프를 받아야 그것을 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를 계속 사용하면 모든 메시지가 같은 스레드 아래에 달린다.

 

4단계. 스레드에 파일 첨부하기

메시지만 보내는 것도 유용하지만, 영수증, 송장, 보고서 같은 파일을 스레드에 첨부해야 할 때도 있다. 이 경우 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를 포함시키면 파일이 스레드 안에 올라온다.

 

5단계. 주의사항 및 흔한 실수

 

✗ 실수 1: thread_ts에 잘못된 값 넣기

thread_ts는 정확히 최상위 메시지의 ts여야 한다. 다른 스레드 메시지의 ts를 사용하면 작동하지 않는다. 또한 ts는 "1234567890.123456" 형식의 문자열이므로 숫자로 변환하면 안 된다.

 

✗ 실수 2: reply_broadcast 옵션 모르기

기본적으로 스레드 메시지는 스레드 안에만 표시된다. 하지만 reply_broadcast: true를 추가하면 메시지가 스레드에도 표시되고 채널 메인 스트림에도 표시된다. 중요한 업데이트를 모두가 볼 수 있게 하려면 이 옵션을 사용하자.

<?php
// 스레드에도 보이고 채널 메인에도 보이는 메시지
$message = sendSlackMessage($slackToken, array(
    'channel' => $channel,
    'thread_ts' => $threadTs,
    'text' => '배송 완료되었습니다.',
    'reply_broadcast' => true
));
?>

 

✗ 실수 3: 타임아웃 설정 무시하기

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 스레드 자동화로 업무 효율 높이기

Slack 스레드는 메시지 구조를 정리하고 채널을 깔끔하게 유지하는 핵심 도구다. 주문 상태, 배포 로그, 알림 등 연관된 업데이트들을 스레드로 묶으면 팀원들이 맥락을 파악하기 훨씬 쉬워진다. thread_ts 개념만 제대로 이해하고, 응답에서 타임스탬프를 정확히 추출해서 다음 메시지에 사용하면 복잡한 자동화도 간단하게 구현할 수 있다. 이 글의 재시도 로직과 에러 처리 부분을 참고해서 안정적인 Slack 연동 시스템을 구축하면, 자동 알림의 신뢰성을 크게 높일 수 있을 것이다.