티스토리 뷰
반응형
슬랙 앱을 만들면서 다음에 같은 실수와 삽질을 하지 않도록 기록하는 포스팅.
(API 구현은 AWS Lambda에 Node기반으로 구현하였다.)
슬랙 앱 설정 관련
1. 슬랙에서 앱을 호출해야 한다면 Workspace에 설치되어야 한다.


2. 슬랙 앱이 Bot으로 호출되어야 한다면 Bot 설정이 필요하다.
- `App Hom`메뉴에서 Bot으로 설정
- Display Name(Bot Name)과 Default Name 설정을 해야한다.


3. 대화식 상호작용을 구현하고 싶다면 Interactivity 설정을 해준다.
- 슬랙내 대화식 상호작용, 버튼이나 숏컷과 같은 컴포넌트로 대화식 상호작용을 구현하고 싶은경우 Interactivity 설정에 API를 명시한다.

4. Slash Command를 구현하고 싶다면 Slash Command 설정을 해준다.
- 슬랙내 슬래시 커맨드를 구현하고 싶은경우 Slash Command Request URL 설정에 API를 명시한다.


API 요청 처리
5. 슬랙요청은 URL 매개변수 형태로 요청된다.
- `=` 로 key, value가 구분되고, `&` 로 여러 파라미터를 입력받는다.
6. SlashCommand로 요청될때와, Interactivity로 요청될때의 요청 매개변수 구성요소가 다르다.
- SlashCommand로 요청할때는 URL 매개변수 안에 모든 정보가 들어있다. channel_name, team_domain, response_url, command, text, user_name 같은 매개변수가 있다.
- command는 요청된 slashCommand이고, text는 command 이후에 입력된 값이다. 예를들어 '/mycommand siyoon' 으로 입력하면 command는 '/mycommand' 이고 text는 'siyoon'이 된다.
- Interactivity로 요청될때는 URL 매개변수는 payload 하나만 있고, 이를 URI decode 한뒤에 JSON 형태로 만들어서 사용해야한다. (URL encode된 String 형태로 요청이 온다.)
- 그래서 Interactivity인지 SlashCommand인지를 구분하려면 payload의 존재유무를 확인하면 된다.
7. SlashCommand 요청은 API 응답으로 바로 메세지를 표기 할 수 있다.
- SlashCommand는 요청받은 API에서 다음과 같이 응답을 내린다면 바로 슬랙내에 메세지를 표기할 수 있다.
// 텍스트를 바로 응답하는 경우
return {
statusCode: 200,
body: '...'
};
// JSON 형태로 보내는 경우
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify('...')
};
8. Interactivity 요청은 API 응답으로 바로 메세지를 표기 할 수 없다. 웹훅URL을 통해서 메세지 응답을 보내야한다.
- payload 값에 response_url을 웹훅 URL로 사용하여서 http 요청을 직접 보내야한다.
9. Interactivity 요청시에 사용자가 입력한 값들은 payload.state.values[] 에 들어있다.
- 키값이 그대로 들어있지 않아서 개발자가 사용하기 편한 형태로 변환하는 방법은 아래 'module.exports.clientInput' 코드 참고
따라서 아래와 같은 Util 코드를 선언해두면 API 구현에 도움이 된다.
const https = require('https');
const url = require('url');
// URL 매개변수식으로 온 슬랙 요청을 JSON (key,value) 형식으로 추출
// 모든 요청 처리시에 처음으로 수행해야 한다.
module.exports.slackRequestParams = (rawRequestBody) => {
const slackRequestParams = rawRequestBody.split('&').reduce((accumulator, pair) => {
const [key, value] = pair.split('=');
accumulator[key] = decodeURIComponent(value);
return accumulator;
}, {});
console.log('=> slackRequestParams:', slackRequestParams);
return slackRequestParams;
};
// Interactivity form으로 요청한 경우만 payload가 존재한다.
module.exports.isInteractivityRequest = (slackRequestParams) => {
return slackRequestParams.payload;
};
// Interactivity payload를 JSON 형식으로 추출 (String -> JSON)
module.exports.parsedInteractivityPayload = (slackRequestParams) => {
const parsedInteractivityPayload = JSON.parse(decodeURIComponent(slackRequestParams.payload));
console.log('=> Interactivity parsedInteractivityPayload:', parsedInteractivityPayload);
return parsedInteractivityPayload;
};
// Interactivity form에서 사용자가 입력한 값 추출 (payload.state.values[] 형식으로 입력됨)
module.exports.clientInput = (parsedInteractivityPayload) => {
const clientInput = {};
Object.values(parsedInteractivityPayload.state.values).forEach(input => {
const key = Object.keys(input)[0];
clientInput[key] = Object.values(input)[0].value;
});
console.log('=> Interactivity clientInput:', clientInput);
return clientInput;
};
// Interactivity 응답시에 사용. 웹휵 URL을 사용한다.
// axios 같은 외부 패키지 의존성없이 구현하기 위해서 https와 url로 구현됨
module.exports.sendResponseViaHookURL = (responseHookUrl, jsonResponse) => {
return new Promise((resolve, reject) => {
const uri = url.parse(responseHookUrl, true);
const options = {
hostname: uri.hostname,
path: uri.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
};
const req = https.request(options, res => {
let rawData = '';
res.on('data', chunk => {
rawData += chunk;
});
res.on('end', () => {
try {
resolve(rawData);
} catch (err) {
reject(err);
}
});
});
req.on('error', err => {
reject(err);
});
req.write(JSON.stringify(jsonResponse));
req.end();
}).then(data => {
console.log('=> success:', data);
return {
statusCode: 200
};
}).catch(error => {
console.error('=> error:', error);
return {
statusCode: 500,
body: error
};
});
};
사용하는 샘플코드
const SlackUtils = require('./slack-utils');
//...
// 슬랙 요청 파라미터를 JSON 형태로 추출
const slackRequestParams = SlackUtils.slackRequestParams(event.body);
// Interactivity 요청인 경우
if (SlackUtils.isInteractivityRequest(slackRequestParams)) {
const parsedInteractivityPayload = SlackUtils.parsedInteractivityPayload(slackRequestParams);
const clientInput = SlackUtils.clientInput(parsedInteractivityPayload);
//...
// Interactivity 요청은 웹훅을 통해서 응답한다.
return await SlackUtils.sendResponseViaHookURL(parsedInteractivityPayload.response_url, '...');
}
// SlashCommand 요청인 경우
if (slackRequestParams.text.startsWith("...")) {
// SlashCommand 요청은 API 응답으로 바로 가능하다.
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify('...')
};
}
기타 사항
10. AWS Lambda는 event.body에 base64 인코딩된 형태로 요청이 온다.
- 그래서 다음과 같은 선행작업을 해둬야 한다.
// 람다 요청 이벤트 확인 & base64 디코딩
console.log('=> event:', event);
if (event.isBase64Encoded) {
event.body = Buffer.from(event.body, "base64").toString('utf8');
console.log('=> base64 decode event.body:', event.body);
}
11. 슬랙 응답 메세지는 별도의 파일로 관리하는 것이 관리하기 편하다.
module.exports.slackResult = () => {
return {
"blocks": [
{
// "..."
}
}
}
12. 외부 워크스페이스에서 설치가 필요하다면 OAuth 요청 핸들링이 필요하다.
const https = require('https');
/**
* 요청받은 람다 이벤트가 OAuth 인증요청인지 확인
*/
module.exports.isOAuthRequest = (event) => {
const isOAuthRequestEvent = event.httpMethod === 'GET' && !!event.queryStringParameters.code;
console.log('=> isOAuthRequest:', isOAuthRequestEvent);
return isOAuthRequestEvent;
};
/**
* 다른 워크스페이스에서 사용하기 위해서는 OAuth 핸들링을 구현해야 한다.
* code : 슬랙에서 보내온 인증용 code값. 이를 기반으로 슬랙에게 인증을 요청한다.
*/
module.exports.handle = async (event) => {
const code = event.queryStringParameters.code;
const clientId = '**********'; // 앱 Settings -> Basic Information
const clientSecret = '********'; // 앱 Settings -> Basic Information
const redirectUri = 'https://my-uri'; // 요청받은 URI와 동일하게 입력
const tokenUrl = `https://slack.com/api/oauth.v2.access?client_id=${clientId}&client_secret=${clientSecret}&code=${code}&redirect_uri=${redirectUri}`;
const response = await new Promise((resolve, reject) => {
https.get(tokenUrl, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
resolve(JSON.parse(data));
});
}).on('error', (err) => {
reject(err);
});
});
console.log('=> slack token Response:', JSON.stringify(response));
if (!response.ok) {
return {
statusCode: 500,
body: JSON.stringify({ error: 'OAuth authentication failed', details: response }),
};
}
// Success - Respond back or redirect to a success page
return {
statusCode: 200,
body: JSON.stringify({ success: true, data: response }),
};
};
반응형
'Development' 카테고리의 다른 글
Null 공포증을 내려놓자 (1) | 2024.07.24 |
---|---|
절차지향 프로그래밍 붐은 온다.. (객체지향에 얽매이지 말자) (0) | 2024.05.15 |
IntelliJ Live Template으로 테스트 코드 빠르게 작성하기 (0) | 2021.10.17 |
maven 저장소에 라이브러리 업로드하기 (2) | 2020.09.13 |
동기 vs 비동기, 블로킹 vs 논블로킹 쉽게 이해하기 (29) | 2019.08.08 |