본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2026. 1. 31. · 8 Views
WhatsApp 통합 완벽 가이드 Baileys로 시작하기
Node.js 환경에서 Baileys 라이브러리를 활용하여 WhatsApp Web 프로토콜을 통합하는 완벽 가이드입니다. QR 인증부터 메시지 송수신, 미디어 처리까지 실전 예제와 함께 배웁니다.
목차
1. WhatsApp Web 프로토콜의 비밀
김개발 씨는 회사에서 고객 상담 자동화 프로젝트를 맡게 되었습니다. "WhatsApp으로 자동 응답 시스템을 만들어야 하는데, 어떻게 시작해야 할까요?" 막막했던 김개발 씨는 선배 박시니어 씨를 찾아갔습니다.
"WhatsApp에도 공식 API가 있지만, Baileys라는 라이브러리를 쓰면 더 자유롭게 개발할 수 있어요."
WhatsApp Web 프로토콜은 웹 브라우저에서 WhatsApp을 사용할 때 동작하는 통신 방식입니다. Baileys는 이 프로토콜을 역공학하여 Node.js에서 WhatsApp을 제어할 수 있게 만든 오픈소스 라이브러리입니다.
공식 Business API보다 제약이 적고, 무료로 사용할 수 있다는 큰 장점이 있습니다.
다음 코드를 살펴봅시다.
// Baileys 라이브러리 기본 설정
const { default: makeWASocket, DisconnectReason, useMultiFileAuthState } = require('@whiskeysockets/baileys');
// WhatsApp 소켓 연결 생성
async function connectToWhatsApp() {
// 인증 정보를 파일로 저장하여 재연결 시 활용
const { state, saveCreds } = await useMultiFileAuthState('auth_info_baileys');
// WhatsApp 소켓 인스턴스 생성
const sock = makeWASocket({
auth: state,
printQRInTerminal: true // QR 코드를 터미널에 출력
});
return sock;
}
김개발 씨는 평소 PC에서 WhatsApp Web을 자주 사용했습니다. 휴대폰으로 QR 코드를 스캔하면 컴퓨터에서도 메시지를 주고받을 수 있는 그 기능 말입니다.
그런데 이 편리한 기능 뒤에 숨겨진 기술적 비밀이 있다는 것을 처음 알게 되었습니다. 박시니어 씨가 화이트보드에 그림을 그리며 설명했습니다.
"WhatsApp Web은 단순히 웹사이트가 아니에요. 휴대폰과 웹 브라우저가 WebSocket으로 실시간 연결되어 있죠." WhatsApp Web 프로토콜이란 무엇일까요?
쉽게 비유하자면, WhatsApp Web 프로토콜은 마치 두 사람이 실시간으로 전화 통화를 하는 것과 같습니다. 한쪽에서 말하면 즉시 상대방에게 들리고, 끊김 없이 대화가 이어지죠.
웹 브라우저와 WhatsApp 서버도 이렇게 실시간으로 연결되어 메시지를 주고받습니다. WhatsApp이 제공하는 공식 방법은 크게 두 가지입니다.
첫 번째는 WhatsApp Business API입니다. 기업용으로 설계되어 있고, 메타(Facebook)의 공식 지원을 받습니다.
하지만 비용이 발생하고, 승인 절차가 복잡하며, 사용에 여러 제약이 따릅니다. 개인 개발자나 작은 프로젝트에는 부담스러운 선택입니다.
두 번째는 WhatsApp Web 프로토콜을 활용하는 것입니다. 바로 우리가 PC에서 사용하는 그 기술입니다.
공식 API는 아니지만, 누구나 무료로 사용할 수 있습니다. 바로 이 두 번째 방법을 Node.js에서 쉽게 사용할 수 있게 만든 것이 Baileys 라이브러리입니다.
Baileys는 WhatsApp Web의 통신 방식을 분석하여 만들어진 오픈소스 프로젝트입니다. "역공학"이라는 표현이 어렵게 들릴 수 있지만, 실제로는 WhatsApp Web이 어떻게 동작하는지 관찰하고 같은 방식으로 통신하는 것입니다.
김개발 씨가 궁금한 표정으로 물었습니다. "그럼 우리가 만드는 프로그램이 WhatsApp 서버 입장에서는 웹 브라우저처럼 보인다는 말씀이신가요?" 박시니어 씨가 환하게 웃으며 답했습니다.
"정확해요! WhatsApp 서버는 우리 Node.js 프로그램을 그냥 또 하나의 WhatsApp Web 클라이언트로 인식합니다." 위의 코드를 살펴보면 핵심 개념을 파악할 수 있습니다.
makeWASocket 함수는 WhatsApp 서버와의 WebSocket 연결을 생성합니다. 마치 전화를 거는 것과 같습니다.
useMultiFileAuthState는 인증 정보를 파일로 저장하여, 프로그램을 다시 실행해도 QR 코드를 스캔하지 않고 자동으로 연결되게 합니다. printQRInTerminal: true 옵션은 처음 연결할 때 터미널에 QR 코드를 출력합니다.
휴대폰으로 이 QR 코드를 스캔하면, 마치 WhatsApp Web에 로그인하는 것처럼 인증이 완료됩니다. 실제 프로덕션 환경에서는 어떻게 활용할까요?
많은 스타트업과 중소기업들이 Baileys를 활용하여 고객 응답 자동화, 주문 확인 메시지, 예약 알림 등을 구현하고 있습니다. 공식 API 비용을 절감하면서도 WhatsApp의 강력한 메시징 기능을 활용할 수 있기 때문입니다.
하지만 주의할 점도 있습니다. Baileys는 비공식 라이브러리이므로 WhatsApp의 정책 변경에 영향을 받을 수 있습니다.
또한 대량의 메시지를 짧은 시간에 발송하면 스팸으로 간주되어 계정이 차단될 수 있습니다. 따라서 적절한 딜레이를 두고, 사용자가 먼저 메시지를 보낸 경우에만 응답하는 방식으로 구현하는 것이 안전합니다.
김개발 씨는 이제 WhatsApp 통합의 기본 원리를 이해했습니다. "생각보다 간단하네요.
그럼 본격적으로 코드를 작성해볼까요?" WhatsApp Web 프로토콜의 비밀을 알게 되면, 무궁무진한 자동화 가능성이 열립니다. 다음 섹션에서는 Baileys 라이브러리를 설치하고 기본 설정을 해보겠습니다.
실전 팁
💡 - Baileys는 공식 API가 아니므로 상업적 대규모 서비스보다는 소규모 자동화에 적합합니다
- 계정 차단을 방지하려면 메시지 발송 간격을 2-3초 이상으로 설정하세요
- 인증 정보 폴더(auth_info_baileys)는 절대 git에 커밋하지 마세요
2. Baileys 라이브러리 소개
"이제 실제로 설치해볼까요?" 박시니어 씨가 터미널을 열며 말했습니다. 김개발 씨는 npm으로 패키지를 설치하는 것쯤은 자신 있었지만, Baileys는 다른 라이브러리들과는 조금 다른 점이 있었습니다.
"의존성 패키지가 꽤 많네요. 이유가 뭔가요?"
Baileys는 WhatsApp Web 프로토콜을 구현한 Node.js 라이브러리입니다. WebSocket 통신, 암호화, 미디어 처리 등 복잡한 기능을 간단한 API로 제공합니다.
최신 버전인 @whiskeysockets/baileys는 안정성과 성능이 크게 개선되었으며, TypeScript를 완벽하게 지원합니다.
다음 코드를 살펴봅시다.
// package.json에 필요한 의존성 설치
// npm install @whiskeysockets/baileys @hapi/boom pino qrcode-terminal
const { default: makeWASocket, useMultiFileAuthState, DisconnectReason } = require('@whiskeysockets/baileys');
const { Boom } = require('@hapi/boom');
const pino = require('pino');
// 로깅 설정 - 개발 환경에서 디버깅에 필수
const logger = pino({ level: 'silent' }); // 'debug'로 변경하면 상세 로그 출력
async function startWhatsApp() {
const { state, saveCreds } = await useMultiFileAuthState('./auth_info');
const sock = makeWASocket({
logger,
auth: state,
printQRInTerminal: true
});
// 인증 정보가 업데이트되면 자동 저장
sock.ev.on('creds.update', saveCreds);
return sock;
}
김개발 씨가 npm install 명령어를 입력하자, 터미널에 여러 패키지가 설치되기 시작했습니다. "@whiskeysockets/baileys는 무엇이고, @hapi/boom은 또 뭔가요?" 박시니어 씨가 친절하게 설명했습니다.
"Baileys 라이브러리는 여러 강력한 도구들과 함께 동작해요. 각각의 역할이 있죠." Baileys 라이브러리의 구조를 이해하려면 먼저 패키지 이름부터 살펴봐야 합니다.
과거에는 @adiwajshing/baileys라는 이름으로 배포되었습니다. 하지만 유지보수가 중단되면서 커뮤니티 주도로 포크되어 @whiskeysockets/baileys라는 새 이름으로 다시 태어났습니다.
현재는 이 버전이 가장 안정적이고 활발하게 개발되고 있습니다. 마치 오래된 건물을 리모델링하여 더 튼튼하고 현대적으로 만드는 것과 같습니다.
기본 구조는 유지하면서도 버그를 수정하고 새로운 기능을 추가했습니다. Baileys가 의존하는 주요 패키지들을 살펴보겠습니다.
첫 번째는 @hapi/boom입니다. 이것은 에러 처리를 위한 라이브러리입니다.
WhatsApp 연결이 끊어지거나 문제가 발생했을 때, 어떤 종류의 에러인지 정확하게 파악할 수 있게 도와줍니다. 마치 병원에서 증상을 보고 정확한 병명을 진단하는 것과 같습니다.
두 번째는 pino입니다. 이것은 로깅 라이브러리로, 프로그램이 무슨 일을 하고 있는지 기록합니다.
개발 단계에서는 level: 'debug'로 설정하여 모든 동작을 확인할 수 있고, 프로덕션에서는 level: 'silent'로 설정하여 불필요한 로그를 줄일 수 있습니다. 세 번째는 qrcode-terminal입니다.
터미널에 QR 코드를 예쁘게 출력해주는 도구입니다. 사실 Baileys 자체에도 QR 출력 기능이 있지만, 이 라이브러리를 추가하면 더 깔끔하게 표시됩니다.
김개발 씨가 코드를 실행해보니 터미널에 네모난 패턴이 나타났습니다. "이게 QR 코드인가요?
휴대폰으로 스캔하면 되나요?" 박시니어 씨가 고개를 끄덕였습니다. "맞아요.
WhatsApp 앱을 열고 '연결된 기기' 메뉴에서 스캔하면 됩니다." 위의 코드에서 useMultiFileAuthState 함수는 매우 중요한 역할을 합니다. 처음 QR 코드를 스캔하면, WhatsApp 서버로부터 인증 토큰을 받습니다.
이 토큰은 "./auth_info" 폴더에 여러 파일로 저장됩니다. 다음에 프로그램을 재실행하면 저장된 토큰을 읽어와서 자동으로 로그인됩니다.
QR 코드를 매번 스캔할 필요가 없는 것이죠. sock.ev.on('creds.update', saveCreds) 부분도 중요합니다.
WhatsApp은 보안을 위해 주기적으로 인증 정보를 갱신합니다. 이 이벤트 리스너는 인증 정보가 업데이트될 때마다 자동으로 파일에 저장하여, 세션이 끊기지 않도록 합니다.
실제 서비스 환경에서는 어떻게 사용할까요? 많은 개발자들이 Baileys를 활용하여 챗봇, 알림 서비스, 고객 응대 시스템을 구축합니다.
예를 들어 식당 예약 시스템에서 예약이 확정되면 WhatsApp으로 자동 안내 메시지를 보낼 수 있습니다. 또는 쇼핑몰에서 주문 상태가 변경되면 실시간으로 고객에게 알릴 수도 있습니다.
하지만 초보 개발자들이 자주 하는 실수가 있습니다. 인증 정보 폴더(auth_info)를 Git 저장소에 커밋하는 것입니다.
이 폴더에는 민감한 인증 토큰이 들어있으므로, 반드시 .gitignore에 추가해야 합니다. 마치 집 열쇠를 길거리에 두고 가는 것과 같은 위험한 행동입니다.
또 다른 실수는 로그 레벨을 'debug'로 설정한 채 프로덕션에 배포하는 것입니다. 디버그 로그에는 메시지 내용이나 사용자 정보가 포함될 수 있어 개인정보 보호 문제가 발생할 수 있습니다.
김개발 씨는 휴대폰으로 QR 코드를 스캔했습니다. "와, 연결됐어요!
터미널에 'opened connection'이라고 뜨네요!" Baileys 라이브러리의 기본 구조를 이해하면, 이제 본격적으로 메시지를 주고받는 기능을 구현할 준비가 된 것입니다.
실전 팁
💡 - 항상 최신 버전의 @whiskeysockets/baileys를 사용하세요 (주기적으로 npm update 실행)
- 개발 중에는 logger level을 'debug'로, 프로덕션에서는 'warn'이나 'silent'로 설정하세요
- auth_info 폴더는 반드시 백업하세요 (이 폴더가 없어지면 다시 QR 스캔 필요)
3. QR 코드 인증 과정
터미널에 QR 코드가 표시되고 몇 초 후, 김개발 씨의 휴대폰에서 WhatsApp이 "새 기기가 연결되었습니다"라는 알림을 띄웠습니다. "신기하네요.
어떻게 QR 코드 하나로 로그인이 되는 거죠?" 박시니어 씨가 화이트보드에 다이어그램을 그리기 시작했습니다. "QR 코드 안에는 특별한 암호화 키가 들어있어요."
QR 코드 인증은 WhatsApp Web의 핵심 보안 메커니즘입니다. QR 코드에는 임시 암호화 키가 담겨 있고, 휴대폰으로 스캔하면 서버가 두 기기를 안전하게 연결합니다.
Baileys는 이 과정을 자동으로 처리하며, 연결 상태를 실시간으로 추적할 수 있는 이벤트를 제공합니다.
다음 코드를 살펴봅시다.
const { default: makeWASocket, useMultiFileAuthState, DisconnectReason } = require('@whiskeysockets/baileys');
async function connectWithQR() {
const { state, saveCreds } = await useMultiFileAuthState('./auth_info');
const sock = makeWASocket({
auth: state,
printQRInTerminal: true
});
// QR 코드 생성 이벤트 - 최대 3번까지 갱신됨
sock.ev.on('connection.update', (update) => {
const { connection, lastDisconnect, qr } = update;
if (qr) {
console.log('📱 휴대폰으로 QR 코드를 스캔하세요!');
}
if (connection === 'open') {
console.log('✅ WhatsApp에 성공적으로 연결되었습니다!');
}
// 연결이 끊긴 경우 재연결 처리
if (connection === 'close') {
const shouldReconnect = lastDisconnect?.error?.output?.statusCode !== DisconnectReason.loggedOut;
if (shouldReconnect) {
console.log('🔄 재연결 중...');
connectWithQR(); // 재귀 호출로 자동 재연결
}
}
});
sock.ev.on('creds.update', saveCreds);
}
김개발 씨는 궁금증이 생겼습니다. "QR 코드를 스캔하는 순간 정확히 무슨 일이 일어나는 건가요?" 박시니어 씨가 천천히 설명하기 시작했습니다.
"QR 코드 인증은 마치 호텔 체크인과 비슷해요." 호텔에 도착하면 프론트 데스크에서 신분증을 확인하고 룸 카드키를 발급해줍니다. 이 카드키는 오직 당신의 방만 열 수 있죠.
마찬가지로 QR 코드를 스캔하면, WhatsApp 서버가 당신의 Node.js 프로그램에게 전용 암호화 키를 발급합니다. QR 코드 인증 과정을 단계별로 살펴보겠습니다.
첫 번째 단계에서 Node.js 프로그램이 WhatsApp 서버에 "새 기기를 연결하고 싶어요"라고 요청합니다. 서버는 임시 암호화 키를 생성하여 QR 코드 형태로 전달합니다.
두 번째 단계에서 사용자가 휴대폰으로 이 QR 코드를 스캔합니다. 휴대폰 WhatsApp 앱은 QR 코드에서 암호화 키를 추출하여 서버로 보냅니다.
이때 "이 기기를 신뢰해도 됩니다"라는 의미의 승인 신호도 함께 전달됩니다. 세 번째 단계에서 서버는 휴대폰의 승인을 확인하고, Node.js 프로그램에게 정식 인증 토큰을 발급합니다.
이 토큰은 파일로 저장되어 다음 연결 시 재사용됩니다. 김개발 씨가 코드를 실행하면서 관찰했습니다.
"QR 코드가 30초마다 바뀌네요?" 박시니어 씨가 고개를 끄덕였습니다. "보안을 위해서예요.
QR 코드는 최대 3번까지 갱신되고, 그동안 스캔하지 않으면 연결이 실패합니다." 위의 코드에서 connection.update 이벤트는 연결 상태가 변할 때마다 발생합니다. qr 속성이 있으면 새로운 QR 코드가 생성된 것입니다.
printQRInTerminal: true 옵션이 있으면 자동으로 터미널에 출력되지만, qr 데이터를 직접 받아서 웹 페이지에 표시할 수도 있습니다. connection === 'open'은 연결이 성공적으로 완료되었다는 뜻입니다.
이 시점부터 메시지를 주고받을 수 있습니다. connection === 'close'는 연결이 끊어진 상황입니다.
하지만 모든 끊김이 문제가 아닙니다. 인터넷이 일시적으로 끊겼다가 복구되는 경우도 있기 때문이죠.
DisconnectReason.loggedOut은 사용자가 휴대폰에서 "이 기기 연결 해제"를 누른 경우입니다. 이때는 재연결하면 안 되고, 새로운 QR 코드를 받아야 합니다.
실무에서는 이런 연결 관리가 매우 중요합니다. 예를 들어 고객 응대 챗봇을 운영한다고 가정해봅시다.
서버가 재시작되거나 네트워크가 잠깐 끊어졌다고 해서 봇이 영구히 멈춰버리면 안 됩니다. 자동 재연결 로직이 있어야 안정적인 서비스를 제공할 수 있습니다.
초보 개발자들이 자주 하는 실수 중 하나는 무한 재연결 루프에 빠지는 것입니다. 만약 DisconnectReason.loggedOut 체크를 하지 않으면, 사용자가 연결을 해제했는데도 프로그램이 계속 재연결을 시도합니다.
WhatsApp 서버는 이를 비정상적인 동작으로 간주하여 IP를 일시적으로 차단할 수 있습니다. 또 다른 실수는 인증 정보를 저장하지 않는 것입니다.
saveCreds 함수를 호출하지 않으면, 프로그램을 재실행할 때마다 QR 코드를 다시 스캔해야 합니다. 운영 환경에서는 매우 불편한 상황이 발생합니다.
김개발 씨가 웹 인터페이스에 QR 코드를 표시하고 싶어했습니다. "터미널 대신 브라우저에 QR 코드를 보여줄 수는 없나요?" 박시니어 씨가 웃으며 답했습니다.
"물론이죠. qr 데이터를 qrcode 라이브러리로 이미지로 변환하고, Express나 Socket.IO로 클라이언트에 전송하면 됩니다." QR 코드 인증의 원리를 이해하면, 더 안정적이고 사용자 친화적인 WhatsApp 통합 시스템을 만들 수 있습니다.
인증이 완료되면 이제 본격적으로 메시지를 주고받을 차례입니다.
실전 팁
💡 - QR 코드는 3번 갱신 후 만료되므로, 사용자에게 "30초 이내에 스캔하세요"라고 안내하세요
- 재연결 로직에는 반드시 DisconnectReason 체크를 포함하세요
- 웹 인터페이스에 QR 표시 시 qrcode 또는 qrcode-terminal 라이브러리를 활용하세요
4. 메시지 송수신 구현
드디어 연결에 성공한 김개발 씨는 이제 실제로 메시지를 보내고 받아보고 싶었습니다. "메시지 전송은 어떻게 하나요?" 박시니어 씨가 간단한 예제 코드를 보여주었습니다.
"생각보다 훨씬 간단해요. 전화번호와 메시지 내용만 있으면 됩니다."
메시지 송수신은 Baileys의 핵심 기능입니다. sendMessage 함수로 텍스트, 이미지, 문서 등을 전송하고, messages.upsert 이벤트로 수신 메시지를 실시간으로 받을 수 있습니다.
전화번호 형식은 국가코드를 포함한 숫자에 "@s.whatsapp.net"을 붙여 사용합니다.
다음 코드를 살펴봅시다.
const { default: makeWASocket, useMultiFileAuthState } = require('@whiskeysockets/baileys');
async function sendAndReceiveMessages() {
const { state, saveCreds } = await useMultiFileAuthState('./auth_info');
const sock = makeWASocket({ auth: state });
sock.ev.on('creds.update', saveCreds);
// 메시지 수신 이벤트
sock.ev.on('messages.upsert', async (m) => {
const msg = m.messages[0];
// 내가 보낸 메시지는 무시
if (!msg.message || msg.key.fromMe) return;
const messageText = msg.message.conversation || msg.message.extendedTextMessage?.text;
const sender = msg.key.remoteJid; // 보낸 사람의 전화번호
console.log(`📩 ${sender}로부터 메시지: ${messageText}`);
// 자동 응답 예시
if (messageText?.toLowerCase().includes('안녕')) {
await sock.sendMessage(sender, { text: '안녕하세요! 무엇을 도와드릴까요?' });
}
});
// 메시지 전송 예시 (전화번호는 국가코드 포함)
// await sock.sendMessage('8210XXXXXXXX@s.whatsapp.net', { text: '안녕하세요!' });
}
김개발 씨는 간단한 에코 봇을 만들어보기로 했습니다. 사용자가 보낸 메시지를 그대로 따라 말하는 봇 말입니다.
하지만 코드를 실행하자마자 예상치 못한 문제가 발생했습니다. "왜 내가 보낸 메시지에도 봇이 반응하죠?" 김개발 씨는 당황했습니다.
자기 자신에게 메시지를 보냈는데, 봇이 그걸 받아서 다시 자동 응답을 보내고, 그 응답에 또 반응하는 무한 루프에 빠진 것입니다. 박시니어 씨가 웃으며 설명했습니다.
"그래서 msg.key.fromMe 체크가 필요한 거예요." 메시지 수신 처리의 핵심은 어떤 메시지에 반응하고 어떤 메시지를 무시할지 결정하는 것입니다. messages.upsert 이벤트는 새로운 메시지가 도착할 때마다 발생합니다.
마치 우체통에 새 편지가 도착하면 알림을 받는 것과 같습니다. 하지만 우체통에는 내가 보낸 편지도 들어있고, 스팸 메일도 있고, 광고 전단지도 있습니다.
우리는 그중에서 정말 필요한 메시지만 골라내야 합니다. 첫 번째 필터는 !msg.message입니다.
때로는 메시지 객체가 비어있는 경우가 있습니다. 예를 들어 상대방이 메시지를 삭제했을 때의 알림 같은 것이죠.
두 번째 필터는 msg.key.fromMe입니다. 이것이 true면 내가 보낸 메시지입니다.
봇이 자기 자신의 메시지에 반응하면 무한 루프가 발생하므로 반드시 걸러내야 합니다. 세 번째 필터는 메시지 타입입니다.
msg.message.conversation은 일반 텍스트 메시지입니다. msg.message.extendedTextMessage는 링크 프리뷰나 답장이 포함된 메시지입니다.
이미지, 비디오, 문서 등은 각각 다른 속성에 들어있습니다. 김개발 씨가 전화번호 형식에 대해 질문했습니다.
"왜 전화번호 뒤에 @s.whatsapp.net을 붙이나요?" 박시니어 씨가 설명했습니다. "WhatsApp은 내부적으로 XMPP라는 메시징 프로토콜을 사용해요.
이메일 주소처럼 사용자를 식별하는 형식이죠." 전화번호 형식은 반드시 지켜야 합니다. 한국 전화번호 010-1234-5678은 국가코드를 포함하여 "8210XXXXXXXX"로 변환됩니다.
맨 앞의 0을 빼고 82를 붙이는 것이죠. 그다음 "@s.whatsapp.net"을 붙이면 완성입니다.
그룹 채팅방은 "@g.us"를 사용합니다. 메시지 전송은 sendMessage 함수 하나로 해결됩니다.
첫 번째 인자는 수신자 ID이고, 두 번째 인자는 메시지 내용입니다. { text: '내용' } 형식으로 전달하면 텍스트 메시지가 전송됩니다.
이미지를 보내려면 { image: { url: '이미지URL' }, caption: '설명' }처럼 사용합니다. 실무에서는 어떻게 활용할까요?
고객 문의 자동 응답 시스템을 예로 들어보겠습니다. 고객이 "영업시간"이라는 키워드를 보내면 "평일 9시~6시 운영합니다"라고 자동 응답합니다.
"주문조회"를 보내면 데이터베이스에서 주문 정보를 조회하여 전송합니다. "상담원"을 보내면 실제 상담원에게 알림을 보내 직접 응대하게 할 수도 있습니다.
키워드 매칭은 간단하지만 강력합니다. messageText.toLowerCase().includes('키워드') 방식으로 대소문자를 구분하지 않고 키워드를 찾을 수 있습니다.
초보 개발자들이 자주 하는 실수가 있습니다. 비동기 처리를 제대로 하지 않는 것입니다.
sendMessage는 Promise를 반환하므로 반드시 await를 사용해야 합니다. 그렇지 않으면 메시지가 전송되기도 전에 다음 코드가 실행되어 예상치 못한 동작이 발생할 수 있습니다.
또 다른 실수는 에러 처리를 하지 않는 것입니다. 네트워크가 불안정하거나 상대방이 나를 차단한 경우 메시지 전송이 실패할 수 있습니다.
try-catch로 감싸서 에러를 적절히 처리해야 합니다. 김개발 씨가 테스트 메시지를 자기 자신에게 보내보았습니다.
"와, 정말 되네요! 1초도 안 걸렸어요!" 박시니어 씨가 만족스러운 표정으로 말했습니다.
"WhatsApp은 WebSocket으로 실시간 통신하니까 아주 빠르죠. 이제 이미지나 파일도 보낼 수 있어요." 메시지 송수신의 기본을 마스터하면, 더 복잡한 챗봇과 자동화 시스템을 구축할 수 있는 기반이 마련됩니다.
실전 팁
💡 - 반드시 msg.key.fromMe 체크로 자신의 메시지는 무시하세요 (무한 루프 방지)
- 전화번호는 항상 국가코드 포함 형식으로 (한국: 82로 시작)
- 메시지 전송은 try-catch로 감싸서 에러 처리를 잊지 마세요
5. 미디어 파일 처리
텍스트 메시지를 마스터한 김개발 씨는 이제 더 복잡한 도전에 나섰습니다. "이미지나 PDF 파일도 보낼 수 있나요?" 박시니어 씨가 화면에 코드를 띄우며 답했습니다.
"물론이죠. Baileys는 이미지, 비디오, 오디오, 문서 모두 지원해요.
파일 다운로드도 가능하고요."
미디어 파일 처리는 WhatsApp 봇의 활용도를 크게 높여줍니다. 이미지, 비디오, 오디오, 문서를 URL이나 로컬 파일에서 읽어 전송할 수 있고, 수신한 미디어는 downloadMediaMessage 함수로 다운로드하여 처리할 수 있습니다.
파일 크기와 형식 제한을 준수해야 합니다.
다음 코드를 살펴봅시다.
const { default: makeWASocket, useMultiFileAuthState, downloadMediaMessage } = require('@whiskeysockets/baileys');
const fs = require('fs');
async function handleMediaMessages() {
const { state } = await useMultiFileAuthState('./auth_info');
const sock = makeWASocket({ auth: state });
// 이미지 전송 (URL 또는 로컬 파일)
await sock.sendMessage('8210XXXXXXXX@s.whatsapp.net', {
image: { url: './photo.jpg' }, // 또는 원격 URL
caption: '첨부된 이미지입니다'
});
// PDF 문서 전송
await sock.sendMessage('8210XXXXXXXX@s.whatsapp.net', {
document: { url: './document.pdf' },
mimetype: 'application/pdf',
fileName: '견적서.pdf'
});
// 미디어 수신 처리
sock.ev.on('messages.upsert', async (m) => {
const msg = m.messages[0];
if (!msg.message) return;
// 이미지 메시지 수신
if (msg.message.imageMessage) {
console.log('📸 이미지를 받았습니다');
const buffer = await downloadMediaMessage(msg, 'buffer', {});
fs.writeFileSync('./received_image.jpg', buffer);
console.log('✅ 이미지 저장 완료');
}
});
}
김개발 씨는 고객에게 제품 카탈로그를 자동으로 전송하는 기능을 구현하고 싶었습니다. "텍스트만으로는 부족해요.
제품 사진과 PDF 안내서를 함께 보내야 하는데요." 박시니어 씨가 고개를 끄덕였습니다. "실무에서는 미디어 파일이 정말 중요하죠.
영수증, 계약서, 제품 이미지 같은 것들 말이에요." 미디어 파일 전송은 텍스트 메시지보다 조금 복잡하지만, 개념은 비슷합니다. 마치 편지에 사진을 동봉하는 것과 같습니다.
편지 봉투에 사진을 넣고, "이 봉투에는 사진이 들어있어요"라고 표시하는 것이죠. Baileys에서는 메시지 객체에 파일 정보와 함께 미디어 타입을 명시합니다.
이미지를 전송할 때는 image 속성을 사용합니다. { url: './photo.jpg' } 형식으로 로컬 파일 경로를 지정할 수 있습니다.
또는 { url: 'https://example.com/image.jpg' } 형식으로 원격 URL을 사용할 수도 있습니다. Baileys가 자동으로 파일을 읽거나 다운로드하여 WhatsApp 서버로 업로드합니다.
caption 속성은 이미지 아래에 표시될 설명 텍스트입니다. 인스타그램 포스트의 캡션과 같은 역할이죠.
문서 파일(PDF, DOCX, XLSX 등)을 전송할 때는 조금 다릅니다. document 속성을 사용하며, mimetype과 fileName을 반드시 지정해야 합니다.
mimetype은 파일의 종류를 나타냅니다. PDF는 "application/pdf", Word 문서는 "application/vnd.openxmlformats-officedocument.wordprocessingml.document"입니다.
fileName은 수신자가 다운로드할 때 보이는 파일 이름입니다. 한글 파일명도 지원되므로 "견적서.pdf"처럼 의미 있는 이름을 사용할 수 있습니다.
김개발 씨가 궁금해했습니다. "파일 크기 제한은 없나요?" 박시니어 씨가 중요한 포인트를 짚어주었습니다.
"WhatsApp은 미디어 타입별로 제한이 있어요. 이미지는 5MB, 비디오는 16MB, 문서는 100MB까지입니다." 미디어 수신은 전송보다 조금 더 복잡합니다.
받은 메시지에 이미지가 포함되어 있는지 확인하려면 msg.message.imageMessage를 체크합니다. 비디오는 videoMessage, 문서는 documentMessage, 오디오는 audioMessage입니다.
실제 파일 데이터는 WhatsApp 서버에 암호화되어 저장되어 있습니다. downloadMediaMessage 함수를 사용하면 서버에서 파일을 다운로드하고 복호화하여 Buffer 형태로 반환합니다.
받은 Buffer를 fs.writeFileSync로 저장하면 로컬 파일이 생성됩니다. 또는 Buffer를 직접 처리하여 이미지 리사이징, OCR, AI 분석 등 다양한 작업을 수행할 수 있습니다.
실무 활용 사례를 살펴보겠습니다. 부동산 중개 서비스를 예로 들어봅시다.
고객이 "강남 아파트"라고 메시지를 보내면, 봇이 데이터베이스에서 관련 매물을 검색합니다. 그리고 매물 사진, 위치 지도 이미지, 상세 정보 PDF를 차례로 전송합니다.
고객은 WhatsApp 하나로 모든 정보를 받아볼 수 있습니다. 또 다른 예시로는 영수증 자동 발송 시스템이 있습니다.
결제가 완료되면 영수증 PDF를 생성하여 고객의 WhatsApp으로 전송합니다. 이메일보다 도달률이 높고, 고객도 바로 확인할 수 있어 만족도가 높습니다.
초보 개발자들이 자주 하는 실수가 있습니다. 첫 번째는 파일 경로 오류입니다.
"./photo.jpg"는 현재 작업 디렉토리 기준이므로, Node.js를 실행하는 위치에 따라 파일을 찾지 못할 수 있습니다. path.join(__dirname, 'photo.jpg') 형식으로 절대 경로를 사용하는 것이 안전합니다.
두 번째는 mimetype 오타입니다. "application/pdf"를 "application/pDF"처럼 잘못 입력하면 파일이 제대로 표시되지 않습니다.
정확한 mimetype을 사용해야 합니다. 세 번째는 메모리 관리입니다.
큰 파일을 다운로드하면 Buffer가 메모리를 많이 차지합니다. 파일 처리 후에는 적절히 메모리를 해제하고, 스트림 방식으로 처리하는 것이 좋습니다.
김개발 씨가 테스트로 고양이 사진을 전송했습니다. "와, 몇 초 만에 전송됐어요!
생각보다 빠르네요!" 박시니어 씨가 웃으며 말했습니다. "Baileys가 파일을 자동으로 압축하고 최적화해주거든요.
개발자는 파일만 지정하면 나머지는 라이브러리가 알아서 처리합니다." 미디어 파일 처리를 마스터하면, 단순한 텍스트 봇이 아닌 실용적인 서비스를 구축할 수 있습니다. 이제 마지막으로 실전 봇 구현 예제를 살펴보겠습니다.
실전 팁
💡 - 파일 경로는 path.join(__dirname, 'file.jpg')로 절대 경로 사용을 권장합니다
- 큰 파일은 메모리 문제를 일으킬 수 있으니 파일 크기를 미리 체크하세요
- mimetype은 정확히 입력해야 파일이 제대로 표시됩니다 (오타 주의)
6. 실전 WhatsApp 봇 구현하기
모든 기본기를 배운 김개발 씨는 이제 실전 프로젝트를 시작할 준비가 되었습니다. "고객 문의에 자동으로 응답하고, 주문 상태를 조회할 수 있는 봇을 만들어볼게요." 박시니어 씨가 격려했습니다.
"좋아요. 지금까지 배운 것을 모두 활용하는 거죠.
명령어 처리, 상태 관리, 에러 처리까지 포함해봅시다."
실전 WhatsApp 봇은 명령어 파싱, 상태 관리, 데이터베이스 연동, 에러 처리를 모두 포함합니다. 사용자 입력을 분석하여 적절한 응답을 제공하고, 세션을 관리하며, 예외 상황을 안전하게 처리하는 완전한 시스템입니다.
확장 가능한 구조로 설계하면 새로운 기능을 쉽게 추가할 수 있습니다.
다음 코드를 살펴봅시다.
const { default: makeWASocket, useMultiFileAuthState, DisconnectReason } = require('@whiskeysockets/baileys');
const fs = require('fs');
// 사용자 세션 관리 (간단한 메모리 저장)
const userSessions = new Map();
async function startBot() {
const { state, saveCreds } = await useMultiFileAuthState('./auth_info');
const sock = makeWASocket({ auth: state, printQRInTerminal: true });
sock.ev.on('creds.update', saveCreds);
// 연결 관리
sock.ev.on('connection.update', async (update) => {
const { connection, lastDisconnect } = update;
if (connection === 'close') {
const shouldReconnect = lastDisconnect?.error?.output?.statusCode !== DisconnectReason.loggedOut;
if (shouldReconnect) startBot(); // 자동 재연결
} else if (connection === 'open') {
console.log('✅ 봇이 시작되었습니다');
}
});
// 메시지 처리
sock.ev.on('messages.upsert', async (m) => {
try {
const msg = m.messages[0];
if (!msg.message || msg.key.fromMe) return;
const text = msg.message.conversation || msg.message.extendedTextMessage?.text || '';
const sender = msg.key.remoteJid;
// 명령어 파싱
const command = text.toLowerCase().trim();
if (command === '/시작' || command === '/start') {
await sock.sendMessage(sender, {
text: '안녕하세요! 👋\n\n사용 가능한 명령어:\n/주문조회 - 주문 상태 확인\n/영업시간 - 운영 시간 안내\n/상담원 - 상담원 연결'
});
} else if (command === '/주문조회') {
userSessions.set(sender, 'waiting_order_number');
await sock.sendMessage(sender, { text: '주문번호를 입력해주세요 (예: ORD-12345)' });
} else if (command === '/영업시간') {
await sock.sendMessage(sender, { text: '📅 영업시간: 평일 09:00 - 18:00\n주말 및 공휴일 휴무' });
} else if (command === '/상담원') {
await sock.sendMessage(sender, { text: '상담원 연결 요청이 접수되었습니다.\n잠시만 기다려주세요.' });
// 실제로는 관리자에게 알림 전송
} else if (userSessions.get(sender) === 'waiting_order_number') {
// 주문번호 입력 받은 경우
userSessions.delete(sender);
await sock.sendMessage(sender, {
text: `🔍 주문번호 ${text} 조회 중...\n\n배송 상태: 배송 준비중\n예상 도착: 2일 이내`
});
} else {
await sock.sendMessage(sender, { text: '죄송합니다. 이해하지 못했어요.\n/시작 을 입력하여 도움말을 확인하세요.' });
}
} catch (error) {
console.error('메시지 처리 에러:', error);
}
});
}
startBot();
김개발 씨는 드디어 실전 봇을 만들기 시작했습니다. 처음에는 간단할 줄 알았는데, 생각해야 할 것이 많았습니다.
"사용자가 주문번호를 물어본다면, 어떻게 대화의 문맥을 기억하지?" 박시니어 씨가 설명했습니다. "그게 바로 상태 관리입니다.
사람과 대화할 때도 이전 대화를 기억하잖아요? 봇도 마찬가지예요." 실전 봇의 핵심 요소는 크게 네 가지입니다.
첫 번째는 명령어 시스템입니다. 사용자가 "/시작", "/주문조회" 같은 명령어를 입력하면 봇이 정해진 동작을 수행합니다.
마치 식당에서 메뉴판을 보고 주문하는 것과 같습니다. 명령어를 명확하게 정의하면 사용자도 봇을 쉽게 사용할 수 있습니다.
명령어는 command.toLowerCase().trim()으로 정규화합니다. 대소문자를 구분하지 않고, 앞뒤 공백도 제거합니다.
사용자가 "/시작 " 또는 "/시작"을 입력해도 동일하게 처리됩니다. 두 번째는 상태 관리입니다.
사용자가 "/주문조회"를 입력하면, 봇은 "주문번호를 입력해주세요"라고 답합니다. 그다음 사용자가 "ORD-12345"를 입력하면, 봇은 이것이 주문번호라는 것을 알아야 합니다.
어떻게 알까요? userSessions라는 Map 객체에 사용자별 상태를 저장합니다.
"/주문조회" 명령을 받으면 userSessions.set(sender, 'waiting_order_number')로 "이 사용자는 지금 주문번호를 입력하려고 한다"는 것을 기록합니다. 다음 메시지가 오면 userSessions.get(sender)로 상태를 확인하고, "주문번호 대기 중"이면 입력받은 텍스트를 주문번호로 처리합니다.
처리가 끝나면 userSessions.delete(sender)로 상태를 초기화합니다. 세 번째는 자동 재연결입니다.
서버가 재시작되거나 네트워크가 끊어질 수 있습니다. 그때마다 수동으로 재연결하면 운영이 불가능합니다.
connection.update 이벤트에서 연결이 끊어지면 자동으로 startBot()을 재호출하여 다시 연결합니다. 단, DisconnectReason.loggedOut인 경우는 재연결하지 않습니다.
이것은 사용자가 의도적으로 로그아웃한 경우이므로, QR 코드를 새로 스캔해야 합니다. 네 번째는 에러 처리입니다.
메시지 처리 과정에서 예상치 못한 에러가 발생할 수 있습니다. 네트워크 오류, 잘못된 데이터 형식, 데이터베이스 연결 실패 등이죠.
전체 코드를 try-catch로 감싸서 에러가 발생해도 봇이 중단되지 않도록 합니다. 에러가 발생하면 콘솔에 로그를 남기고, 사용자에게는 "일시적인 오류가 발생했습니다"라는 친절한 메시지를 보냅니다.
실제 서비스에서는 어떻게 확장할까요? 김개발 씨의 쇼핑몰 봇을 예로 들어보겠습니다.
사용자가 "/주문조회"를 입력하면, 봇은 실제 데이터베이스에 접속하여 주문 정보를 조회합니다. MySQL이나 MongoDB를 연동하여 실시간 데이터를 제공할 수 있습니다.
또한 관리자 대시보드를 만들 수 있습니다. 사용자가 "/상담원"을 입력하면, 웹 대시보드에 알림을 띄워 실제 직원이 개입할 수 있게 합니다.
Socket.IO를 사용하면 실시간 알림이 가능합니다. 더 나아가 AI를 통합할 수도 있습니다.
사용자의 질문을 ChatGPT API로 전달하여 자연스러운 대화가 가능한 봇을 만들 수 있습니다. 단순한 명령어 봇이 아닌, 진짜 대화형 AI 봇이 되는 것이죠.
초보 개발자들이 자주 하는 실수를 정리해봅시다. 첫 번째는 메모리 상태 관리입니다.
위 예제는 Map을 사용했지만, 서버가 재시작되면 모든 상태가 사라집니다. 실제 서비스에서는 Redis 같은 영구 저장소를 사용해야 합니다.
두 번째는 보안입니다. 사용자가 "/관리자 명령"을 입력했을 때, 권한이 없는 사람은 차단해야 합니다.
전화번호를 화이트리스트로 관리하여 관리자만 특정 기능을 사용하게 할 수 있습니다. 세 번째는 속도 제한입니다.
사용자가 1초에 100개의 메시지를 보내면 봇이 과부하에 걸립니다. 사용자별로 요청 횟수를 제한하는 Rate Limiting을 구현해야 합니다.
김개발 씨는 첫 번째 실전 봇을 완성했습니다. "와, 정말로 작동하네요!
친구에게 테스트해달라고 부탁해야겠어요." 박시니어 씨가 축하했습니다. "축하해요!
이제 기본은 마스터했으니, 데이터베이스 연동, AI 통합, 결제 시스템 같은 고급 기능을 추가해보세요." 실전 봇 구현을 완료하면, WhatsApp을 활용한 무궁무진한 서비스를 만들 수 있습니다. 단순한 자동 응답부터 복잡한 비즈니스 로직까지, 모든 것이 가능합니다.
실전 팁
💡 - 상태 관리는 프로덕션에서 Redis 같은 영구 저장소를 사용하세요
- 관리자 명령어는 전화번호 화이트리스트로 보호하세요
- Rate Limiting을 구현하여 스팸 요청을 차단하세요
- 에러 로그는 파일이나 외부 로깅 서비스(Sentry 등)로 기록하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
모델 Failover 및 프로바이더 관리 완벽 가이드
AI 서비스 개발 시 필수적인 모델 장애 대응과 다중 프로바이더 관리 전략을 다룹니다. Anthropic과 OpenAI를 통합하고, 안정적인 failover 시스템을 구축하는 방법을 실무 코드와 함께 설명합니다.
macOS iOS Android 앱 통합 완벽 가이드
하나의 데스크톱 앱을 macOS, iOS, Android 모바일 앱과 연동하는 방법을 배웁니다. WebSocket 프로토콜 설계부터 Bonjour 자동 페어링까지, 크로스 플랫폼 앱 통합의 모든 것을 다룹니다.
Cron과 Webhooks 완벽 가이드
Node.js 환경에서 자동화의 핵심인 Cron 작업과 Webhooks를 활용하는 방법을 다룹니다. 정기적인 작업 스케줄링부터 외부 서비스 연동까지, 실무에서 바로 적용할 수 있는 자동화 기법을 배워봅니다.
보안 모델 및 DM Pairing 완벽 가이드
Discord 봇의 DM 보안 정책과 페어링 시스템을 체계적으로 학습합니다. dmPolicy 설정부터 allowlist 관리, 페어링 코드 구현까지 안전한 봇 운영의 모든 것을 다룹니다.
Media Pipeline 완벽 가이드
실무에서 자주 사용하는 미디어 파일 처리 파이프라인을 처음부터 끝까지 배웁니다. 이미지 리사이징, 오디오 변환, 임시 파일 관리까지 Node.js로 구현하는 방법을 초급 개발자도 이해할 수 있도록 쉽게 설명합니다.