구글 스프레드시트의 상태 변경 내용을 Slack으로 보내는 Apps Script를 실행했는데 메시지가 도착하지 않고 다음과 같은 오류가 나타날 수 있습니다.
Exception: Request failed for https://hooks.slack.com
returned code 400
Slack 응답 본문에는 invalid_payload,
no_text, no_service 같은 문구가 표시될 수도
있습니다. 메시지가 너무 긴 경우에는 블록 제한 오류가 발생하거나 일부
내용이 잘릴 수 있습니다.
해결할 때는 코드 전체를 바로 바꾸지 말고, 먼저 Slack이 반환한 HTTP 상태 코드와 응답 본문을 확인해야 합니다. 응답 내용을 알아야 웹훅 주소 문제인지, JSON 형식 문제인지, 메시지 길이 문제인지 구분할 수 있습니다.
|
| Slack 알림이 도착하지 않으면 JSON 형식을 추측해서 수정하기 전에 HTTP 상태 코드와 응답 본문을 먼저 기록합니다. |
이 글은 Slack Incoming Webhook, 메시지 및 Block Kit 제한과 Google Apps Script의 UrlFetchApp·Properties Service·설치형 트리거 공식 문서를 기준으로 작성했습니다. 코드의 시트 이름과 열 번호는 실제 파일에 맞게 변경해야 합니다.
첫 단계: Slack 응답 코드와 본문을 기록하세요
UrlFetchApp.fetch()는 400번이나 500번대 응답을 받으면
예외를 발생시킬 수 있습니다. muteHttpExceptions: true를
사용하면 해당 응답을 객체로 받아 상태 코드와 내용을 확인할 수 있습니다.
이 옵션은 오류를 해결하거나 전송을 성공시키는 기능이 아닙니다. 실패 응답을 코드에서 직접 확인하게 해주는 디버깅 옵션입니다.
function testSlackResponse() {
const webhookUrl =
PropertiesService
.getScriptProperties()
.getProperty('SLACK_WEBHOOK_URL');
if (!webhookUrl) {
throw new Error(
'스크립트 속성에 SLACK_WEBHOOK_URL이 없습니다.'
);
}
const payload = {
text: 'Slack 웹훅 연결 테스트'
};
const response = UrlFetchApp.fetch(webhookUrl, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
const statusCode = response.getResponseCode();
const responseBody = response.getContentText().trim();
console.log(
JSON.stringify({
statusCode: statusCode,
responseBody: responseBody
})
);
if (statusCode !== 200 || responseBody !== 'ok') {
throw new Error(
'Slack 전송 실패 - HTTP ' +
statusCode +
': ' +
responseBody
);
}
console.log('Slack 메시지 전송 성공');
}
실행 결과를 확인하는 위치
- 스프레드시트에서 확장 프로그램 → Apps Script를 엽니다.
- 함수 목록에서
testSlackResponse를 선택합니다. - 실행을 누릅니다.
- 처음 실행한다면 요청되는 권한을 승인합니다.
- 편집기 아래쪽의 실행 로그를 확인합니다.
- 실패했다면 왼쪽의 실행 메뉴에서 해당 실행을 선택합니다.
정상 전송이면 HTTP 상태 코드 200과 응답 본문
ok가 기록됩니다.
응답 문구에 따라 원인을 구분하세요
invalid_payloadJSON 문법이나 Slack이 요구하는 필드 구조가 잘못됐을 가능성이 있습니다. 문자열을 직접 이어 붙이지 말고 객체를 만든 뒤
JSON.stringify()로 변환합니다.
no_text전송한 객체에 사용할 수 있는
text 내용이 없습니다.
최소한 {text: "알림 내용"} 형태로 테스트합니다.
no_service 또는 no_active_hooks웹훅이 삭제되거나 비활성화됐을 수 있습니다. Slack 앱 설정에서 현재 Incoming Webhook URL을 다시 확인합니다.
channel_is_archived웹훅이 연결된 Slack 채널이 보관 처리됐습니다. 활성 채널용 웹훅을 새로 만들어야 합니다.
action_prohibited워크스페이스 관리자가 웹훅을 통한 메시지 전송을 제한했을 수 있습니다. 같은 요청을 반복하기보다 Slack 관리자에게 앱 정책을 확인합니다.
too_many_attachments하나의 메시지에 첨부 항목을 지나치게 많이 넣었습니다. Slack Incoming Webhook의 메시지에는 최대 100개의 attachments가 허용됩니다.
화면에 단순히 Payload Size Exceeded 또는
payload too large처럼 표시된다면 그 문구만으로 원인을
확정하지 마세요. Apps Script가 보여주는 예외 문장과 Slack 응답 본문이
서로 다를 수 있으므로 위 코드로 실제 응답을 기록해야 합니다.
invalid_payload가 나타나면 문자열 연결부터 제거
셀에서 읽은 내용을 JSON 문자열 안에 직접 이어 붙이면 따옴표, 역슬래시와 줄바꿈 때문에 JSON 구조가 깨질 수 있습니다.
문제가 생길 수 있는 방식
const message = sheet.getRange('A2').getValue();
const payload =
'{"text":"' + message + '"}';
A2 셀에 큰따옴표나 여러 줄의 문장이 있다면 완성된 JSON 문자열이 올바르지 않을 수 있습니다.
자바스크립트 객체를 만든 뒤 JSON으로 변환
const message = sheet.getRange('A2').getDisplayValue();
const payloadObject = {
text: message
};
const payloadJson = JSON.stringify(payloadObject);
JSON.stringify()는 객체 안의 큰따옴표, 줄바꿈과
역슬래시가 JSON 문법을 깨뜨리지 않도록 변환합니다.
다만 JSON 형식이 올바르다고 해서 웹훅 주소, 채널 상태, Slack 필드 제한까지 해결되는 것은 아닙니다. 전송 후 상태 코드와 응답 본문을 계속 확인해야 합니다.
가장 작은 메시지부터 성공시키세요
복잡한 blocks와 attachments를 모두 포함한 상태에서는 어떤 필드가 원인인지 찾기 어렵습니다. 먼저 다음 최소 요청이 성공하는지 확인하세요.
{
"text": "연결 테스트"
}
최소 요청도 실패한다면 다음 항목을 먼저 확인합니다.
- 웹훅 URL을 복사할 때 앞뒤 공백이 들어가지 않았는지
- 현재 Slack 앱의 Incoming Webhook이 활성 상태인지
- 웹훅이 연결된 채널이 보관되지 않았는지
- 워크스페이스 관리자가 앱 전송을 제한하지 않았는지
- Apps Script가 외부 요청 권한을 승인받았는지
최소 요청은 성공하고 복잡한 메시지만 실패한다면, 웹훅 URL보다 blocks·attachments·메시지 길이와 JSON 구조를 확인해야 합니다.
긴 메시지는 한 번에 전송하지 말고 나누세요
일반 메시지의 text는 짧고 읽기 쉬운 형태가 적절합니다.
Slack은 4,000자 이내를 권장하며, 40,000자를 넘는 긴 메시지는 잘릴 수
있습니다.
Block Kit을 사용한다면 메시지 하나에 최대 50개 블록을 사용할 수 있고,
section 블록의 text는 최대 3,000자입니다.
스프레드시트의 전체 행이나 긴 로그를 모두 Slack 메시지 하나에 넣기보다 다음 정보만 보내는 편이 좋습니다.
- 무슨 작업에서 문제가 발생했는지
- 담당자나 상태
- 오류 문구의 앞부분
- 발생 시각
- 전체 자료를 확인할 스프레드시트 링크
일반 텍스트를 3,500자 이내로 나누어 전송하는 코드
const SLACK_WEBHOOK_PROPERTY =
'SLACK_WEBHOOK_URL';
const MAX_SLACK_TEXT_LENGTH = 3500;
const MAX_SLACK_MESSAGE_COUNT = 10;
function getSlackWebhookUrl_() {
const url =
PropertiesService
.getScriptProperties()
.getProperty(SLACK_WEBHOOK_PROPERTY);
if (!url) {
throw new Error(
'스크립트 속성에서 SLACK_WEBHOOK_URL을 찾지 못했습니다.'
);
}
return url.trim();
}
function postSlackPayload_(payloadObject) {
const webhookUrl = getSlackWebhookUrl_();
const payloadJson = JSON.stringify(payloadObject);
const response = UrlFetchApp.fetch(webhookUrl, {
method: 'post',
contentType: 'application/json',
payload: payloadJson,
muteHttpExceptions: true
});
const statusCode = response.getResponseCode();
const responseBody = response.getContentText().trim();
console.log(
JSON.stringify({
statusCode: statusCode,
responseBody: responseBody,
payloadCharacters: payloadJson.length
})
);
if (statusCode !== 200 || responseBody !== 'ok') {
throw new Error(
'Slack 전송 실패 - HTTP ' +
statusCode +
': ' +
responseBody
);
}
}
function splitSlackText_(sourceText) {
const text = String(sourceText || '')
.replace(/\r\n/g, '\n')
.trim();
if (!text) {
return ['(전송할 내용이 없습니다.)'];
}
const chunks = [];
let remaining = text;
while (
remaining.length >
MAX_SLACK_TEXT_LENGTH
) {
let cutPosition =
remaining.lastIndexOf(
'\n',
MAX_SLACK_TEXT_LENGTH
);
if (
cutPosition <
MAX_SLACK_TEXT_LENGTH / 2
) {
cutPosition =
MAX_SLACK_TEXT_LENGTH;
}
chunks.push(
remaining
.slice(0, cutPosition)
.trim()
);
remaining =
remaining
.slice(cutPosition)
.trim();
}
if (remaining) {
chunks.push(remaining);
}
if (
chunks.length >
MAX_SLACK_MESSAGE_COUNT
) {
throw new Error(
'메시지가 너무 깁니다. ' +
'전체 내용을 보내지 말고 요약과 문서 링크를 전송하세요.'
);
}
return chunks;
}
function sendSlackText(text) {
const chunks = splitSlackText_(text);
chunks.forEach(function(chunk, index) {
const sequence =
chunks.length > 1
? '[' +
(index + 1) +
'/' +
chunks.length +
']\n'
: '';
postSlackPayload_({
text: sequence + chunk
});
if (index < chunks.length - 1) {
Utilities.sleep(1100);
}
});
}
function testLongSlackMessage() {
const sampleText = [
'*작업 알림*',
'상태: 확인 필요',
'발생 시각: ' + new Date(),
'상세 내용은 스프레드시트에서 확인하세요.'
].join('\n');
sendSlackText(sampleText);
}
이 코드는 메시지를 무조건 많이 보내기 위한 코드가 아닙니다. 최대 10개를 넘으면 전송을 중단하고, 긴 원문 대신 요약과 문서 링크를 사용하도록 안내합니다.
메시지 사이의 짧은 대기는 여러 요청이 한꺼번에 몰리는 것을 줄이기 위한 것입니다. 이미 발생한 일일 Apps Script 할당량 오류를 복구하는 기능은 아닙니다.
Block Kit을 사용한다면 블록별 제한 확인
Slack 메시지의 모양을 꾸미기 위해 blocks를 사용하면 전체 글자 수만 확인해서는 안 됩니다. 각 블록 종류마다 별도의 제한이 있습니다.
다음 예제는 한 개의 header와 한 개의 section만 사용하는 단순한 알림입니다.
function sendSlackBlockMessage() {
const taskName = '월간 보고서 확인';
const status = '완료';
const payload = {
text:
taskName +
' 상태가 ' +
status +
'로 변경됐습니다.',
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: '업무 상태 변경',
emoji: true
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text:
'*업무:* ' +
taskName +
'\n*상태:* ' +
status
}
}
]
};
postSlackPayload_(payload);
}
header의 텍스트는 최대 150자이고, section의 텍스트는 최대 3,000자입니다. 한 메시지에 사용할 수 있는 blocks는 최대 50개입니다.
각 스프레드시트 행을 별도의 section 블록으로 만들면 데이터가 많을 때 블록 개수를 빠르게 넘을 수 있습니다. 모든 행을 보내지 말고 필요한 행만 선택하거나 여러 메시지로 분리하세요.
웹훅 URL을 코드에 직접 적지 마세요
Slack Incoming Webhook URL은 메시지를 보낼 수 있는 비밀 주소입니다. 블로그, 공개 문서, GitHub와 다른 사람에게 공유되는 코드에 실제 주소를 넣으면 안 됩니다.
Apps Script의 스크립트 속성에 저장하는 방법
- Apps Script 편집기 왼쪽에서 프로젝트 설정을 선택합니다.
- 스크립트 속성 영역으로 이동합니다.
- 스크립트 속성 추가를 선택합니다.
- 속성 이름에
SLACK_WEBHOOK_URL을 입력합니다. - 값에 실제 Incoming Webhook URL을 붙여 넣습니다.
- 스크립트 속성 저장을 누릅니다.
코드에서는 다음처럼 값을 읽습니다.
const webhookUrl =
PropertiesService
.getScriptProperties()
.getProperty('SLACK_WEBHOOK_URL');
코드에서 주소만 삭제하는 것으로 끝내지 말고 Slack 앱 설정에서 해당 웹훅을 폐기한 뒤 새 웹훅을 만들어야 합니다.
스프레드시트 편집 시 Slack 알림 보내기
예를 들어 업무현황 시트의 D열 상태가
완료로 바뀌었을 때 Slack 메시지를 보내려면 다음과 같이
수정 범위를 먼저 확인할 수 있습니다.
- A열: 업무명
- B열: 담당자
- C열: 마감일
- D열: 상태
- 1행: 제목
- 2행부터: 실제 데이터
function handleStatusEdit(e) {
if (!e || !e.range) {
return;
}
const range = e.range;
const sheet = range.getSheet();
if (sheet.getName() !== '업무현황') {
return;
}
if (
range.getNumRows() !== 1 ||
range.getNumColumns() !== 1
) {
return;
}
if (
range.getRow() < 2 ||
range.getColumn() !== 4
) {
return;
}
const status = range.getDisplayValue();
if (status !== '완료') {
return;
}
const row = range.getRow();
const values =
sheet
.getRange(row, 1, 1, 3)
.getDisplayValues()[0];
const taskName = values[0];
const owner = values[1];
const dueDate = values[2];
const spreadsheetUrl =
e.source.getUrl();
const message = [
'*업무 완료 알림*',
'업무: ' + taskName,
'담당자: ' + owner,
'마감일: ' + dueDate,
'상태: ' + status,
'문서: ' + spreadsheetUrl
].join('\n');
sendSlackText(message);
}
코드의 업무현황, D열과 완료라는 값은 실제
시트 구조에 맞게 변경해야 합니다.
Slack 전송에는 설치형 수정 트리거를 사용하세요
함수 이름을 단순히 onEdit로 작성한 단순 트리거는 권한이
필요한 외부 요청 서비스를 사용할 수 없습니다.
UrlFetchApp으로 Slack에 요청하려면 설치형 수정 트리거를
만들어 권한을 승인해야 합니다.
- Apps Script 편집기 왼쪽에서 트리거를 선택합니다.
- 오른쪽 아래의 트리거 추가를 누릅니다.
- 실행할 함수에서
handleStatusEdit를 선택합니다. - 이벤트 소스에서 스프레드시트에서를 선택합니다.
- 이벤트 유형에서 수정 시를 선택합니다.
- 저장을 누르고 필요한 권한을 승인합니다.
설치형 트리거는 트리거를 만든 사람의 계정 권한으로 실행됩니다. 다른 사용자가 셀을 수정해도 Slack 요청은 트리거를 만든 계정의 권한으로 처리됩니다.
이 함수는 사용자가 시트를 수정할 때 전달되는 이벤트 객체
e가 필요합니다. 직접 실행하면 실제 수정 정보가 없어 바로
종료됩니다. 웹훅 연결 자체는 testSlackResponse로 먼저
확인하세요.
전송 오류를 숨긴 채 정상 처리하지 마세요
다음처럼 응답 코드를 기록만 하고 함수를 종료하면 Slack 메시지가 실패했는데도 이후 코드가 정상 완료된 것처럼 보일 수 있습니다.
if (response.getResponseCode() !== 200) {
console.log(response.getContentText());
}
중요한 알림이라면 응답 내용을 기록한 뒤 예외를 발생시키거나, 전송 실패 상태를 별도 시트에 남기는 처리가 필요합니다.
if (
statusCode !== 200 ||
responseBody !== 'ok'
) {
throw new Error(
'Slack 전송 실패 - HTTP ' +
statusCode +
': ' +
responseBody
);
}
단순히 try...catch로 모든 오류를 잡고 아무것도 기록하지
않으면 알림 누락을 발견하기 어려워집니다.
현재 상황에 맞는 수정 위치
JSON 문자열 직접 연결을 제거하고 객체와
JSON.stringify()를 사용합니다.
text 길이, blocks 수와 각 section의 길이를 확인합니다.
전체 데이터를 한 메시지로 보내지 말고 요약과 스프레드시트 링크를 전송합니다.
단순 onEdit이 아니라 권한을 승인한 설치형 수정 트리거인지 확인합니다.
기존 웹훅이 삭제되거나 비활성화되지 않았는지 Slack 앱 설정을 확인합니다.
Slack 요청 형식 외에도 Apps Script 할당량과 단시간 호출 제한을 함께 확인합니다.
내용 확인에 사용한 공식 자료
- Slack Developer Docs: Incoming Webhook으로 메시지 보내기
- Slack Developer Docs: 메시지 텍스트 길이와 오류 응답
- Slack Developer Docs: Section block 필드와 글자 제한
- Slack Developer Docs: 메시지당 Block Kit 블록 제한
- Google for Developers: Apps Script에서 외부 API와 JSON 사용
- Google for Developers: UrlFetchApp 요청과 응답
- Google for Developers: Properties Service로 설정값 저장
- Google for Developers: 설치형 수정 트리거
공식 문서 확인일: 2026년 7월 28일
댓글 쓰기