Discord 게이트웨이로 실시간 이벤트 받기

봇 개발을 하다 보면 결국 마주치는 통로가 게이트웨이(Gateway)입니다. 슬래시 커맨드를 만들 때 client.login() 한 줄로 봇을 온라인으로 띄웠는데, 그 순간 봇은 바로 이 게이트웨이로 디스코드와 연결을 맺은 거였어요. 이번 글에서는 그 통로의 안쪽을 들여다보며, 봇이 어떻게 서버의 모든 일을 실시간으로 지켜보는지, 그리고 그렇게 만든 봇을 어떻게 24시간 운영하는지까지 살펴봅니다.
게이트웨이는 항상 열려 있는 WebSocket
REST API가 “내가 물어보면 답해주는” 요청-응답 방식이었다면, 게이트웨이는 정반대입니다. 봇이 디스코드와 WebSocket 연결을 항상 열어두고, 서버에서 메시지가 올라오거나 멤버가 입장하는 순간 디스코드가 먼저 알려주는 구조예요. 채널의 모든 사건을 실시간으로 받아보려면 이 방식이어야 합니다.
연결 수명주기
client.login() 뒤에서는 사실 꽤 정교한 절차가 돌아갑니다. 다행히 discord.js가 전부 자동으로 처리해 주지만, 무슨 일이 벌어지는지 알아두면 연결 문제를 디버깅할 때 큰 도움이 돼요.
- 봇이 게이트웨이에 접속하면 디스코드가 Hello를 보내며 하트비트 간격(
heartbeat_interval)을 알려줍니다. - 봇은 그 간격마다 하트비트(Heartbeat)를 보내 “나 살아 있어요”라고 신호하고, 디스코드는 ACK로 답합니다. 이 신호가 끊기면 연결이 “좀비” 상태로 간주돼 재연결돼요.
- 봇은 Identify로 토큰과 인텐트를 보내 정식 인사를 합니다.
- 디스코드가 Ready 이벤트로 연결 성공을 확인해 주는데, 이때
session_id와 재연결용 주소를 함께 줍니다. - 이후로는 서버에서 일어나는 일들이 Dispatch 이벤트로 계속 흘러들어옵니다.
여기서 핵심은 하트비트를 제때 보내지 않으면 연결이 끊긴다는 거예요. discord.js가 알아서 보내주지만, 봇이 무거운 동기 작업으로 이벤트 루프를 오래 막으면 하트비트가 밀려 끊길 수 있습니다.
인텐트: 무엇을 받을지 신청하기
위 Identify 단계에서 보낸 인텐트(intents)가 게이트웨이의 핵심 개념입니다. 디스코드는 수많은 이벤트가 오가는 곳이라, 봇은 필요한 종류만 골라 구독해요. 인텐트는 비트 플래그라 여러 개를 OR로 합쳐 보냅니다.
discord.js 14 기준으로 지원하는 인텐트를 특권 여부와 함께 정리하면 다음과 같습니다. 대부분의 봇은 이 중 한두 개만 켜면 됩니다.
인텐트 (GatewayIntentBits.) | 받는 주요 이벤트 | 특권 |
|---|---|---|
Guilds | 서버, 채널, 스레드 생성과 수정 | |
GuildMembers | 멤버 입장, 퇴장, 정보 변경 | ✅ |
GuildModeration | 차단(ban)과 감사 로그 | |
GuildExpressions | 이모지, 스티커, 사운드보드 변경 | |
GuildIntegrations | 서버 연동(integration) 변경 | |
GuildWebhooks | 웹훅 생성과 삭제 | |
GuildInvites | 초대 생성과 삭제 | |
GuildVoiceStates | 음성 채널 입장과 퇴장 상태 | |
GuildPresences | 멤버 접속 상태와 활동 | ✅ |
GuildMessages | 서버 채널의 메시지 | |
GuildMessageReactions | 서버 메시지의 리액션 | |
GuildMessageTyping | 서버에서 입력 중 표시 | |
DirectMessages | DM 메시지 | |
DirectMessageReactions | DM 리액션 | |
DirectMessageTyping | DM에서 입력 중 표시 | |
MessageContent | 메시지 본문 내용 | ✅ |
GuildScheduledEvents | 예약된 이벤트 | |
AutoModerationConfiguration | 자동 조절 규칙 설정 | |
AutoModerationExecution | 자동 조절 동작 실행 | |
GuildMessagePolls | 서버 메시지의 투표 | |
DirectMessagePolls | DM의 투표 |
✅로 표시한 GuildMembers, GuildPresences, MessageContent 세 가지가 특권 인텐트입니다. 이 셋은 Developer Portal에서 토글을 켜야 동작하고, 봇이 10,000 사용자를 넘어가면 Discord의 심사를 통과해야 활성화돼요. 나머지는 코드에서 배열에 추가하기만 하면 바로 쓸 수 있습니다.
한 가지 짚고 넘어갈 게 있어요. 이 포털 토글을 게이트웨이 스위치라고만 생각하기 쉬운데, 사실은 REST API 접근까지 함께 잠그는 스위치입니다. 예를 들어 서버의 전체 멤버 목록을 가져오는 GET /guilds/{id}/members는 GuildMembers 토글이 켜져 있어야 응답을 줘요. 그리고 이 REST 제한은 위에서 코드로 지정한 IDENTIFY 인텐트와는 무관하게 걸립니다. 결국 ‘인텐트’라는 한 이름 아래 (1) 게이트웨이로 받을 이벤트를 거르는 필터와 (2) 민감한 REST 엔드포인트를 여닫는 잠금이라는, 서로 다른 두 메커니즘이 묶여 있는 셈이에요. (2)번 이야기는 REST API를 직접 호출하는 글에서 이어집니다.
이벤트 처리하기
이제 실제로 이벤트를 받아볼게요. 채널에 올라온 메시지에 반응하려면 GuildMessages와 특권 인텐트인 MessageContent가 필요합니다.
import { Client, Events, GatewayIntentBits } from "discord.js";
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent, // 특권 인텐트: 포털에서 켜야 함
],
});
client.on(Events.MessageCreate, (message) => {
if (message.author.bot) return; // 봇 자신의 메시지는 무시
if (message.content === "안녕") {
message.reply("안녕하세요! 👋");
}
});
client.login(process.env.DISCORD_TOKEN);
message.author.bot 체크는 빼먹기 쉬운데, 안 넣으면 봇이 자기 답장에 또 반응하는 무한 루프에 빠질 수 있으니 습관처럼 넣어두세요.
끊겨도 이어가기: Resume
WebSocket은 네트워크 사정으로 언제든 끊길 수 있습니다. 그럴 때마다 처음부터 다시 Identify하면 그동안 놓친 이벤트가 사라지겠죠. 그래서 게이트웨이는 Resume를 지원합니다. 끊기기 전에 받아둔 session_id와 마지막 시퀀스 번호를 보내면, 디스코드가 끊긴 사이의 이벤트를 다시 흘려보내 줘요. discord.js가 이 재연결과 Resume를 자동으로 처리하니, 우리는 봇 로직에만 집중하면 됩니다.
샤딩: 큰 봇을 위한 분할
봇이 점점 인기를 얻어 2,500개 서버를 넘어가면, 디스코드는 연결을 여러 개로 쪼개는 샤딩(sharding)을 의무화합니다. 각 샤드가 최대 2,500개 서버를 맡고, 서버는 ID를 기준으로 샤드에 분배돼요. discord.js는 ShardingManager로 이걸 도와주지만, 대부분의 봇은 이 규모에 닿기 한참 전이니 “이런 게 있다” 정도만 알아두면 충분합니다.
게이트웨이 봇을 24시간 띄우기
게이트웨이 봇의 숙명은 프로세스가 계속 살아 있어야 한다는 점이에요. 연결이 끊기면 봇이 오프라인이 되니, 내 노트북이 아니라 늘 켜져 있는 곳에 올려야 합니다. 요즘은 직접 서버를 관리하기보다 컨테이너 이미지로 만들어 클라우드에 맡기는 흐름이 일반적이에요. 아래 예시는 Bun 이미지를 베이스로 씁니다.
FROM oven/bun:1
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production
COPY . .
# 디스코드 봇은 HTTP 서버가 아니라 게이트웨이에 붙는 백그라운드 워커라
# 따로 포트를 열지 않습니다
CMD ["bun", "run", "index.js"]
이 이미지를 Railway, Render, Fly.io, Cloudflare Containers 같은 곳의 백그라운드 워커(Background Worker) 타입으로 올리면, 깃 푸시만으로 빌드되고 죽으면 자동으로 다시 살아납니다. 봇 토큰 같은 비밀 값은 .env를 커밋하는 대신 플랫폼 대시보드의 Secrets에 넣고요. 많은 입문자가 봇을 “웹 서비스” 타입으로 올렸다가 “포트를 안 연다”며 헬스체크에 실패하는데, 게이트웨이 봇은 포트를 열지 않는 워커라는 점만 기억하면 됩니다.
마치며
게이트웨이의 안쪽까지 깊이 들여다봤습니다. WebSocket 연결의 수명주기, 인텐트로 이벤트를 구독하는 법, 끊겼을 때의 Resume, 샤딩, 그리고 봇을 24시간 운영하는 배포까지요.
지금까지를 돌아보면, 앱과 봇의 개념에서 출발해 REST API로 디스코드에 명령을 보내고, 인터랙션과 컴포넌트로 사용자와 대화하고, 권한으로 안전하게 통제하고, OAuth2로 사용자를 인증하고, 마지막으로 게이트웨이로 실시간 이벤트를 받는 데까지 왔습니다. “REST로 시키고, 게이트웨이나 HTTP 인터랙션으로 전달받는다”는 큰 그림이 이제 전부 손에 익었을 거예요.
코드를 직접 짜는 대신 기성 봇을 AI에게 맡기는 방법이 궁금하다면 Discord MCP 서버 활용법도 함께 보시면 좋습니다. 더 자세한 내용은 Discord 게이트웨이 문서를 참고하세요.
This work is licensed under CC BY 4.0