Sign In
블로그 ✍️

슬랙 연동 과정에서 생긴 이슈

Kidow
안녕하세요, 일간 ProductHunt입니다.
프로젝트를 새단장하는 과정에서 슬랙 연동 방식을 새롭게 바꾸었는데요,
테스트 때 잘되서 괜찮은 줄 알았는데, 알고 보니 제가 오해했던 부분이 있어서 일부 유저들 분들이 제대로 작동하지 않았고 해결하느라 시간을 너무 잡아먹어버리고 말았네요.
이 글에서는 어떤 이슈가 있었고 어떻게 해결했는지 다뤄보고자 합니다.

원래 의도했던 것

슬랙으로 개발자 컨디션 관리하는 앱, hay
원래 의도했던 방식은 위처럼 앱의 메시지 탭을 통해 콘텐츠를 전달하는 방식이었습니다. 이 방식을 통하면 유저에게 채널을 선택할 부담을 주지 않기 때문에 더 간편하고 직관적이라고 생각했거든요. 무언가 잘못 됐다는 걸 알기 까지는요...

Incoming Webhook

슬랙 API는 Incoming Webhook이라는 기능이 있습니다. 간단하게 말하면 Webhook URL을 생성하는 것인데요,
Incoming Webhook 활성화
이 기능은 기본적으로 비활성화되어 있습니다. 이 기능을 활성화하면 기존처럼 유저가 연동 시 특정 채널을 선택해야 하고, 활성화하지 않으면 채널을 선택하지 않고 연동하게 됩니다.
활성화하지 않은 상태의 연동 모습. 채널 선택이 없다
왜 기존에 하던 Incoming Webhook 방식을 버리고 위와 같은 방식을 따라하려고 했느냐, 크게 별다른 이유는 없었습니다. 이런 방식으로 하는 앱도 있는 것을 보고 나니 이 방식이 더 편해 보였거든요. 이 프로젝트로 테스트해보고 싶은 것도 있긴 했습니다. 좀 더 알아보고 했으면 좋았을 텐데 아쉽긴 합니다.
그렇다면 이제 무슨 실수를 했는지 좀 얘기를 해보려고 합니다. 여러분들은 슬랙 연동할 때 이런 실수를 하지 않기를 바랍니다.

슬랙 URL 규칙

슬랙의 URL은 일종의 규칙이 있습니다. URL의 파라미터 앞글자를 따서 각각의 id를 URL에 표현하는 방식이죠.
슬랙 워크스페이스의 팀 id, 채널 id, dm id 표현 방식
위의 hay에서도 볼 수 있듯이 슬랙의 워크스페이스에는 채널, 다이렉트 메시지, 그리고 앱이 있습니다. 채널 페이지에는 Channel ID가, 다이렉트 메시지 페이지에는 DM ID가 할당이 되죠.
그럼 여기서 앱은 무엇에 할당이 되느냐, 바로 DM ID에 할당이 됩니다.

Bot Token, User Token

User Token과 Bot Token
채널이 그냥 채널도 있고 DM 채널도 있기 때문에 API를 활용할 때도 2종류의 토큰을 사용합니다. Bot Token은 xoxb- 형식으로 되어 있으며, 대부분의 경우에서 사용합니다. 유저가 채널로 연동한 경우 Bot Token을 통해 API를 활용합니다.
User Token은 xoxp- 형식으로 되어 있으며, 유저가 DM 채널로 연동한 경우 User Token으로 API를 활용합니다.
Bot Token은 말 그대로 봇의 토큰이기 때문에 하나의 워크스페이스에 하나의 봇으로 멤버들에게 한 번에 메시지가 갈 수 있는 토큰이지만, User Token은 유저의 토큰이기 때문에 메시지를 보낼 때 한 명의 멤버에게만 메시지가 갑니다. 따라서 워크스페이스의 모든 멤버에게 User Token으로 메시지를 보낼 때는 모든 유저의 User Token을 알아야 합니다.
앞서 말했듯 앱 메시지 탭은 일반 채널이 아닌 DM 채널이기 때문에, Bot Token으로 보내는 게 아니라 User Token으로 보내야 했는데 여기서 제가 실수를 해버려서 누구는 메시지를 받고 누구는 메시지를 못 받는 일이 일어났던 겁니다.

해결 과정

💡
여기서부터는 자바스크립트의 이해가 조금 필요합니다.
(Next.js App router + Typescript)
Slack API를 자바스크립트로 연동하는 경우, @slack/web-api 라는 라이브러리를 주로 사용하는데요,
처음에는 Slack APP을 만들 때 제공되는 Bot Token, Client Id, Client Secret Key를 사용하여 유저의 OAuth 정보를 가져오게 됩니다.
// /app/api/slack/route.ts import { NextResponse } from 'next/server' import { WebClient } from '@slack/web-api' export async function GET(req: Request) { const web = new WebClient('[Bot Token]') const url = new Url(req.url) const code = url.searchParams.get('code') as string const result = await web.oauth.v2.access({ client_id: '[Client Id]', client_secret: '[Client Secret]', code: code, redirect_uri: 'https://daily-producthunt.kidow.me/api/redirect/slack' }) return NextResponse.json(result) }
여기서 result 값은 다음과 같습니다.
{ "ok": true, "app_id": "[App Id]", "authed_user": { "id": "UKLFE42HE" }, "scope": "channels:read,chat:write,groups:read,im:read,mpim:read", "token_type": "bot", "access_token": "[Bot Token]", "bot_user_id": "[Bot User ID]", "team": { "id": "[Team ID]", "name": "[Team Name]" }, "enterprise": null, "is_enterprise_install": false, "response_metadata": { "scopes": [ "channels:read", "chat:write", "groups:read", "im:read", "mpim:read" ] } }
보시면 access_tokenauther_user.id 를 보실 수 있을텐데요,
이 두 개의 값으로 추가로 conversation.list 메소드를 통해 유저가 속한 워크스페이스의 채널 목록을 조회합니다.
const bot = new WebClient(result.access_token) const { channels } = await bot.conversations.list({ types: 'im' }) const channelId = channels.find(channel => chanenl.user === result?.authed_user?.id)
슬랙의 채널에는 4가지 유형이 있습니다. public_channel, private_channel, mpim, im이 있는데요,
public_channel: 가장 보편적인 채널. 워크스페이스 내 멤버 누구나 액세스 가능
private_channel: 비공개 채널. 특정 멤버들만 초대되어 대화 가능
im: 개인간 DM 채널. 일대일로 대화 가능
mpim: 그룹 DM 채널. 여러명이 대화 가능
여기서 앱 메시지 탭으로 대화하는 채널은 DM 채널이기 때문에 im, mpim 둘 중 하나에 속해야 하는데, 제가 테스트할 때는 im으로 나와서 im 채널을 찾은 뒤, 해당 채널마다 가지고 있는 user 정보를 authed_user.id와 대응해서 맞는 채널의 id를 저장하는 방식이 제가 시도했던 방식이었습니다.
"channels": [ { "id": "D05EZ35K2Q3", "created": 1688099992, "is_archived": false, "is_im": true, "is_org_shared": false, "context_team_id": "[Team ID]", "updated": 1688099992116, "user": "USLACKBOT", "is_user_deleted": false, "priority": 0 }, { "id": "D05EW8RUFRB", "created": 1688099992, "is_archived": false, "is_im": true, "is_org_shared": false, "context_team_id": "[Team ID]", "updated": 1688099992301, "user": "U048RA3PYRY", "is_user_deleted": false, "priority": 0 }, { "id": "D05ESGRP77G", "created": 1688099992, "is_archived": false, "is_im": true, "is_org_shared": false, "context_team_id": "[Team ID]", "updated": 1688099992201, "user": "UKLFE42HE", "is_user_deleted": false, "priority": 0 } ]
conversations.list 를 통해 im 채널 목록을 가져오면 위와 같은 형식으로 데이터가 오게 됩니다.
잘 보시면, 위와 OAuth 정보에서 authed_user.id 값이 UKLFE42HE인데, 채널 목록에서도 마지막 요소의 user 값이 UKLFE42HE로 동일합니다. 앱을 설치한 유저의 id가 일치하는 채널이 1개만 있다보니 그 채널의 id인 D05ESGRP77G를 저장해서 메시지를 보내고 있었던 거였습니다. 네, 이게 제가 오해했던 부분이었습니다. 이렇게 하면 아마 워크스페이스 내에서 앱을 설치한 유저 한 명만 메시지를 받게 될 겁니다.

마무리

지금은 incoming webhook 기능을 활성화해서 디스코드처럼 웹훅 url로 메시지를 전달하고 있습니다. 사이드 프로젝트에서 실수한 걸 다행으로 여기고, 반면교사 삼아 더 견고한 서비스와 좋은 콘텐츠로 찾아뵙도록 해보겠습니다. 감사합니다.
Subscribe to 'daily-producthunt'
Subscribe to my site to be the first to receive notifications and emails about the latest updates, including new posts.
Join Slashpage and subscribe to 'daily-producthunt'!
Subscribe
👍