회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
AWS 이용 중이라면 최대 700만 원 지원받으세요
오랜만에 재미있는 사이드 프로젝트를 하나 했습니다. 프로젝트 이름은 대나무숲(Bamboo Forest)이라고 지었는데, 어떤 주제인지 대충 감이 오시나요?
회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!
확인
본 프로젝트의 전체 코드는 Github에서 확인할 수 있습니다.
오랜만에 재미있는 사이드 프로젝트를 하나 했습니다. 프로젝트 이름은 대나무숲(Bamboo Forest)이라고 지었는데, 어떤 주제인지 대충 감이 오시나요?
네, 짐작하신 게 맞습니다! 대나무숲 애플리케이션(이하 앱)에서는 기본적으로 슬랙 채널에 익명으로 메시지와 스레드를 전송할 수 있는 기능을 제공합니다. 이를 이용해 슬랙 워크스페이스의 멤버들이 익명에 기대어 솔직한 이야기를 할 수 있는 채널을 만들고자 했습니다.
사실 데모를 보면 알 수 있듯이, 아이디어 제안부터 배포까지 3일이 채 걸리지 않은 간단한 프로젝트입니다. 그럼에도 불구하고 이 프로젝트의 개발 후기를 공유하는 이유는, 짧은 시간 내에 약 200명 규모의 커뮤니티가 마주한 문제를 효율적으로 해결했기 때문입니다.
기존에 어떤 문제가 있었는지를 진단하고, 이를 기술적 관점에서 어떻게 해결했으며, 사용자의 피드백을 다시 제품에 적용하는 그 과정이 실제 서비스를 개발하는 것과 크게 다르지 않다고 여겨졌습니다. 그래서 오늘은 대나무숲 슬랙 앱의 개발 과정 및 후기를 공유합니다. 이번 포스트를 통해 슬랙 앱 개발에 관심이 있으신 분들께 도움이 되었으면 좋겠습니다.
제가 활동하고 있는 글 쓰는 개발자 커뮤니티 글또는 주 활동을 슬랙에서 합니다. 글또는 기수제로 운영되기 때문에 폐쇄성을 지닌 커뮤니티인데요, 하지만 커뮤니티의 심리적 안정감이 아무리 높아도 고민을 실명으로 말하기는 어려울 수 있다는 생각으로 인해 약 2년 전부터는 익명으로 고민 상담을 할 수 있는 채널이 생겼습니다. 당시에는 간편하게 적용할 수 있는 Anonymous Bot이라는 서드파티 슬랙 앱을 이용했습니다.
이 덕분에 개발과 관련된 진지하고 민감한 고민이 많이 올라왔습니다. 커리어 관련 고민, 이직과 연봉 협상, 취업과 면접, 번아웃 등등… 익명에 기대어 이야기할 수 있는 채널이 마련되었기 때문에 많은 사람이 부담 없이 솔직하게 자신의 고민을 남겼습니다.
스레드 역시 익명으로 달 수 있었기에 응원과 토론이 가능했고, 이에 따라 고민 상담 채널은 글또 슬랙 내에서도 많은 분이 좋아하는 채널이 되었습니다.
하지만 커뮤니티의 규모가 점차 커지면서 문제가 발생했습니다. Anonymous Bot의 무료 플랜은 월 10개의 익명 메시지만 보낼 수 있는데, 7기에 이르면서 멤버 수가 거의 200명에 가까워질 정도로 규모가 성장했기 때문입니다. Anonymous Bot은 멤버 수에 따라 과금액이 늘어나기 때문에 조금은 부담스러운 수준이 되었죠. 이를 대체할 만한 다른 서드파티 앱도 찾아보았지만, 웬만해서는 다 과금해야 사용할 수 있었습니다.
그런데, 생각해보면 요구사항이 그리 복잡한 게 아닙니다. 그냥 사용자가 입력한 메시지를 다른 계정의 명의로 똑같이 전송하면 됐기 때문이죠. 마치 echo
명령어처럼요.
마침 이번 기수부터 운영진 역할로 커뮤니티에 참여하게 되면서, 이 부분에 대한 개선을 맡기로 했습니다. 7기의 시작 날짜가 5월 첫째 주였고, 오리엔테이션에서 대나무숲 앱 시연도 하면 좋겠다는 의견이 있어 ‘빠르게 개발을 시작해야겠다’라고 결심했습니다. 이때 오리엔테이션까지 남은 시간이 일주일 남짓이었습니다.
우선 완성된 아키텍처부터 소개해드리겠습니다. 생각보다 별거 없습니다.
슬랙 앱을 처음 보는 분들을 위해 잠깐 설명하겠습니다. 처음 접했을 때 앱이라는 단어가 혼란을 일으킬 수 있는데, 모든 비즈니스 로직은 별도 서버를 이용해 처리해야 합니다.
그럼 우리가 슬랙 API 대시보드에서 생성한 앱은 무엇일까요? 바로 슬랙 워크스페이스에서 상호 작용할 수 있는 인터페이스를 활성화해 줄 뿐입니다. 가령 ‘메시지 입력창에서 슬래시를 이용한 커맨드를 입력한다거나(/github)’, ‘멘션할 수 있는 봇이 생기거나(@github)’, ‘버튼이 있는 메시지가 전송되거나’, ‘폼(form)을 입력할 수 있는 모달이 열린다’거나 하는 것처럼 말이죠.
슬랙 앱은 이처럼 상호 작용이 가능한 컴포넌트에서 사용자가 어떤 행동을 취했을 때, 이에 해당하는 이벤트를 발생시킵니다. 그림에 적혀져 있는 Action, Event, Shortcut 등이 슬랙 이벤트의 한 종류입니다. 이 이벤트를 HTTP 요청 또는 웹 소켓 같은 형태로 수신해야 하는데, 이를 수신하기 위해서는 외부와 통신 가능한 서버가 있어야 합니다.
따라서 슬랙 앱을 동작시키기 위해서는 별도의 서버에서 이벤트에 맞는 적절한 콜백 함수를 실행시키는 방식으로 구현해야 합니다. 즉, 서버 역할을 할 수 있는 어떤 언어로도 슬랙 앱 개발이 가능합니다. 저 같은 경우는 빠르게 개발하는 것이 목표였기 때문에 익숙한 환경인 Node.js에 TypeScript를 끼얹은 구성으로 개발을 시작했습니다.
본격적으로 각 개발 과정을 상세히 알아보도록 합시다.
워크스페이스에 새 앱 생성
우선 슬랙 워크스페이스에 새 슬랙 앱을 생성해야 합니다. 슬랙 앱 대시보드에서 신규 앱을 생성할 수 있습니다. 앱 이름은 원하는 대로 설정하고, 설치할 슬랙 워크스페이스도 골라줍니다.
처음으로 앱을 생성하면 이런 식의 대시보드가 뜹니다. 현재는 Add features and functionality
아코디언 메뉴에 배치된 여섯 개의 항목 중 아무 기능도 활성화되어 있지 않습니다.
우선 Permission
기능에서 봇의 접근 권한을 설정해 봅시다.
권한 설정과 봇 활성화
일반적으로 ‘슬랙 앱’과 ‘슬랙 봇’이라는 단어를 혼용해서 쓰다 보니, 두 용어가 조금 헷갈릴 수 있습니다. 정확하게는 사실 앱이 봇을 포함하고 있는 관계입니다(봇 ⊂ 앱).
슬랙 앱은 말 그대로 슬랙 워크스페이스에서 사용할 수 있는 인터페이스를 활성화하고 각종 설정을 할 수 있는 메뉴를 제공하지만, 슬랙 봇은 슬랙의 인터페이스 중에서 마치 멤버처럼 취급되며 상호 작용할 수 있는 인터페이스의 한 종류를 말합니다.
즉, 일반적인 멤버처럼 채널과 대화를 통한 인터페이스를 사용하고자 한다면 이는 곧 봇을 사용해야 한다는 것을 의미합니다. 대나무숲 앱의 요구사항에는 어떤 멤버가 전송하고자 하는 메시지를 ‘대나무숲 봇’이라는 멤버의 계정으로 대신 전송하는 것이 포함되어 있기 때문에, 봇을 활성화하는 것이 필요합니다.
아래에 내려보면 Scope
가 있는데, 이곳에서 봇의 권한을 설정할 수 있습니다. 둘러보면 꽤 많은 권한을 설정할 수 있는데, 우선 꼭 필요한 기능만 넣어보았습니다.
그 후 Bots
메뉴에서 봇의 이름(Display name)과 유저네임(Username)을 설정합니다. 그러면 Permission
과 Bots
메뉴가 활성화된 상태가 됩니다.
Bolt.js 보일러 플레이트에서 개발 시작하기
본격적으로 앱 바로가기 기능을 넣기 전에, 이에 앞서 개발 환경 세팅을 먼저 진행해봅시다. 앱 바로가기 기능을 넣기 위해서는 이 과정이 필수적이기 때문입니다.
우선 개발 환경 구축에는 슬랙에서 자체 클라이언트 SDK를 쓰기 쉽게 래핑한 라이브러리 Bolt.js를 사용하려 합니다. 언어별로 라이브러리가 있고, 슬랙 앱 개발에 필요한 기본적인 도구들이 다 내장되어 있기 때문에 쉽게 개발 환경을 구축할 수 있습니다.
예제 코드를 살펴보면 TypeScript 보일러 플레이트도 제공되기 때문에, 해당 코드를 내려받은 후 실행하는 것을 추천합니다.
import { App } from '@slack/bolt';
const app = new App({
token: process.env.SLACK_BOT_TOKEN, // Basic Information -> App Credential 에서 확인 가능
signingSecret: process.env.SLACK_SIGNING_SECRET, // OAuth Permission -> Bot User OAuth Token에서 확인 가능
});
app.message('hello', async ({ message, say }) => { /* ... */ });
app.action('button_click', async ({ message, say }) => { /* ... */ });
app.event('reaction_added', async ({ event, client }) => { /* ... */ });
예제 코드를 살펴보면 앱에서 발생하는 상호 작용 종류에 따라 이벤트 리스너와 콜백 함수를 덧붙이는 방식이 Express와 크게 다르지 않다는 것을 알 수 있습니다. 이는 Bolt.js가 Express 기반으로 제작되었기 때문입니다.
또한 두 개의 토큰이 환경 변수로 설정된 것을 확인할 수 있습니다. 각 토큰을 얻는 방법은 주석으로 달아놓았고, 슬랙 앱 대시보드에서 확인할 수 있습니다. 복사한 뒤 .env 파일에 넣어주세요. 보일러 플레이트 코드에서는 개발 서버가 내장된 번들러가 따로 없기 때문에 직접 번들러를 설치해도 되고 아니면 그냥 터미널 두 개를 켜고 개발해도 됩니다.
# 터미널 1: 타입스크립트 컴파일 용도
$ yarn build:watch
# 터미널 2: 컴파일된 JS 실행 용도
$ yarn start
컴파일된 JavaScript 파일을 실행해서 서버를 켜는 건 좋은데, 이 로컬 네트워크에 외부 접근이 가능해야 개발 환경 구성이 완료됩니다. 이를 위해 우리는 ‘ngrok’이라는 프로그램을 이용하겠습니다.
ngrok을 이용한 로컬 네트워크 터널링
ngrok은 로컬 네트워크를 외부에서 접속할 수 있게 해주는 터널링 기능을 제공합니다.
우선 홈페이지에 들어가서 간단하게 회원 가입을 진행합니다. 그 후 MacOS 기준으로 실행 파일을 직접 다운로드하거나, brew를 이용해 설치할 수 있습니다. 실행 파일을 직접 다운로드 받았다면 /usr/local/bin
으로 이동시켜 터미널에서도 실행할 수 있는 상태로 만들어야 합니다.
그러면 터미널에서 ngrok http 포트번호
를 입력하는 방식으로 해당 포트의 로컬 네트워크를 외부에서 접속 가능하게 만들 수 있습니다. 즉, 보일러 플레이트 기반으로 개발하면 ngrok까지 총 3개의 터미널이 켜져 있는 상태여야 합니다. 보일러 플레이트 코드 기준으로 3000번 포트가 기본이기 때문에, 다음과 같이 명령어를 입력했습니다.
# 3000번 포트로 ngrok 실행
$ ngrok http 3000
그러면 로컬 네트워크 정보가 뜨게 되는데, 이 중에서 웹 인터페이스(Web Interface)와 포워딩(Forwarding) 주소를 기억해둡시다.
Web Interface http://127.0.0.1:4040
Forwarding https://xxxx-xxx-xxx-xxx-xx.jp.ngrok.io
앱 바로가기(Shortcut) 생성
다시 슬랙 앱 대시보드로 돌아와 Interactive Component
메뉴를 눌러줍니다. 이 메뉴에서 앱 바로가기를 활성화해야 하는데, Shortcuts API를 이용하면 됩니다.
우측 상단 스위치를 활성화해주면 아래에 Request URL
을 입력하는 인풋 박스가 뜹니다. 박스에 위에서 포워딩 된 주소에 /slack/events
를 덧붙인 주소를 입력합니다. 가령 이런 식으로요.
https://xxxx-xxx-xxx-xxx-xx.jp.ngrok.io/slack/events
슬랙 앱에서 발생하는 모든 이벤트는 위 URL로 HTTP 요청을 보내게 됩니다. 슬랙의 정책이기 때문에 괜히 저항하지 말고 따라줍시다.
이벤트와 관련된 세부 정보들이 페이로드에 실려 오는데, Bolt.js에서 적절하게 파싱해주기 때문에 크게 신경 쓸 필요가 없습니다. 우리는 이벤트 종류에 맞춰 콜백 함수만 잘 구현하면 됩니다. 이를 코드에 적용하는 방법은 차후에 설명하겠습니다.
다음으로는 두 가지 종류의 앱 바로가기에 대해 알아봅시다. Create New Shortcut 버튼을 누르면 아래의 사진이 뜹니다. 바로가기 종류마다 특징이 있는데, 사진을 살펴보면 쉽게 이해가 가실 겁니다.
따라서 우리는 전역 바로가기 API를 이용해 채널에 메시지를 보내고, 메시지 바로가기 API를 이용해 스레드를 다는 기능을 추가할 예정입니다.
만약 사용자가 바로가기를 클릭하거나 커맨드를 입력했을 때, 위에서 설정한 Request URL
로 HTTP 요청이 발생할 것입니다. 그 후 Bolt.js 서버에서 적절한 콜백 함수가 실행되어야 하는데, 이때 각 바로가기의 종류를 구별하기 위해 콜백 ID를 설정해야 합니다. 이름은 본인이 원하는 대로 아무렇게나 지어도 됩니다.
저는 전역 바로가기 메뉴의 콜백 ID를 bamboo_message
로 설정했습니다.
비슷하게, 메시지 바로가기는 bamboo_thread
로 설정했습니다.
최종적으로 설정한 바로가기 콜백 ID는 위와 같습니다. 우측 하단의 Save Changes
를 눌러 변경 사항을 저장합니다.
다시 앱 메인 화면으로 돌아와 Install to Workspace
버튼을 눌러 워크스페이스에 설치해봅시다.
앱 바로가기가 잘 적용된 모습입니다. 바로가기 버튼을 누르면 위에서 적용한 Request URL
로 HTTP 요청이 발생해야 합니다. 이를 확인하기 위해서 ngrok의 웹 인터페이스를 살펴봅시다.
ngrok 로그에서도 바로가기 버튼을 눌러 생긴 이벤트가 페이로드에 실려 온 것을 확인할 수 있습니다.
하지만 지금은 Bolt.js 서버에서 앱 바로가기를 눌렀을 때 어떤 식으로 처리해주어야 하는지 비즈니스 로직을 작성하지 않았기 때문에 아무런 동작을 하지 않습니다. 지금부터는 사용자가 바로가기 버튼을 누른 경우를 처리하기 위한 상호 작용 가능한 모달을 제작해 봅시다.
상호 작용 가능한 모달 호출하기
앱 바로가기에서 호출할 콜백 이름을 리스닝하기 위해서는 app.shortcut()
메소드를 이용합니다. 전역 바로가기든 메시지 바로가기든 같은 메소드를 쓰는데, 각 상황에 따라오는 페이로드 종류가 다르기 때문에 TypeScript 제네릭으로 이를 구별해줍시다.
// 전역 바로가기
app.shortcut("bamboo_message", openMessageModal);
// 메시지 바로가기
app.shortcut("bamboo_thread", ...);
우선 전역 바로가기의 콜백 함수를 살펴봅시다.
앱에서 bamboo_message
라는 ID의 바로가기 액션이 발생했을 때, openMessageModal
이라는 콜백 함수를 실행시킵니다. openMessageModal
함수는 대략 아래와 같이 생겼습니다.
const openMessageModal = async ({
shortcut,
ack,
client,
logger,
}: SlackShortcutMiddlewareArgs<GlobalShortcut> & AllMiddlewareArgs) => {
try {
await ack(); // ACK
const result = await client.views.open({
trigger_id: shortcut.trigger_id,
view: {
type: "modal",
callback_id: "bamboo_message", // app.view() 실행시킬 콜백 ID
blocks: [ /* 생략 */],
},
});
logger.info(result);
} catch (error) {
logger.error(error);
}
};
이상하게 제일 처음에 비동기 함수인 ack
를 호출합니다. 이게 무슨 의미일까요?
슬랙 앱의 정책 중에는 3초 ACK 룰이 있습니다. 슬랙의 일부 컴포넌트들, 가령 체크박스나 버튼 같은 경우는 상호 작용할 때마다 HTTP 요청이 오게 되는데, 이 요청이 올 때 3초 내로 200 OK
응답이 가능하게 코드를 구현해야 합니다. 다행히 Bolt.js에서는 ack
라는 빌트인 메소드를 제공하고 있기 때문에, 제일 처음에 이 함수를 먼저 호출해줍니다.
그 후 모달을 슬랙 클라이언트에서 열기 위한 client.views.open
메소드를 사용합니다. 이 내부에는 모달의 메타데이터와 UI에 대한 정보가 들어있습니다.
모달의 UI 같은 경우는 슬랙에서 제공되는 블록 키트 웹 애플리케이션을 이용하면 어렵지 않게 구성할 수 있습니다.
모달 값 검증하기
사용자가 모달에서 제출(Submit) 버튼을 눌렀을 때, 일부 값의 검증이 필요할 수 있습니다. 저 같은 경우는 체크박스를 일부러 두어서 혹시 실수로라도 메시지를 전송하는 일이 없도록 했습니다.
모달을 제출한 경우를 검증하기 위해서는 app.view()
메소드를 이용합니다. 이때, 콜백 ID로는 모달의 속성으로 지정한 views.callback_id
에 해당하는 이벤트 이름을 설정합니다.
app.view("bamboo_message", responseModal);
responseModal
콜백은 아래와 같이 구성했습니다.
const responseModal = async ({
ack,
view,
client,
logger,
}: SlackViewMiddlewareArgs<SlackViewAction> & AllMiddlewareArgs) => {
try {
// 중첩 객체에서 값 꺼내기
const values = view["state"]["values"];
const name = values["#name"]["#message"].value;
const message = values[`#content`][`#message`].value ?? "";
const checked =
(values[`#confirm`][`#checked`]["selected_options"]?.length ?? 0) > 0;
// 혹시라도 체크 안 되어 있다면 에러 띄우고 return
if (!checked) {
await ack({
response_action: "errors",
errors: {
[`#content`]: "아래 체크박스에 동의해주세요.",
},
});
return;
}
// 체크 되어있다면 문제없이 ACK
await ack();
// 슬랙 클라이언트의 chat.postMessage 메소드 이용하여 메시지 전송
await client.chat.postMessage({
text: message,
channel,
});
} catch (error) {
logger.error(error);
}
};
우선 views
를 통해 전달된 중첩 객체에서 각 프로퍼티를 꺼내는 모습을 확인할 수 있습니다. 이처럼 각 블록에 정확하게 접근하기 위해서는 모달의 각 블록마다 block_id
를 추가로 설정해야 합니다.
그 후 체크박스의 체크 여부를 확인합니다. 혹시라도 체크가 되어 있지 않다면 오류를 띄우고 추가 진행을 막아야 합니다. 이를 위해서는 ack
의 아규먼트로 에러 관련 객체를 전달하면, 오류가 발생했음을 알릴 수 있습니다. 에러 메시지의 출력은 #content
라는 ID의 블록에 출력하도록 세팅했습니다.
만약 체크박스에 체크가 되어있다면, 슬랙 클라이언트의 내장 메소드인 chat.postMessage()
를 이용해 봇 계정으로 메시지를 전송합니다. 아규먼트로는 보낼 메시지의 내용을 text
에, 어떤 채널에 보낼지를 channel
에 설정합니다.
혹시라도 메시지가 가지 않는다면 먼저 슬랙 봇을 해당 채널에 초대해주세요.
스레드 달기
스레드에 익명 댓글을 다는 로직도 사실 채팅과 크게 다른 내용이 없습니다. 비슷하긴 한데, 두 가지 추가 전달해야 하는 정보가 있습니다.
첫 번째는 원본 메시지입니다. 이것은 스레드를 달 때, ‘내가 어떤 메시지에 스레드 다는지’를 파악하면서 본문을 작성하기 위한 목적입니다. 다행히 app.shortcut()
메소드의 파라미터를 이용한다면, 어렵지 않게 값을 얻을 수 있습니다. 아래의 코드를 참조해주세요.
const openThreadModal = async ({ body }: (SlackShortcutMiddlewareArgs<MessageShortcut> & AllMiddlewareArgs)) => {
// 원본 메시지를 body 파라미터에서 획득할 수 있다.
const originalMessage = body.message?.text;
}
두 번째는 어떤 메시지에 스레드를 달지 결정해주기 위한 정보인 message_ts
입니다. 각 메시지를 식별하는 고유의 값으로, chat.postMessage
의 아규먼트로 전달하면 스레드가 달립니다. 해당 속성은 옵셔널한 속성이기 때문에, 생략한다면 채널에 일반 메시지가 전송됩니다.
await client.chat.postMessage({
text: message,
channel,
thread_ts, // 이 값이 있으면 해당 메시지에 스레드로 달리고, 없으면 채널에 메시지로 보내집니다.
});
하지만 문제가 있습니다. app.shortcut()
콜백에서 얻은 message_ts
값을 다음 콜백인 app.views()
로 넘겨주어야 스레드 달기가 가능하기 때문입니다. 하지만 app.views()
의 파라미터에 임의의 값을 전달할 수는 없습니다.
이를 해결하기 위해서는 슬랙 모달의 숨겨진 속성 중 하나인 private_metadata
라는 속성을 사용합니다. 이는 공식 문서에서 임의의 데이터 전달이 필요할 때 권장하는 방법입니다.
private_metadata
라는 속성에는 임의의 문자열을 사용자에게 보이지 않는 형태로 저장할 수 있는데, 여기에 message_ts
값을 저장하여 app.views()
의 아규먼트로 값을 전달할 수 있습니다.
이때도 단순히 message_ts
값을 집어넣는 것이 아닌, 혹시라도 나중에 전달할 정보가 추가될 수 있음을 감안하여 JSON.stringify
로 객체를 직렬화하여 전달하는 방식을 선택합니다.
// 메타데이터를 직렬화함
const private_metadata = JSON.stringify({
message_ts: body.message?.ts ?? "",
});
// 모달을 열 때 private_metadata 에 값을 숨김
await client.views.open({
trigger_id: body.trigger_id,
view: {
type: "modal",
private_metadata,
callback_id: "bamboo_thread",
blocks: [/* ... */]
}
})
app.views("bamboo_thread", ({ view } => {
// 모달이 제출될 때 메타데이터도 얻을 수 있음
const private_metadata = JSON.parse(JSON.parse(view.private_metadata));
}))
이렇게 하면 데모 동영상과 같은 방법의 스레드 달기가 가능해집니다. 이렇게 해서 필요한 기능 개발이 모두 완료되었습니다.
저는 별도의 워크스페이스에서 테스트용으로 앱을 먼저 개발했고, 완성된 앱을 글또 슬랙으로 이전하기로 했습니다. 다행히 앱과 관련된 모든 설정이 포함된 매니페스트 파일을 yaml
또는 json
확장자로 추출할 수 있기 때문에 이를 활용해줍니다.
이렇게 추출한 파일은 나중에 슬랙 워크스페이스에 새 앱을 생성할 때 매니페스트 파일 불러오는 모드로 활용할 수 있습니다.
배포는 헤로쿠(heroku)를 이용했습니다. 24시간 내내 개인 노트북에 로컬 네트워크 터널을 열어놓을 수는 없는 노릇이고, 무엇보다 무료이기도 해서요. 헤로쿠의 Node.js 배포 튜토리얼을 참고했고, 서버 실행을 위한 Procfile
을 추가하는 정도로 마무리했습니다.
이렇게 해서 간단하게 대나무숲 봇을 만들어봤습니다. 다행히 정해진 시간 내에 개발을 완료할 수 있었고, 덕분에 오리엔테이션에서도 동작하는 앱을 시연할 수 있었습니다. 감사하게도 많은 분께서 호응해주셨는데, 쏟아지는 실시간 채팅에 정신을 못 차렸습니다.
그렇게 배포 후 일주일 정도 사용자의 반응을 지켜봤는데, 사용자가 스레드에 댓글을 제대로 달지 못하는 문제가 꽤 많이 발생했습니다. 스레드를 달 때는 추가 작업 창에 있는 메시지 바로가기 메뉴를 눌러야 하는데, 스레드 창에 있는 전역 바로가기 메뉴를 사용한 것으로 보입니다. 사용자 입장에서 조금 직관적이지 않은 모양인가 봅니다.
전역 바로가기 메뉴는 어떤 입력창에서든 호출 가능하다는 특징이 있기 때문에, 호출된 위치에 따라 분기 처리를 하는 것이 어렵습니다. 그래서 리팩토링을 통해 명시적으로 상호 작용 가능한 버튼이 있는 메시지를 보내는 것으로 기능을 수정했습니다.
완성된 버전은 위와 같습니다.
슬랙 API와 bolt.js를 사용한 경험은, 사실 생각보다 좋지만은 않았습니다. 기능이 찾아보면 이것저것 많이 있긴 한데, 문서가 많고 좀 많이 복잡했습니다. 권한 처리를 제대로 하지 않아서 이벤트 구독이 되지 않거나 웹 대시보드에서 저장 버튼 안 눌러서 설정이 안 되는 이런 자잘한 삽질 경험이 좀 많았습니다. 슬랙 API의 정책을 전혀 모르는 상태에서 개발을 시작하는 분이라면, 좀 많이 헤맬 것 같습니다.
그런데 이건 제가 슬랙 API에 익숙하지 않아서 그렇지 기술 자체의 난이도가 그렇게 어렵거나 복잡한 것은 아니기 때문에, 충분한 시간이 있다면 문서를 참조해서 누구나 만들 수 있는 수준의 앱이라고 생각합니다.
이 글에서 다루고자 하는 핵심은 얼마나 복잡한 기술을 썼는지가 아닙니다. 커뮤니티가 마주한 문제점을 파악하고, 최소한의 기능만을 목표로 빠르게 프로토타입을 제작했으며, 배포 이후 사용자를 관찰하여 부족한 부분을 빠르게 개선한 점에서 가치가 있다고 생각합니다.