본문으로 건너뛰기

Hugo 블로그 구축기 7편 - Invalid Token 지옥 탈출기

·1215 단어수·6 분
Hugo 블로그 구축기 - 이 글은 시리즈의 일부입니다.
부분 : 이 글

😤 또 다른 지옥의 시작…
#

안녕하세요! Hugo 블로그 구축기 7편으로 돌아온 CoderRed입니다 😅

6편에서 텔레그램 알림 시스템을 구축했잖아요? 드디어 실시간으로 댓글 알림을 받을 수 있게 되었어요! 🎉

그런데… 여기서 끝이 아니었습니다. 😭

텔레그램으로 알림은 잘 오는데, ⚡ 댓글 승인 버튼을 누르면 계속 이런 에러가 떴어요:

Invalid Token

“뭐지? 방금 생성된 토큰인데 왜 무효라는 거야?” 🤔

결국 4시간의 지옥같은 디버깅 끝에 해결했는데… 정말 예상치 못한 곳에 함정들이 숨어있더라고요.

오늘은 그 4시간 삽질의 전 과정을 솔직하게 공유해드릴게요!

🔥 1단계: JWT_SECRET 의심 (첫 번째 함정)
#

처음엔 당연히 JWT 설정 문제라고 생각했어요.

“아, 컨테이너 재시작하면서 JWT_SECRET이 바뀌었나?”

시도 1: JWT_SECRET 재설정
#

docker stop cusdis && docker rm cusdis

docker run -d \
  --name cusdis \
  --restart unless-stopped \
  --network npm_default \
  -p 3000:3000 \
  -e USERNAME="admin" \
  -e PASSWORD="비밀번호" \
  -e JWT_SECRET="ofcourseistillloveyou" \
  -e DB_URL="file:/data/db.sqlite" \
  -e NEXTAUTH_URL="https://comments.coderred.com" \
  -v ~/cusdis-data:/data \
  djyde/cusdis:latest

브라우저 쿠키도 모두 삭제하고, 다시 로그인하고…

결과: 여전히 Invalid Token 😤

🔥 2단계: NextAuth.js의 진짜 함정 발견
#

Cusdis 로그를 자세히 보니 이런 에러가 떴어요:

https://next-auth.js.org/errors#jwt_session_error 
JWSVerificationFailed: signature verification failed

이때 깨달았어요. NextAuth.js는 JWT_SECRET과 별도로 NEXTAUTH_SECRET을 찾을 수 있다는 것!

시도 2: NEXTAUTH_SECRET 추가
#

docker stop cusdis && docker rm cusdis

docker run -d \
  --name cusdis \
  --restart unless-stopped \
  --network npm_default \
  -p 3000:3000 \
  -e USERNAME="admin" \
  -e PASSWORD="비밀번호" \
  -e JWT_SECRET="ofcourseistillloveyou" \
  -e NEXTAUTH_SECRET="ofcourseistillloveyou" \
  -e DB_URL="file:/data/db.sqlite" \
  -e NEXTAUTH_URL="https://comments.coderred.com" \
  -v ~/cusdis-data:/data \
  djyde/cusdis:latest

핵심: JWT_SECRET과 NEXTAUTH_SECRET을 똑같은 값으로 설정!

브라우저 쿠키 삭제, 다시 로그인, 새 댓글 달기…

결과: 여전히 Invalid Token 😭😭

🔥 3단계: 충격적인 진실 - 엉뚱한 서버로!
#

그때 텔레그램에서 받은 승인 링크를 자세히 봤어요:

https://cusdis.com/open/approve?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

어?! 이거 cusdis.com으로 가고 있네? 😱

우리 서버 https://comments.coderred.com이 아니라 공식 Cusdis 서비스로 가고 있었던 거예요!

당연히 우리 서버에서 생성한 토큰을 cusdis.com에서 검증할 수 없죠. 이게 Invalid Token의 진짜 원인이었어요!

시도 3: CUSDIS_BASE_URL 추가
#

“이건 Base URL 설정 문제구나!”

docker stop cusdis && docker rm cusdis

docker run -d \
  --name cusdis \
  --restart unless-stopped \
  --network npm_default \
  -p 3000:3000 \
  -e USERNAME="admin" \
  -e PASSWORD="비밀번호" \
  -e JWT_SECRET="ofcourseistillloveyou" \
  -e NEXTAUTH_SECRET="ofcourseistillloveyou" \
  -e DB_URL="file:/data/db.sqlite" \
  -e NEXTAUTH_URL="https://comments.coderred.com" \
  -e CUSDIS_BASE_URL="https://comments.coderred.com" \
  -v ~/cusdis-data:/data \
  djyde/cusdis:latest

새 댓글 달아서 테스트…

결과: 여전히 https://cusdis.com/open/approve로 감! 😤😤😤

🔥 4단계: 웹훅에서 URL 강제 교체
#

“그럼 웹훅에서 직접 바꿔버리자!”

webhook-telegram.js 수정
#

if (type === 'new_comment') {
  const { approve_link, ...otherData } = data;
  
  // 🔥 강제로 URL 교체!
  const fixedApproveLink = approve_link.replace(
    'https://cusdis.com/open/approve',
    'https://comments.coderred.com/api/approve'  // /api/approve로 시도
  );
  
  const message = `🔔 새 댓글 알림!
...
⚡ [댓글 승인](${fixedApproveLink})
...`;
}

웹훅 컨테이너 재빌드하고 테스트…

결과: 이제 https://comments.coderred.com/api/approve로 가는데… 404 Not Found 😤😤😤😤

🔥 5단계: API 경로의 미스터리
#

“404라니? API 경로가 틀렸나?”

디버깅 시작
#

# Cusdis 로그 실시간 확인
docker logs cusdis --follow

승인 링크를 클릭했는데… 로그에 아무것도 안 뜸! 😱

이건 요청이 Cusdis까지 도달하지 않고 있다는 뜻이에요!

NPM 로그 확인
#

# NPM 로그 확인
docker logs npm-app-1 --follow

여기서도 404 에러… 그럼 NPM에서 라우팅이 안 되고 있다는 뜻!

다른 경로들 시도
#

혹시 API 경로가 다를 수도 있다고 생각해서:

// webhook-telegram.js에서 여러 경로 시도
const possiblePaths = [
  'https://comments.coderred.com/api/approve',      // 시도 1
  'https://comments.coderred.com/open/approve',     // 시도 2  
  'https://comments.coderred.com/approve',          // 시도 3
  'https://comments.coderred.com/api/comments/approve' // 시도 4
];

하나씩 테스트해보니…

🎉 6단계: 드디어 정답 발견!
#

https://comments.coderred.com/open/approve?token=...

바로 이거였어요! /open/approve가 정답이었습니다! 🎉

최종 웹훅 코드
#

const express = require('express');
const axios = require('axios');
const app = express();

app.use(express.json());

const BOT_TOKEN = process.env.BOT_TOKEN;
const CHAT_ID = process.env.CHAT_ID;

app.post('/cusdis-telegram', async (req, res) => {
  try {
    const { type, data } = req.body;
    
    if (type === 'new_comment') {
      const {
        by_nickname,
        by_email,
        content,
        page_title,
        page_id,
        approve_link
      } = data;
      
      // 🎯 최종 해결책!
      const fixedApproveLink = approve_link.replace(
        'https://cusdis.com/open/approve',
        'https://comments.coderred.com/open/approve'  // 이게 정답!
      );
      
      const postUrl = `https://coderred.com/posts/${page_id}/`;
      
      const message = `🔔 새 댓글 알림!

📝 **${page_title}**
👤 작성자: ${by_nickname}${by_email ? ` (${by_email})` : ''}
💬 댓글: ${content}

🔗 [포스트 보기](${postUrl})
⚡ [댓글 승인](${fixedApproveLink})
🎛️ [관리 대시보드](https://comments.coderred.com)`;

      const telegramUrl = `https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`;
      
      await axios.post(telegramUrl, {
        chat_id: CHAT_ID,
        text: message,
        parse_mode: 'Markdown',
        disable_web_page_preview: false
      });
      
      console.log(`✅ 댓글 알림 전송 완료: ${by_nickname}${page_title}`);
    }
    
    res.status(200).send('OK');
  } catch (error) {
    console.error('❌ 텔레그램 알림 실패:', error);
    res.status(500).send('Error');
  }
});

app.listen(3001, '0.0.0.0', () => {
  console.log('🤖 텔레그램 웹훅 서버 실행: http://0.0.0.0:3001');
});

🛠️ 최종 시스템 구성
#

Cusdis 설정 (완전체)
#

docker run -d \
  --name cusdis \
  --restart unless-stopped \
  --network npm_default \
  -p 3000:3000 \
  -e USERNAME="admin" \
  -e PASSWORD="비밀번호" \
  -e JWT_SECRET="ofcourseistillloveyou" \
  -e NEXTAUTH_SECRET="ofcourseistillloveyou" \
  -e DB_URL="file:/data/db.sqlite" \
  -e NEXTAUTH_URL="https://comments.coderred.com" \
  -v ~/cusdis-data:/data \
  djyde/cusdis:latest

웹훅 컨테이너 재빌드
#

cd ~/telegram-webhook

# 컨테이너 재빌드
docker stop telegram-webhook && docker rm telegram-webhook
docker build -t telegram-webhook .
docker run -d \
  --name telegram-webhook \
  --restart unless-stopped \
  --network npm_default \
  -e BOT_TOKEN="봇토큰" \
  -e CHAT_ID="채팅ID" \
  telegram-webhook

🎯 드디어 성공!
#

이제 정말로 완벽한 시스템이 완성되었어요:

📝 포스트 작성 → 📤 Git 푸시 → 🚀 자동 배포
                    ↓
💬 독자 댓글 작성 → 📱 텔레그램 알림 → ⚡ 원클릭 승인 (성공!)

🧠 4시간 삽질에서 얻은 교훈들
#

기술적 교훈
#

  1. NextAuth.js의 복잡성

    • JWT_SECRET과 NEXTAUTH_SECRET을 모두 설정해야 할 수 있음
    • 둘 다 동일한 값으로 설정하는 것이 안전
  2. 셀프 호스팅의 함정

    • 공식 서비스 URL이 하드코딩되어 있을 수 있음
    • Base URL 환경변수가 모든 곳에 적용되지 않을 수 있음
  3. API 경로 추적의 중요성

    • /api/approve vs /open/approve 같은 미묘한 차이
    • 로그를 통한 실제 요청 경로 확인이 필수
  4. 디버깅 순서의 중요성

    • 브라우저 → NPM → 애플리케이션 순서로 단계별 확인

삽질 방지 교훈
#

  1. 가정하지 말기

    • “당연히 이럴 거야"라는 생각이 가장 위험
    • 로그가 안 나오는 시스템일 수도 있으니 다른 방법으로 확인
  2. 단계별 격리 테스트

    • 한 번에 여러 개를 바꾸지 말고 하나씩
    • 각 단계의 결과를 명확히 확인
  3. URL과 경로 주의깊게 관찰

    • 어디로 요청이 가는지 항상 확인
    • 브라우저 개발자 도구 Network 탭 활용
  4. 환경변수의 함정

    • 설정했다고 반드시 적용되는 건 아님
    • 실제 동작하는 방식을 코드로 확인

🔧 트러블슈팅 체크리스트
#

다음에 비슷한 문제가 생기면 이 순서로 확인하세요:

1단계: 기본 확인
#

# 컨테이너 상태
docker ps | grep -E "(cusdis|telegram)"

# 환경변수 확인
docker exec cusdis printenv | grep -E "(JWT|AUTH)"

2단계: 로그 확인
#

# 실시간 로그 모니터링
docker logs cusdis --follow
docker logs telegram-webhook --follow
docker logs npm-app-1 --follow

3단계: URL 추적
#

  • 텔레그램 알림에서 받은 링크 URL 확인
  • 브라우저 개발자 도구에서 실제 요청 경로 확인
  • NPM 설정에서 라우팅 규칙 확인

4단계: 단계별 테스트
#

# 직접 API 테스트
curl -I https://comments.coderred.com/open/approve
curl -I https://comments.coderred.com/api/approve

# 컨테이너 내부 테스트
docker exec cusdis curl -I localhost:3000/open/approve

5단계: 브라우저 초기화
#

  • 쿠키 완전 삭제
  • 시크릿/프라이빗 모드로 테스트
  • 다른 브라우저로 테스트

🚨 주요 에러 해결 가이드
#

“Invalid Token” 에러
#

  1. JWT_SECRET = NEXTAUTH_SECRET 확인
  2. 브라우저 쿠키 삭제
  3. 승인 링크 URL 도메인 확인 (cusdis.com vs comments.coderred.com)
  4. API 경로 확인 (/open/approve vs /api/approve)

“404 Not Found” 에러
#

  1. NPM 로그에서 라우팅 에러 확인
  2. API 경로가 실제 존재하는지 확인
  3. Cusdis 컨테이너까지 요청이 도달하는지 확인

텔레그램 알림은 오는데 승인이 안 됨
#

  1. 승인 링크 URL이 올바른 도메인인지 확인
  2. 웹훅에서 URL 교체 로직 확인
  3. 브라우저에서 링크 클릭 시 Network 탭 확인

🎉 마지막으로…
#

4시간의 삽질이었지만, 덕분에 정말 많은 것을 배웠어요:

  • NextAuth.js의 숨겨진 동작 방식
  • 셀프 호스팅 서비스의 함정들
  • 체계적인 디버깅 방법론
  • URL과 API 경로 추적의 중요성

이제 정말로 완전 자동화된 블로그 시스템이 완성되었어요!

앞으로는 이런 삽질 없이 오로지 글쓰기에만 집중할 수 있을 것 같아요 😊

혹시 비슷한 문제로 고생하시는 분들께 도움이 되었으면 좋겠어요. 댓글로 경험 공유해주세요! (이제 실시간 알림이 와서 바로 답변드릴 수 있어요 😄)

다음 편에서는 정말로 기술 콘텐츠나 투자 이야기로 돌아올 예정입니다! 🚀

Hugo 블로그 구축기 - 이 글은 시리즈의 일부입니다.
부분 : 이 글

💬 댓글