배경
🎋
개발 •  • 읽는데 19분 소요

임금님 귀는 당나귀 귀! 대나무숲 슬랙 앱 만들기

익명으로 메시지 전송이 가능한 슬랙 앱을 개발하여 커뮤니티 운영에 기여한 경험을 공유합니다.

#JavaScript
#TypeScript


본 프로젝트의 전체 코드는 Github에서 확인할 수 있습니다.

오랜만에 재미있는 사이드 프로젝트를 하나 했습니다. 프로젝트 이름은 대나무숲(Bamboo Forest) 이라고 지었는데, 어떤 주제인지 대충 감이 오시나요?

대나무숲 앱의 프로토타입

네, 짐작하신 게 맞습니다! 대나무숲 앱에서는 기본적으로 슬랙 채널에 익명으로 메시지와 스레드를 전송할 수 있는 기능을 제공합니다. 이를 이용해 슬랙 워크스페이스의 멤버들이 익명에 기대어 솔직한 이야기를 할 수 있는 채널을 만들고자 했습니다.

사실 데모를 보면 알 수 있듯이, 아이디어 제안부터 배포까지 3일이 채 걸리지 않은 간단한 프로젝트입니다. 그럼에도 불구하고 이 프로젝트의 개발 후기를 공유하는 이유는, 짧은 시간 내에 약 200명 규모의 커뮤니티가 마주한 문제를 효율적으로 해결했기 때문입니다.

기존에 어떤 문제가 있었는지를 진단하고, 이를 기술적 관점에서 어떻게 해결했으며, 사용자의 피드백을 다시 제품에 적용하는 그 과정이 실제 서비스를 개발하는 것과 크게 다르지 않다고 여겨졌습니다.

그래서 오늘은 대나무숲 슬랙 앱의 개발 과정 및 후기를 공유합니다. 이번 포스트를 통해 슬랙 앱 개발에 관심이 있으신 분 들께 도움이 되었으면 좋겠습니다.

만들게 된 동기

익명 실제 Anonymous Bot을 이용해 고민 상담 채널에 올라왔던 사연들

제가 활동하고 있는 글 쓰는 개발자 커뮤니티 글또는 주 활동을 슬랙에서 합니다. 글또는 기수제로 운영되기 때문에 폐쇄성을 지닌 커뮤니티인데요, 하지만 커뮤니티의 심리적 안정감이 아무리 높아도 고민을 실명으로 말하기는 어려울 수 있다는 생각으로 인해 약 2년 전부터는 익명으로 고민 상담을 할 수 있는 채널이 생겼습니다. 당시에는 간편하게 적용 가능한 Anonymous Bot이라는 서드파티 슬랙 앱을 이용했습니다.

이 덕분에 개발과 관련된 진지하고 민감한 고민들이 많이 올라왔습니다. 커리어 관련 고민, 이직과 연봉 협상, 취업과 면접, 번아웃 등등… 익명에 기대어 이야기 할 수 있는 채널이 마련되었기 때문에 많은 사람들이 부담없이 솔직하게 자신의 고민을 남겨주셨습니다.

스레드 역시 익명으로 달 수 있었기에 응원과 토론이 가능했고, 이로 인해 고민 상담 채널은 글또 슬랙 내에서도 많은 분들이 좋아해주는 채널이 되었습니다.

익명 이게 뭐라고 한 달에 15만원을 태워…?

하지만 커뮤니티의 규모가 점차 커지면서 문제가 발생했습니다. 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가 있는데, 이 곳에서 봇의 권한을 설정할 수 있습니다. 둘러보면 꽤 많은 권한을 설정할 수 있는데, 우선 꼭 필요한 기능만 넣어보았습니다.

  • commands: 앱 바로가기를 클릭하거나 커맨드를 통해 대나무숲 메시지를 보내기 위해
  • chat:write: 봇이 직접 메시지를 전송하기 위해

사진

그 후 Bots 메뉴에서 봇의 이름(Display name)과 유저네임(Username)을 설정합니다. 그러면 PermissionBots 메뉴가 활성화된 상태가 됩니다.

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 버튼을 누르면 아래의 사진이 뜹니다. 바로가기 종류마다 특징이 있는데, 사진을 살펴보면 쉽게 이해가 가실 겁니다.

사진

  • 전역 바로가기(Global Shortcut) API: 전역이라는 이름에서 볼 수 있듯이 특정 채널이나 메시지에 종속되지 않고 호출할 수 있는 바로가기, 슬랙 메시지 입력창 뿐만 아니라 검색창에서도 사용할 수 있음
  • 메시지 바로가기(Message Shortcut) API: 특정 메시지에 어떤 액션을 취할 수 있게 만드는 것

따라서 우리는 전역 바로가기 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<GlobalShortcut>("bamboo_message", openMessageModal);

// 메시지 바로가기
app.shortcut<MessageShortcut>("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<ViewSubmitAction>('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 에 설정합니다.

사진 채널 ID은 사이드바에서 우클릭하여 나오는 채널 세부 정보에서 확인 가능

혹시라도 메시지가 가지 않는다면 먼저 슬랙 봇을 해당 채널에 초대해주세요.

스레드 달기

사진

스레드에 익명 댓글을 다는 로직도 사실 채팅과 크게 다른 내용이 없습니다. 비슷하긴 한데, 두 가지 추가 전달해야 하는 정보가 있습니다.

첫 번째는 원본 메시지입니다. 이것은 스레드를 달 때, 내가 어떤 메시지에 스레드 다는지를 파악하면서 본문을 작성하기 위한 목적입니다. 다행히 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시간 내내 개인 노트북에 로컬 네트워크 터널을 열어놓을 수는 없는 노릇이라… 무엇보다 무료이기도 해서요. Heroku의 Node.js 배포 튜토리얼을 참고했고, 서버 실행을 위한 Procfile을 추가하는 정도로 마무리했습니다.

개발 끝?

사진

이렇게 해서 간단하게 대나무숲 봇을 만들어봤습니다. 다행히 정해진 시간 내에 개발을 완료할 수 있었고, 덕분에 오리엔테이션에서도 동작하는 앱을 시연할 수 있었습니다. 감사하게도 많은 분께서 호응해주셨는데… 쏟아지는 실시간 채팅에 정신을 못 차리겠더군요. 😂

사진 이렇게 스레드 창에서 /bamboo 를 입력하면 전역 바로가기가 호출돼서 스레드를 달 수 없다.

그렇게 배포 후 일주일 정도 사용자의 반응을 지켜봤는데, 사용자가 스레드에 댓글을 제대로 달지 못하는 문제가 꽤 많이 발생했습니다. 스레드를 달 때는 추가 작업 창에 있는 메시지 바로가기 메뉴를 눌러야 하는데, 스레드 창에 있는 전역 바로가기 메뉴를 사용한 것으로 보여집니다. 사용자 입장에서 조금 직관적이지 않은 모양인가 봅니다.

사진 그래서 명시적으로 스레드를 달 수 있게 버튼을 추가했다.

전역 바로가기 메뉴는 어떤 입력창에서든 호출 가능하다는 특징이 있기 때문에, 호출된 위치에 따라 분기 처리를 하는 것이 어렵습니다. 그래서 리팩토링을 통해 명시적으로 상호 작용 가능한 버튼이 있는 메시지를 보내는 것으로 기능을 수정했습니다.

데모 익명 메시지와 스레드 전송 데모 최종 버전

완성된 버전은 위와 같습니다.

후기

로고 겸사겸사 로고도 하나 만들어봤다

이렇게 만든 대나무숲 앱은 현재 글또 커뮤니티 내에서 문제 없이 잘 사용 중입니다. 배포한지 일주일 정도 됐는데 100여개 정도의 익명 메시지가 올라온 것 같네요. 다행히 커뮤니티 내에서만 트래픽이 발생하기 때문에, 아직까지 트래픽 감당을 못한다거나 그런 일은 없었습니다.

슬랙 API와 bolt.js를 사용한 경험은… 사실 생각보다 좋지만은 않았습니다. 기능이 찾아보면 이것저것 많이 있긴 한데, 문서가 더럽게 많고(…) 좀 많이 복잡했습니다. 권한 처리를 제대로 하지 않아서 이벤트 구독이 안된다거나, 웹 대시보드에서 저장 버튼 안 눌러서 설정이 안됐다거나 이런 자잘한 삽질 경험이 좀 많았습니다. 슬랙 API의 정책을 전혀 모르는 상태에서 개발을 시작하는 분이라면… 좀 많이 헤멜 것 같습니다.

그런데 이건 제가 슬랙 API에 익숙하지 않아서 그런거지 기술 자체의 난이도가 그렇게 어렵거나 복잡한 것은 아니기 때문에, 충분한 시간이 있다면 문서를 참조해서 누구나 만들 수 있는 수준의 앱이라고 생각합니다.

이 글에서 다루고자 하는 핵심은 얼마나 복잡한 기술을 썼는지가 아닙니다. 커뮤니티가 마주한 문제점을 파악하고, 최소한의 기능만을 목표로 빠르게 프로토타입을 제작했으며, 배포 이후 사용자를 관찰하여 부족한 부분을 빠르게 개선한 점에서 가치가 있다고 생각합니다.

사실 본문에서 언급한 내용 외에도 자잘한 부가 기능들이 이것저것 많은데, 이걸 모두 적기에는 어려워서 생략한 부분이 있습니다. 보다 정확한 코드가 궁금하시다면, Github을 참조해주시기 바랍니다. 그리고 겸사겸사 스타(⭐)도 눌러주시면…? 더 할 나위가 없겠네요.

이 포스트가 유익하셨다면?




프로필 사진

👨‍💻 정종윤

글 쓰는 것을 좋아하는 프론트엔드 개발자입니다. 온라인에서는 재그지그라는 닉네임으로 활동하고 있습니다.


Copyright © 2024, All right reserved.

Built with Gatsby