
첫 번째 시행착오: Heroku 배포 실패
문제 상황
로컬에서는 완벽하게 작동하던 봇이 Heroku에 배포하자 다음과 같은 오류가 발생했습니다:
Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds
State changed from starting to crashed
문제 분석 과정
이 오류 메시지를 분석해보면:
Boot timeout: 애플리케이션이 지정된 시간 내에 시작되지 않음failed to bind to $PORT: HTTP 포트에 바인딩하지 못함Web process: Heroku가 이 앱을 웹 애플리케이션으로 인식함
근본 원인 이해
Heroku는 기본적으로 모든 애플리케이션을 웹 애플리케이션으로 간주합니다. 웹 애플리케이션은 HTTP 요청을 받기 위해 특정 포트에서 서버를 실행해야 하는데, 디스코드 봇은 HTTP 서버가 아니라 WebSocket 클라이언트이므로 포트 바인딩이 불필요합니다.
해결 과정
1. Procfile 생성
프로젝트 루트에 Procfile 파일 생성:
worker: node index.js
이는 Heroku에게 "이 앱은 웹 서버가 아니라 백그라운드 워커입니다"라고 알려주는 역할입니다.
2. Dyno 타입 변경
heroku ps:scale web=0 worker=1
이 명령어는 웹 dyno를 0개로, 워커 dyno를 1개로 설정합니다.
학습 포인트
- 클라우드 플랫폼은 애플리케이션 타입에 따라 다른 실행 방식을 가짐
Procfile은 Heroku에게 앱을 어떻게 실행할지 알려주는 설정 파일- 웹 애플리케이션과 백그라운드 서비스의 차이점 이해
두 번째 시행착오: 주차 계산 오류
문제 발견
!test 명령어를 실행했을 때 "테스트 포스트 생성 완료" 메시지는 나타나지만 실제 포스트는 생성되지 않았습니다.
디버깅 전략
문제를 찾기 위해 createWeeklyPost 함수에 단계별 로깅을 추가했습니다:
async function createWeeklyPost() {
try {
console.log('🚀 createWeeklyPost() 시작');
const channel = client.channels.cache.get(process.env.FORUM_CHANNEL_ID);
console.log('📡 Channel 찾기:', channel ? '성공' : '실패');
console.log('📋 Channel 타입:', channel?.type, 'vs', ChannelType.GuildForum);
const weekInfo = getWeekInfo();
console.log('📅 Week Info:', weekInfo);
const imageIndex = (weekInfo.week - 7) % weeklyImages.length;
const imageName = weeklyImages[imageIndex];
console.log('🖼️ Image Info:', { imageIndex, imageName });
// ... 나머지 코드
} catch (error) {
console.error('❌ Failed to create weekly post:', error);
console.error('Error stack:', error.stack);
}
}
문제 발견
로그 결과:
📅 Week Info: { week: 6, period: '08.25 ~ 08.31' }
🖼️ Image Info: { imageIndex: -1, imageName: undefined }
분석: 8월 31일 기준으로 계산했을 때 6주차가 나왔고, 이는 7주차부터 시작하는 이미지 배열에서 인덱스 -1을 만들어 undefined를 반환했습니다.
해결책 구현
function getWeekInfo(date = new Date()) {
const week7StartDate = new Date(2025, 8, 1);
const diffTime = date.getTime() - week7StartDate.getTime();
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
// 핵심 수정: 음수 처리
let weekNumber = diffDays < 0 ? 7 : Math.floor(diffDays / 7) + 7;
// ... 나머지 로직
}
학습 포인트
- 엣지 케이스 처리의 중요성: 시작일 이전 날짜에 대한 방어 로직
- 체계적 디버깅: 단계별 로깅을 통한 문제 지점 정확한 파악
- 배열 인덱스 계산: 상대적 인덱스 계산 시 음수 처리 필요
세 번째 시행착오: 환경변수와 권한 문제
문제 상황 1: 명령어 미인식
!weekinfo를 입력해도 봇이 반응하지 않았습니다.
진단 과정
client.on('messageCreate', message => {
// 모든 메시지 수신 여부 확인
console.log(`메시지 수신: "${message.content}" from ${message.author.id}`);
console.log(`환경변수 ADMIN_USER_ID: ${process.env.ADMIN_USER_ID}`);
console.log(`일치 여부: ${message.author.id === process.env.ADMIN_USER_ID}`);
// ... 명령어 처리
});
발견된 문제들
- 인텐트 누락:
MessageContent인텐트가 없어 메시지 내용을 읽을 수 없음 - 환경변수 불일치: 로컬
.env와 Heroku Config Vars 간 동기화 문제 - 환경 조건:
NODE_ENV=production일 때 테스트 명령어 비활성화
해결 과정
1. 인텐트 추가
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent // 이 줄 추가!
]
});
2. 환경변수 동기화
Heroku Dashboard → Settings → Config Vars에서 모든 환경변수 등록
3. 환경 조건 제거
// 변경 전: 개발환경에서만 작동
if (process.env.NODE_ENV === 'development') {
client.on('messageCreate', message => { ... });
}
// 변경 후: 모든 환경에서 작동
client.on('messageCreate', message => { ... });
네 번째 시행착오: Discord.js 버전 호환성
경고 메시지 분석
DeprecationWarning: The ready event has been renamed to clientReady
혼란스러웠던 부분
이 경고 메시지 때문에 ready 이벤트가 잘못된 것으로 오해했습니다. 실제로는 최신 Discord.js v14에서 clientReady가 권장되는 이벤트명이었습니다.
올바른 이해
ready이벤트: 여전히 작동하지만 향후 버전에서 제거 예정clientReady이벤트: 새로운 표준, Gateway READY 이벤트와 구분
// 권장 방식
client.once('clientReady', readyClient => {
console.log(`${readyClient.user.tag} 봇이 준비되었습니다!`);
});
에러 패턴별 해결 전략
1. TokenInvalid 에러
Error [TokenInvalid]: An invalid token was provided.
원인: 잘못된 봇 토큰 또는 환경변수 로드 실패
해결:
- Discord Developer Portal에서 토큰 재발급
.env파일 또는 Config Vars 정확성 확인console.log(process.env.DISCORD_TOKEN)으로 로드 여부 확인
2. Missing Permissions 에러
원인: 봇에게 필요한 권한이 부족
해결:
- Discord 서버에서 봇 역할 권한 확인
- 채널별 권한 설정 점검
- OAuth2 URL 재생성하여 권한 추가
3. Channel Not Found 에러
원인: 잘못된 채널 ID 또는 봇이 해당 채널에 접근 권한 없음
해결:
- 개발자 모드에서 채널 ID 재확인
- 봇이 해당 서버와 채널에 참여되어 있는지 확인
- 채널 타입이 예상과 일치하는지 검증
개발 과정에서 배운 핵심 개념들
1. 비동기 프로그래밍 패턴
Promise와 async/await 이해:
디스코드 API 호출은 모두 비동기입니다. 순차적 실행이 필요한 작업(스레드 생성 → 리액션 추가)에서는 적절한 await 사용이 중요합니다.
// 잘못된 방식: 리액션이 동시에 실행되어 Rate Limit 발생 가능
reactions.forEach(emoji => {
thread.lastMessage?.react(emoji);
});
// 올바른 방식: 순차 실행으로 안정성 확보
for (const emoji of reactions) {
await thread.lastMessage?.react(emoji);
await new Promise(resolve => setTimeout(resolve, 500));
}
2. 에러 핸들링 전략
계층별 에러 처리:
- 함수 레벨: try-catch로 예외 상황 포착
- 애플리케이션 레벨: process 이벤트로 전역 에러 처리
- 사용자 레벨: 명확한 에러 메시지 제공
// 함수 레벨 에러 처리
async function createWeeklyPost() {
try {
// ... 주요 로직
} catch (error) {
console.error('포스트 생성 실패:', error.message);
console.error('상세 스택:', error.stack);
}
}
// 전역 에러 처리
process.on('unhandledRejection', error => {
console.error('처리되지 않은 Promise 거부:', error);
});
client.on('error', error => {
console.error('Discord.js 에러:', error);
});
3. 디버깅 방법론
체계적 로깅 전략:
// 상태 추적용 로그
console.log('🚀 함수 시작');
console.log('📡 채널 상태:', channel ? '정상' : '오류');
console.log('📅 계산된 주차:', weekInfo);
// 조건문 분기 추적
if (condition) {
console.log('✅ 조건 통과');
} else {
console.log('❌ 조건 실패:', reason);
}
문제 해결 사고 과정의 변화
초기 접근법 (비효율적)
- 에러 메시지만 보고 추측
- 여러 수정사항을 동시에 적용
- 변경 효과를 정확히 파악하지 못함
개선된 접근법 (체계적)
- 문제 격리: 하나의 이슈에만 집중
- 단계별 검증: 각 수정사항의 효과를 개별적으로 확인
- 로그 기반 진단: 추측이 아닌 실제 데이터로 판단
- 점진적 해결: 작은 문제부터 해결하여 복잡도 관리
실제 적용 사례
문제: 포스트 생성 실패
1. 함수 진입 여부 확인 → ✅ 진입함
2. 채널 찾기 성공 여부 → ✅ 성공
3. 주차 계산 결과 → ❌ 6주차 (음수 문제)
4. 이미지 인덱스 계산 → ❌ -1 (undefined 발생)
5. 근본 원인: 날짜 계산 로직 오류