티스토리 뷰

Development

슬랙 앱 만들기 삽질 기록기

siyoon210 2023. 10. 15. 23:13
반응형

슬랙 앱을 만들면서 다음에 같은 실수와 삽질을 하지 않도록 기록하는 포스팅.

(API 구현은 AWS Lambda에 Node기반으로 구현하였다.)

 

슬랙 앱 설정 관련

1. 슬랙에서 앱을 호출해야 한다면 Workspace에 설치되어야 한다.

Workspace 어드민에게 요청한 상태
어드민 승인이 되었고 실제로 설치가 가능한 상태

 

2. 슬랙 앱이 Bot으로 호출되어야 한다면 Bot 설정이 필요하다.

  • `App Hom`메뉴에서 Bot으로 설정
  • Display Name(Bot Name)과 Default Name 설정을 해야한다.

Bot 이름을 정해줘야 Bot 설정이 된다.
정상적으로 설치가 되었다면 'Bots' 항목이 활성화 되어있다.

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": [
            {
              // "..."
            }
      }
}
반응형
댓글