본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
AI Generated
2026. 1. 31. · 11 Views
Telegram Bot grammY 완벽 가이드
Telegram Bot API의 강력한 기능을 grammY 프레임워크로 쉽게 구현하는 방법을 배웁니다. 명령어 핸들러부터 인라인 키보드, 콜백 처리까지 실전 예제로 익히는 Telegram 봇 개발 완벽 가이드입니다.
목차
1. Telegram Bot API의 강력함
어느 날 김개발 씨는 회사에서 새로운 과제를 받았습니다. "고객 지원팀을 위한 자동 응답 봇을 만들어주세요." 처음에는 막막했지만, Telegram Bot API를 알게 되면서 생각보다 쉽게 해결할 수 있다는 것을 알게 되었습니다.
Telegram Bot API는 텔레그램 메신저에서 봇을 만들 수 있게 해주는 강력한 도구입니다. 마치 자동 응답 비서를 고용하는 것과 같습니다.
사용자가 메시지를 보내면 봇이 즉시 반응하고, 버튼을 클릭하면 원하는 작업을 수행합니다. 이를 제대로 이해하면 고객 지원, 알림 서비스, 자동화 업무 등 다양한 분야에 활용할 수 있습니다.
다음 코드를 살펴봅시다.
// BotFather로 봇 생성 후 토큰 발급
import { Bot } from "grammy";
// 봇 인스턴스 생성 - 토큰으로 인증
const bot = new Bot(process.env.BOT_TOKEN);
// 메시지 핸들러 등록
bot.on("message:text", (ctx) => {
console.log("받은 메시지:", ctx.message.text);
return ctx.reply("안녕하세요! 무엇을 도와드릴까요?");
});
// 봇 시작
bot.start();
김개발 씨는 입사 6개월 차 개발자입니다. 어느 날 팀장님이 다가와 말했습니다.
"김 개발자님, 고객 지원팀에서 자주 묻는 질문이 너무 많다고 합니다. 자동으로 답변해주는 봇을 만들 수 있을까요?" 김개발 씨는 고민에 빠졌습니다.
웹 개발은 해봤지만 메신저 봇은 처음이었기 때문입니다. 그때 선배 박시니어 씨가 다가왔습니다.
"Telegram Bot API를 사용해보세요. 생각보다 간단합니다." 그렇다면 Telegram Bot API란 정확히 무엇일까요?
쉽게 비유하자면, Telegram Bot API는 마치 우체국에 자동 분류 시스템을 설치하는 것과 같습니다. 편지가 들어오면 자동으로 분류해서 적절한 답장을 보내주는 시스템 말입니다.
텔레그램도 마찬가지입니다. 사용자가 메시지를 보내면 봇이 자동으로 읽고 적절한 응답을 보냅니다.
Telegram Bot API가 없던 시절에는 어땠을까요? 개발자들은 메신저 서비스를 만들려면 처음부터 모든 것을 구축해야 했습니다.
사용자 인증, 메시지 전송, 알림 시스템 등 모든 것을 직접 만들어야 했습니다. 서버 인프라 구축도 필요했고, 보안도 신경 써야 했습니다.
더 큰 문제는 사용자를 모으는 것이었습니다. 아무리 좋은 기능을 만들어도 사용자가 없으면 무용지물이었습니다.
바로 이런 문제를 해결하기 위해 Telegram Bot API가 등장했습니다. Telegram Bot API를 사용하면 이미 수억 명이 사용하는 텔레그램 플랫폼 위에서 바로 서비스를 시작할 수 있습니다.
인증 시스템도 이미 구축되어 있고, 메시지 전송 인프라도 모두 준비되어 있습니다. 개발자는 오직 비즈니스 로직에만 집중하면 됩니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 grammY 라이브러리에서 Bot 클래스를 가져옵니다.
이것이 봇의 뼈대가 됩니다. 다음으로 환경변수에서 BOT_TOKEN을 읽어와 봇 인스턴스를 생성합니다.
이 토큰은 BotFather라는 특별한 텔레그램 봇에게서 발급받을 수 있습니다. 그다음 bot.on() 메서드로 이벤트 핸들러를 등록합니다.
"message:text" 이벤트는 사용자가 텍스트 메시지를 보낼 때 발생합니다. 콜백 함수의 ctx 파라미터는 컨텍스트 객체로, 메시지 정보와 응답 메서드를 모두 포함하고 있습니다.
마지막으로 bot.start()를 호출하면 봇이 실제로 작동하기 시작합니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 이커머스 회사를 운영한다고 가정해봅시다. 고객들이 "배송 조회", "반품 신청", "상품 문의" 같은 질문을 자주 합니다.
Telegram Bot을 만들면 이런 질문에 24시간 자동으로 답변할 수 있습니다. 실제로 많은 스타트업들이 고객 지원 비용을 줄이기 위해 텔레그램 봇을 적극 활용하고 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 봇 토큰을 코드에 직접 넣는 것입니다.
이렇게 하면 코드가 GitHub에 올라갔을 때 누구나 토큰을 볼 수 있어 보안 문제가 발생합니다. 따라서 반드시 환경변수로 관리해야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.
"생각보다 간단하네요!" Telegram Bot API를 제대로 이해하면 고객과 소통하는 새로운 채널을 쉽게 만들 수 있습니다. 여러분도 오늘 배운 내용으로 첫 번째 봇을 만들어 보세요.
실전 팁
💡 - BotFather에서 봇을 생성하면 토큰이 발급됩니다. 이 토큰을 반드시 안전하게 보관하세요.
- 개발 환경과 프로덕션 환경을 분리하려면 봇을 두 개 만들어서 각각 다른 토큰을 사용하세요.
2. grammY 프레임워크 특징
김개발 씨는 Telegram Bot API를 처음 접하고 여러 라이브러리를 찾아봤습니다. node-telegram-bot-api, telegraf, grammY 등 선택지가 많았습니다.
선배 박시니어 씨는 주저 없이 "grammY를 쓰세요"라고 말했습니다.
grammY는 TypeScript 기반의 최신 Telegram Bot 프레임워크입니다. 마치 고급 요리사가 쓰는 프리미엄 주방 도구 세트와 같습니다.
타입 안전성, 미들웨어 패턴, 플러그인 시스템 등 현대적인 개발 방식을 모두 지원합니다. 이를 활용하면 유지보수하기 쉽고 확장 가능한 봇을 만들 수 있습니다.
다음 코드를 살펴봅시다.
import { Bot, Context } from "grammy";
import type { Update } from "grammy/types";
// 커스텀 컨텍스트 타입 정의
interface MyContext extends Context {
userId: number;
}
// 타입 안전한 봇 생성
const bot = new Bot<MyContext>(process.env.BOT_TOKEN);
// 미들웨어로 userId 자동 주입
bot.use(async (ctx, next) => {
ctx.userId = ctx.from?.id || 0;
await next();
});
김개발 씨는 구글링을 하다가 Telegram Bot 라이브러리가 생각보다 많다는 것을 알게 되었습니다. "어떤 걸 써야 하지?" 고민하던 중 박시니어 씨가 다가왔습니다.
"grammY를 추천합니다. 제가 3년간 여러 봇을 만들어봤는데, 이게 제일 좋더라고요." 그렇다면 grammY의 특별한 점은 무엇일까요?
쉽게 비유하자면, grammY는 마치 최신형 자동차와 같습니다. 예전 자동차도 달리는 데는 문제없지만, 최신 자동차는 자동 주차, 차선 유지, 스마트 크루즈 등 편리한 기능이 가득합니다.
grammY도 마찬가지입니다. 기본적인 메시지 송수신은 물론이고, TypeScript 타입 안전성, 미들웨어 체인, 플러그인 시스템 같은 현대적인 기능을 모두 제공합니다.
다른 라이브러리들의 문제는 무엇이었을까요? 예를 들어 node-telegram-bot-api는 오래된 라이브러리로 JavaScript만 지원합니다.
타입 체크가 없어서 런타임에 오류가 발생하기 쉽습니다. telegraf는 TypeScript를 지원하지만 타입 정의가 완벽하지 않아 여전히 any 타입을 자주 쓰게 됩니다.
코드가 길어질수록 유지보수가 어려워집니다. 바로 이런 문제를 해결하기 위해 grammY가 만들어졌습니다.
grammY는 처음부터 TypeScript First 철학으로 설계되었습니다. 모든 API가 완벽한 타입 정의를 가지고 있어서 VSCode에서 자동완성이 완벽하게 작동합니다.
잘못된 파라미터를 넘기면 컴파일 단계에서 바로 에러가 납니다. 런타임 오류를 미리 방지할 수 있는 것입니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 grammY에서 Bot과 Context를 가져옵니다.
여기서 핵심은 커스텀 컨텍스트 타입을 정의하는 부분입니다. MyContext 인터페이스를 만들어서 ctx 객체에 userId 속성을 추가했습니다.
이제 모든 핸들러에서 ctx.userId를 타입 안전하게 사용할 수 있습니다. 다음으로 Bot<MyContext>처럼 제네릭으로 타입을 지정합니다.
이것이 grammY의 강력한 점입니다. 그다음 bot.use()로 미들웨어를 등록합니다.
이 미들웨어는 모든 요청에서 자동으로 실행되어 userId를 ctx에 주입합니다. Express.js의 미들웨어 패턴과 똑같습니다.
실제 현업에서는 어떻게 활용할까요? 대규모 봇 서비스를 운영한다고 가정해봅시다.
사용자 인증, 로깅, 에러 처리, 권한 체크 등 공통 로직이 많습니다. grammY의 미들웨어 시스템을 사용하면 이런 로직을 재사용 가능한 모듈로 분리할 수 있습니다.
코드가 깔끔해지고 테스트하기도 쉬워집니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 미들웨어에서 next()를 호출하지 않는 것입니다. 이렇게 하면 다음 미들웨어나 핸들러가 실행되지 않아 봇이 응답하지 않습니다.
반드시 await next()를 호출해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 추천을 받은 김개발 씨는 grammY로 봇을 만들기 시작했습니다. "타입 자동완성이 이렇게 편할 줄은 몰랐어요!" grammY를 제대로 이해하면 더 안전하고 유지보수하기 쉬운 봇을 만들 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - TypeScript를 사용하면 grammY의 진가를 100% 활용할 수 있습니다.
- 공통 로직은 미들웨어로 분리하면 코드 재사용성이 높아집니다.
3. src telegram 코드 분석
김개발 씨는 회사 프로젝트의 src/telegram 폴더를 열었습니다. 파일이 여러 개로 나뉘어져 있었습니다.
bot.ts, handlers/, middlewares/, utils/ 등 체계적으로 구조화되어 있었습니다. "어떻게 이렇게 깔끔하게 관리하지?" 궁금해졌습니다.
src/telegram 디렉토리 구조는 봇 코드를 체계적으로 관리하는 패턴입니다. 마치 도서관에서 책을 분류하듯이 기능별로 코드를 분리합니다.
bot.ts는 봇 인스턴스, handlers는 명령어 처리, middlewares는 공통 로직, utils는 헬퍼 함수를 담당합니다. 이렇게 분리하면 코드 찾기가 쉽고 협업도 원활해집니다.
다음 코드를 살펴봅시다.
// src/telegram/bot.ts
import { Bot } from "grammy";
import { registerHandlers } from "./handlers";
import { errorMiddleware } from "./middlewares/error";
// 봇 인스턴스 생성 및 설정
export const bot = new Bot(process.env.BOT_TOKEN);
// 에러 처리 미들웨어 등록
bot.use(errorMiddleware);
// 모든 핸들러 등록
registerHandlers(bot);
// 웹훅 설정 또는 폴링 시작
export async function startBot() {
await bot.api.setWebhook(process.env.WEBHOOK_URL);
}
김개발 씨는 처음에 모든 코드를 하나의 파일에 작성했습니다. 100줄, 200줄, 어느새 500줄이 넘어갔습니다.
코드를 수정하려고 하면 원하는 부분을 찾기가 어려웠습니다. "이건 아닌 것 같은데..." 고민하던 중 박시니어 씨가 코드 리뷰를 했습니다.
"김 개발자님, 코드를 분리해야 합니다. 하나의 파일에 모든 것을 넣으면 나중에 유지보수가 불가능해요." 그렇다면 코드 분리는 어떻게 해야 할까요?
쉽게 비유하자면, 코드 분리는 마치 옷장을 정리하는 것과 같습니다. 상의, 하의, 속옷, 양말을 각각 다른 서랍에 보관하면 필요한 옷을 빨리 찾을 수 있습니다.
코드도 마찬가지입니다. 봇 설정, 명령어 핸들러, 미들웨어, 유틸리티 함수를 각각 다른 파일에 보관하면 원하는 코드를 빨리 찾을 수 있습니다.
코드를 분리하지 않으면 어떤 문제가 생길까요? 첫 번째 문제는 가독성입니다.
1000줄이 넘는 파일에서 특정 함수를 찾으려면 스크롤을 끝없이 내려야 합니다. 두 번째 문제는 충돌입니다.
여러 명이 같은 파일을 수정하면 Git 충돌이 자주 발생합니다. 세 번째 문제는 테스트입니다.
모든 것이 한 파일에 있으면 특정 기능만 테스트하기 어렵습니다. 바로 이런 문제를 해결하기 위해 디렉토리 구조 패턴이 등장했습니다.
src/telegram 폴더를 만들고 그 안에 bot.ts, handlers/, middlewares/, utils/ 같은 하위 폴더를 만듭니다. bot.ts는 봇의 진입점으로, 봇 인스턴스를 생성하고 설정합니다.
handlers/는 각 명령어를 처리하는 함수들을 담습니다. middlewares/는 인증, 로깅 같은 공통 로직을 담습니다.
utils/는 날짜 포맷팅, 문자열 처리 같은 헬퍼 함수들을 담습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 grammY에서 Bot을 가져오고, 다른 모듈에서 registerHandlers와 errorMiddleware를 가져옵니다. 이것이 모듈화의 핵심입니다.
다음으로 봇 인스턴스를 생성하고 export로 내보냅니다. 다른 파일에서도 이 봇 인스턴스를 사용할 수 있습니다.
그다음 bot.use(errorMiddleware)로 에러 처리 미들웨어를 등록합니다. 이 미들웨어는 모든 에러를 잡아서 적절히 처리합니다.
다음으로 registerHandlers(bot)를 호출해서 모든 명령어 핸들러를 한 번에 등록합니다. 마지막으로 startBot 함수에서 웹훅을 설정합니다.
실제 현업에서는 어떻게 활용할까요? 대규모 팀에서 개발한다고 가정해봅시다.
A 개발자는 결제 명령어를, B 개발자는 배송 조회 명령어를, C 개발자는 관리자 기능을 담당합니다. 코드가 잘 분리되어 있으면 각자 담당한 파일만 수정하면 되므로 충돌이 거의 발생하지 않습니다.
코드 리뷰도 쉬워집니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 너무 세세하게 분리하는 것입니다. 한 줄짜리 함수를 별도 파일로 만들면 오히려 코드를 찾기 어려워집니다.
적절한 수준에서 분리해야 합니다. 일반적으로 파일 하나가 200-300줄을 넘지 않는 것이 좋습니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언을 듣고 김개발 씨는 코드를 분리했습니다.
"이제 훨씬 찾기 쉬워졌어요!" 코드 구조를 제대로 잡으면 장기적으로 유지보수 비용을 크게 줄일 수 있습니다. 여러분도 오늘 배운 패턴을 프로젝트에 적용해 보세요.
실전 팁
💡 - 파일 하나가 300줄을 넘으면 분리를 고려하세요.
- handlers/ 폴더 안에서도 기능별로 파일을 나누면 더 깔끔합니다 (payment.ts, shipping.ts 등).
4. 명령어 핸들러 구현
김개발 씨는 첫 번째 명령어를 만들기로 했습니다. "/start" 명령어를 입력하면 환영 메시지를 보여주는 간단한 기능이었습니다.
"이것도 핸들러로 분리해야 하나?" 고민했지만, 선배의 조언을 따르기로 했습니다.
명령어 핸들러는 사용자가 특정 명령어를 입력했을 때 실행되는 함수입니다. 마치 리모컨 버튼을 누르면 TV가 반응하는 것과 같습니다.
/start, /help, /settings 같은 명령어마다 핸들러를 만들어서 등록합니다. 이렇게 하면 명령어마다 다른 동작을 수행할 수 있습니다.
다음 코드를 살펴봅시다.
// src/telegram/handlers/start.ts
import { Context } from "grammy";
// /start 명령어 핸들러
export async function handleStart(ctx: Context) {
const userName = ctx.from?.first_name || "사용자";
await ctx.reply(
`안녕하세요, ${userName}님! 👋\n\n` +
`저는 고객 지원 봇입니다.\n` +
`/help - 도움말 보기\n` +
`/settings - 설정 변경`,
{ parse_mode: "Markdown" }
);
}
김개발 씨는 간단한 /start 명령어부터 만들기로 했습니다. 처음에는 bot.ts 파일에 직접 작성하려고 했습니다.
하지만 박시니어 씨의 조언이 떠올랐습니다. "핸들러는 별도 파일로 분리하세요." 그래서 src/telegram/handlers/start.ts 파일을 새로 만들었습니다.
그렇다면 명령어 핸들러는 어떻게 작동할까요? 쉽게 비유하자면, 명령어 핸들러는 마치 콜센터 상담원과 같습니다.
고객이 "배송 조회"라고 말하면 배송팀으로 연결해주고, "환불 신청"이라고 하면 환불팀으로 연결해줍니다. 텔레그램 봇도 마찬가지입니다.
사용자가 /start를 입력하면 handleStart 함수가 실행되고, /help를 입력하면 handleHelp 함수가 실행됩니다. 핸들러를 분리하지 않으면 어떤 문제가 생길까요?
모든 명령어를 하나의 함수에서 if-else로 처리하면 코드가 지저분해집니다. 새로운 명령어를 추가할 때마다 기존 코드를 수정해야 합니다.
또한 특정 명령어만 테스트하기 어렵습니다. 팀원들과 협업할 때도 불편합니다.
누군가 /payment 명령어를 수정하는데 같은 파일에서 /shipping 명령어를 수정하면 충돌이 발생합니다. 바로 이런 문제를 해결하기 위해 핸들러 분리 패턴이 등장했습니다.
각 명령어마다 별도의 파일을 만들고, 핸들러 함수를 export 합니다. 그리고 bot.ts나 별도의 registerHandlers 함수에서 이들을 한 번에 등록합니다.
이렇게 하면 새로운 명령어를 추가할 때 기존 코드를 건드리지 않아도 됩니다. 개방-폐쇄 원칙을 따르는 것입니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 grammY에서 Context 타입을 가져옵니다.
핸들러 함수는 ctx 파라미터를 받는데, 이 객체에 메시지 정보와 응답 메서드가 모두 들어있습니다. 다음으로 ctx.from?.first_name으로 사용자 이름을 가져옵니다.
옵셔널 체이닝(?.)을 사용해서 값이 없을 때 에러를 방지합니다. 그다음 ctx.reply()로 응답 메시지를 보냅니다.
메시지 안에 템플릿 리터럴로 사용자 이름을 삽입합니다. 옵션 객체에 parse_mode: "Markdown"을 넘겨서 마크다운 포맷팅을 활성화합니다.
이제 볼드, 이탤릭 같은 서식을 사용할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?
이커머스 봇을 만든다고 가정해봅시다. /orders 명령어는 주문 목록을 보여주고, /track 명령어는 배송을 추적하고, /return 명령어는 반품을 신청합니다.
각각 별도의 파일로 만들어서 독립적으로 개발하고 테스트할 수 있습니다. 나중에 새로운 기능이 추가되어도 기존 코드에 영향을 주지 않습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 핸들러에서 비즈니스 로직을 직접 작성하는 것입니다.
예를 들어 데이터베이스 쿼리를 핸들러 안에 넣으면 재사용이 어렵고 테스트도 복잡해집니다. 대신 서비스 레이어를 만들어서 비즈니스 로직을 분리하는 것이 좋습니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 첫 번째 핸들러를 만든 김개발 씨는 뿌듯했습니다.
"봇이 내 이름을 불러주네!" 명령어 핸들러를 제대로 구현하면 사용자 경험을 크게 개선할 수 있습니다. 여러분도 오늘 배운 패턴을 활용해 보세요.
실전 팁
💡 - 핸들러에서는 응답만 처리하고, 비즈니스 로직은 별도 서비스로 분리하세요.
- 에러 처리는 미들웨어에서 일괄적으로 하는 것이 좋습니다.
5. 인라인 키보드와 콜백
김개발 씨는 단순한 텍스트 응답만으로는 부족하다고 느꼈습니다. 사용자가 버튼을 클릭해서 선택할 수 있으면 더 편리할 것 같았습니다.
"텔레그램 봇에서 버튼을 만들 수 있나?" 검색하다가 인라인 키보드를 발견했습니다.
인라인 키보드는 메시지 아래에 표시되는 버튼들입니다. 마치 스마트폰 앱의 버튼 UI와 같습니다.
사용자가 버튼을 클릭하면 콜백 쿼리가 발생하고, 이를 처리하는 핸들러가 실행됩니다. 텍스트를 입력하는 대신 버튼을 클릭하면 되므로 사용자 경험이 크게 향상됩니다.
다음 코드를 살펴봅시다.
// src/telegram/handlers/menu.ts
import { Context, InlineKeyboard } from "grammy";
// 메뉴 보여주기
export async function showMenu(ctx: Context) {
const keyboard = new InlineKeyboard()
.text("주문 조회", "callback_orders")
.text("배송 추적", "callback_shipping").row()
.text("고객센터", "callback_support");
await ctx.reply("원하시는 메뉴를 선택하세요:", {
reply_markup: keyboard
});
}
// 콜백 처리
export async function handleCallback(ctx: Context) {
const data = ctx.callbackQuery?.data;
if (data === "callback_orders") {
await ctx.answerCallbackQuery();
await ctx.reply("주문 목록을 조회합니다...");
}
}
김개발 씨는 사용자가 매번 명령어를 입력하는 것이 불편하다고 생각했습니다. "주문 조회", "배송 추적" 같은 메뉴를 버튼으로 만들 수 있다면 훨씬 편할 것 같았습니다.
박시니어 씨에게 물어봤습니다. "텔레그램에서 버튼을 만들 수 있나요?" "물론이죠.
인라인 키보드를 사용하면 됩니다." 그렇다면 인라인 키보드란 무엇일까요? 쉽게 비유하자면, 인라인 키보드는 마치 식당의 태블릿 메뉴판과 같습니다.
손님이 직접 메뉴 이름을 말하는 대신 화면에 있는 버튼을 터치하면 됩니다. 텔레그램도 마찬가지입니다.
사용자가 "/order"라고 타이핑하는 대신 "주문 조회" 버튼을 클릭하면 됩니다. 훨씬 직관적이고 빠릅니다.
인라인 키보드가 없으면 어떤 문제가 생길까요? 사용자는 모든 명령어를 외워야 합니다.
"/order"인지 "/orders"인지 헷갈립니다. 오타가 나면 봇이 반응하지 않습니다.
특히 모바일에서는 타이핑이 불편합니다. 이런 불편함 때문에 사용자들이 봇을 잘 쓰지 않게 됩니다.
바로 이런 문제를 해결하기 위해 인라인 키보드가 등장했습니다. InlineKeyboard 클래스로 버튼을 만들 수 있습니다.
text() 메서드로 버튼 텍스트와 콜백 데이터를 지정합니다. row() 메서드로 다음 줄로 넘어갑니다.
마지막으로 reply_markup 옵션에 키보드를 넘기면 메시지와 함께 버튼이 표시됩니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 grammY에서 InlineKeyboard를 가져옵니다. 다음으로 new InlineKeyboard()로 키보드 인스턴스를 생성합니다.
text("주문 조회", "callback_orders")는 "주문 조회"라는 텍스트를 가진 버튼을 만들고, 클릭하면 "callback_orders"라는 데이터를 보냅니다. 두 번째와 세 번째 버튼도 같은 방식으로 추가합니다.
중간에 .row()를 호출하면 다음 버튼은 새로운 줄에 배치됩니다. 마지막으로 ctx.reply()에 reply_markup: keyboard를 넘기면 메시지와 함께 버튼이 표시됩니다.
콜백 핸들러에서는 ctx.callbackQuery?.data로 어떤 버튼이 클릭되었는지 확인합니다. "callback_orders"이면 주문 조회 로직을 실행합니다.
중요한 것은 ctx.answerCallbackQuery()를 반드시 호출해야 한다는 점입니다. 이것을 호출하지 않으면 사용자 화면에 로딩 표시가 계속 돕니다.
실제 현업에서는 어떻게 활용할까요? 고객 지원 봇을 만든다고 가정해봅시다.
사용자가 문제를 선택하면 그에 맞는 해결책을 제시합니다. "배송 지연", "제품 불량", "환불 요청" 같은 버튼을 만들어서 사용자가 쉽게 선택하게 합니다.
각 버튼마다 적절한 답변이나 추가 질문을 보여줍니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 콜백 데이터에 너무 긴 값을 넣는 것입니다. 텔레그램은 콜백 데이터를 최대 64바이트로 제한합니다.
긴 값을 저장하려면 데이터베이스에 저장하고 ID만 콜백 데이터로 보내야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
인라인 키보드를 추가한 김개발 씨는 봇이 훨씬 전문적으로 보인다고 느꼈습니다. "이제 진짜 앱 같아요!" 인라인 키보드를 제대로 활용하면 사용자 경험을 크게 향상시킬 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 콜백 데이터는 짧고 명확하게 작성하세요 (최대 64바이트).
- 반드시 answerCallbackQuery()를 호출해야 로딩이 사라집니다.
6. 실전 Telegram 봇 배포하기
김개발 씨는 드디어 봇을 완성했습니다. 로컬에서는 완벽하게 작동했습니다.
이제 실제 서버에 배포할 차례였습니다. "어떻게 배포하지?" 고민하던 중 선배가 두 가지 방법을 알려줬습니다.
폴링과 웹훅이었습니다.
Telegram 봇 배포는 개발한 봇을 실제 서버에서 실행하는 과정입니다. 마치 만든 음식을 식당에서 손님에게 내놓는 것과 같습니다.
폴링 방식은 봇이 주기적으로 텔레그램 서버에 요청해서 새 메시지를 가져오고, 웹훅 방식은 텔레그램이 새 메시지를 봇 서버로 직접 보내줍니다. 프로덕션에서는 웹훅이 더 효율적입니다.
다음 코드를 살펴봅시다.
// src/telegram/webhook.ts
import { webhookCallback } from "grammy";
import { bot } from "./bot";
// Next.js API 라우트에서 웹훅 처리
export async function setupWebhook() {
const webhookUrl = process.env.WEBHOOK_URL;
// 웹훅 URL 설정
await bot.api.setWebhook(webhookUrl, {
drop_pending_updates: true
});
console.log("웹훅 설정 완료:", webhookUrl);
}
// 웹훅 핸들러 (Express/Next.js에서 사용)
export const handleWebhook = webhookCallback(bot, "std/http");
김개발 씨는 로컬에서 bot.start()를 실행해서 봇을 테스트했습니다. 잘 작동했습니다.
하지만 서버에 배포하려니 막막했습니다. "계속 실행되게 하려면 어떻게 해야 하지?" 박시니어 씨가 설명했습니다.
"두 가지 방법이 있습니다. 폴링과 웹훅입니다." 그렇다면 폴링과 웹훅의 차이는 무엇일까요?
쉽게 비유하자면, 폴링은 마치 우체통을 주기적으로 확인하는 것과 같습니다. 매 30초마다 우체통을 열어보고 새 편지가 있는지 확인합니다.
웹훅은 우편배달부가 편지를 직접 집 앞까지 가져다주는 것과 같습니다. 편지가 도착하면 바로 벨을 눌러서 알려줍니다.
폴링 방식의 문제는 무엇일까요? 폴링은 새 메시지가 없어도 계속 확인 요청을 보냅니다.
트래픽이 낭비되고 서버 부하가 늘어납니다. 또한 확인 주기만큼 지연이 발생합니다.
30초마다 확인한다면 최대 30초까지 응답이 늦어질 수 있습니다. 대규모 서비스에서는 효율적이지 않습니다.
바로 이런 문제를 해결하기 위해 웹훅 방식이 등장했습니다. 웹훅은 새 메시지가 도착하면 텔레그램이 즉시 봇 서버로 HTTP POST 요청을 보냅니다.
봇은 이 요청을 받아서 처리하면 됩니다. 불필요한 트래픽이 없고, 응답도 즉각적입니다.
프로덕션 환경에서는 웹훅이 표준입니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 grammY에서 webhookCallback을 가져옵니다. 이것이 웹훅 요청을 처리하는 핵심 함수입니다.
다음으로 환경변수에서 WEBHOOK_URL을 읽어옵니다. 이것은 텔레그램이 메시지를 보낼 주소입니다.
예를 들어 "https://your-domain.com/api/telegram" 같은 형식입니다. 그다음 bot.api.setWebhook()으로 텔레그램에 웹훅 URL을 등록합니다.
drop_pending_updates: true 옵션은 봇이 오프라인이었을 때 쌓인 메시지를 무시합니다. 마지막으로 webhookCallback(bot, "std/http")으로 웹훅 핸들러를 만듭니다.
이것을 Express나 Next.js API 라우트에서 사용하면 됩니다. 실제 현업에서는 어떻게 활용할까요?
Next.js 프로젝트를 Vercel에 배포한다고 가정해봅시다. /api/telegram 라우트를 만들고 handleWebhook을 연결합니다.
Vercel 도메인을 WEBHOOK_URL로 설정합니다. 이제 사용자가 메시지를 보내면 텔레그램이 Vercel 서버로 요청을 보내고, Next.js API가 처리합니다.
서버리스 환경에서도 완벽하게 작동합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 웹훅 URL을 HTTPS로 설정하지 않는 것입니다. 텔레그램은 보안을 위해 반드시 HTTPS를 요구합니다.
HTTP로는 작동하지 않습니다. 로컬 개발 시에는 ngrok 같은 터널링 도구를 사용해서 HTTPS를 얻을 수 있습니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 웹훅을 설정하고 배포한 김개발 씨는 스마트폰으로 봇을 테스트했습니다.
"와, 실시간으로 답장이 와요!" Telegram 봇을 제대로 배포하면 24시간 자동으로 사용자를 응대할 수 있습니다. 여러분도 오늘 배운 내용으로 실전 서비스를 만들어 보세요.
실전 팁
💡 - 프로덕션에서는 반드시 웹훅을 사용하세요 (폴링은 개발/테스트 용도).
- 웹훅 URL은 반드시 HTTPS여야 합니다.
- PM2나 Docker로 봇을 관리하면 안정적으로 운영할 수 있습니다.
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
UX와 협업 패턴 완벽 가이드
AI 에이전트와 사용자 간의 효과적인 협업을 위한 UX 패턴을 다룹니다. 프롬프트 핸드오프부터 인터럽트 처리까지, 현대적인 에이전트 시스템 설계의 핵심을 배웁니다.
자가 치유 및 재시도 패턴 완벽 가이드
AI 에이전트와 분산 시스템에서 필수적인 자가 치유 패턴을 다룹니다. 에러 감지부터 서킷 브레이커까지, 시스템을 스스로 복구하는 탄력적인 코드 작성법을 배워봅니다.
Feedback Loops 컴파일러와 CI/CD 완벽 가이드
컴파일러 피드백 루프부터 CI/CD 파이프라인, 테스트 자동화, 자가 치유 빌드까지 현대 개발 워크플로우의 핵심을 다룹니다. 초급 개발자도 쉽게 이해할 수 있도록 실무 예제와 함께 설명합니다.
실전 MCP 통합 프로젝트 완벽 가이드
Model Context Protocol을 활용한 실전 통합 프로젝트를 처음부터 끝까지 구축하는 방법을 다룹니다. 아키텍처 설계부터 멀티 서버 통합, 모니터링, 배포까지 운영 레벨의 MCP 시스템을 구축하는 노하우를 담았습니다.
MCP 동적 도구 업데이트 완벽 가이드
AI 에이전트의 도구를 런타임에 동적으로 로딩하고 관리하는 방법을 알아봅니다. 플러그인 시스템 설계부터 핫 리로딩, 보안까지 실무에서 바로 적용할 수 있는 내용을 다룹니다.