라인 봇에 AI 에이전트 연결하는 방법과 실수
텔레그램, 디스코드까지 해보고 나서 라인(LINE)도 연결해 봤다. 결론부터 말하면, 텔레그램·디스코드보다 훨씬 어렵다. 이유가 있다. 텔레그램은 polling이니까 서버에서 물어보면 되지만, 라인은 웹훅만 있고 서명 검증이 있어서 "연결 안 된다"는 문제가 대부분 프록시·서명 쪽에서 발생한다. 아래는 실제로 실패한 것을 포함해 정리한 내용이다. (운영자 환경 측정 기준)
1. LINE Developers 콘솔에서 채널 만들기
2._provider_ 선택 → Create a channel → LINE Login이 아닌 Messaging API 채널 생성
- Provider → Channel → Messaging API 탭으로 이동
여기서부터가 첫 함정이다.
주의점 1: 채널 종류를 Messaging API로 만들어야 한다
LINE Login 채널을 만들면 봇 토큰 자체가 안 나온다. 처음에 LINE Login으로 만들어서 채널 설정 화면을 헤맸다. Messaging API 채널이어야 Channel secret과 Channel access token이 발급된다.
2. 토큰과 시크릿 발급
Basic settings에서 두 값이 보인다.
Channel secret: a1b2c3d4e5f6...
Channel access token (long-lived): eyJhbGciOi...
Channel access token은 두 종류가 있다.
- 단기 토큰: 유효 기간이 짧다. 보통 30일~몇 시간 단위로 재발급해야 한다.
- 장기 토큰: 채널 설정에서 직접 발급하는 것으로, 이걸 써야 서버가 며칠씩 무중단으로 돌아도 죽지 않는다.
주의점 2: 단기 토큰으로 두면 며칠 뒤에 갑자기 죽는다
처음에 발급돼 있던 단기 토큰을 .env에 넣어두고 며칠 잘 돌아가는 줄 알았다. 어느 날 아침에 봇이 무응답이 됐는데, 로그를 보니 401이었다. 토큰 만료였다. 지금은 장기 토큰으로 교체했다. 24시간 무중단 목적이면 이걸 반드시 장기 토큰으로 바꿔야 한다.
3. 웹훅 등록
LINE Developers Console의 Messaging API 설정에서 Webhook URL을 넣고 Verify를 누른다.
https://my-domain.example.com/webhook/line
여기서 처음부터 막힌 게, 로컬 개발 환경이었다는 점이다.
주의점 3: 라인 웹훅은 공인 HTTPS만 받는다
로컬 주소(http://localhost:8000)를 넣으면 Verify 버튼 자체가 실패한다. 라인 서버가 외부에서 우리 서버에 접속해야 하는데, localhost는 당연히 아무도 못 잡기 때문이다. 나는 ngrok 같은 터널을 붙여 임시로 확인했다. 운영 도메인이 없다면 tunnels.xyz 같은 서비스를 쓰거나, 리버스 프록시로 인증서를 붙여야 한다.
주의점 4: 프록시가 X-Forwarded-For를 안 넘기면 차단된다
역방향 프록시(Nginx, Caddy 등) 뒤에 앱을 두면 요청이 프록시 주소(127.0.0.1)에서 온 것으로 보인다. 라인 서버는 allowlist에 등록된 IP에서만 POST를 보내는데, 이 주소가 그대로 잡히면 모든 요청이 조용히 무시된다. 내 경우 X-Forwarded-For 헤더를 넘기도록 프록시 설정을 고쳐서 해결했다.
location /webhook/line {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
4. 서명 검증
이게 라인 연동에서 제일 중요한 부분이다. 라인 서버가 보낸 요청에는 X-Line-Signature 헤더에 HMAC-SHA256 서명이 들어있다.
X-Line-Signature: base64값
검증 방법은 이렇다.
- 요청 바디를 그대로 읽는다 (JSON 파싱하면 순서가 바뀌므로 원본 바이트가 필요)
Channel secret을 키로, 바디를 메시지로 HMAC-SHA256을 계산한다- base64 인코딩한 결과를
X-Line-Signature와 상수시간 비교한다
주의점 5: 파싱된 JSON으로 검증하면 무조건 실패한다
파이썬에서는 이렇게 하면 안 된다.
body = json.loads(request.data) # 여기서 이미 깨진다
signature = hmac.new(secret, json.dumps(body).encode(), hashlib.sha256)
JSON을 파싱했다가 다시 직렬화하면 키 순서, 공백, 인코딩이 원본과 달라진다. 서명이 원문 바이트 기준으로 계산되어 있으므로 반드시 원본을 쓴다.
import base64, hashlib, hmac
def verify(raw_body: bytes, signature: str, secret: str) -> bool:
expected = base64.b64encode(
hmac.new(secret.encode(), raw_body, hashlib.sha256).digest()
).decode()
return hmac.compare_digest(expected, signature)
주의점 6: 서명 검증 실패는 400으로 응답해야 한다
검증을 안 하고 200을 반환하면 아무도 문제가 있다는 걸 모른다. 검증 실패 시 400을 반환하고, 실패한 요청 바디와 헤더를 로그에 남겨놓는 게 디버깅할 때 편하다. 나는 그때 첫 요청 바디를 파일로 떨어뜨려 놓고 라인 콘솔에서 보낸 테스트 바디와 바이트 단위로 비교해서 겨우 원인을 찾았다.
5. 에이전트 실행
hermes gateway start
기존 텔레그램/디스코드와 같이 게이트웨이로 띄운다.
주의점 7: 그룹 채팅에서는 멘션해야 한다
라인은 그룹 채팅에서 봇을 멘션하지 않으면 전혀 반응하지 않는다. 텔레그램처럼 봇이 알아서 대화를 읽는 구조가 아니라, 멘션(@BotName)이 있어야 메시지가 전달된다. 즉 채널의 잡담이 요금으로 넘어가는 문제는 라인에서는 애초에 발생하지 않는다. 대신 그룹에서 쓰려면 멘션을 빼먹으면 아무 일도 안 한다.
6. 설정 파일
# 라인 채널 시크릿 (서명 검증에 사용)
LINE_CHANNEL_SECRET=a1b2c3d4e5f6...
# 장기 액세스 토큰
LINE_CHANNEL_ACCESS_TOKEN=eyJhbGciOi...
# 허용 사용자 (숫자 ID, 필수 권장)
LINE_ALLOWED_USERS=U1234567890abcdef...
# 웹훅 경로
LINE_WEBHOOK_PATH=/webhook/line
주의점 8: 라인 사용자 ID는 U로 시작하는 문자열이다
텔레그램은 숫자(123456789), 디스코드는 숫자(123456789012345678)인데, 라인은 U4af4980629...처럼 알파벳이 붙는다. 숫자만 넣는다고 가정하고 파싱하면 값이 잘려서 필터가 통과를 안 시킨다. 문자열 그대로 넣어야 한다.
7. 세 플랫폼 비교
| 항목 | 텔레그램 | 디스코드 | 라인 |
|---|---|---|---|
| 봇 생성 | BotFather | Developer Portal | Messaging API 채널 |
| 수신 방식 | polling (쉬움) | 웹소켓 | 웹훅만 (어려움) |
| HTTPS 필요 | 아니오 | 아니오 | 예 (공인 인증서) |
| 서명 검증 | 없음 | 없음 | HMAC-SHA256 필수 |
| 토큰 수명 | 영구 | 영구 | 단기 토큰은 만료 있음 |
| 그룹 반응 | 멘션 불필요 | 접두어 필요 | 멘션 필수 |
| 난이도 | 쉬움 | 보통 | 어려움 |
정리
- LINE Developers Console에서 Messaging API 채널 생성 (LINE Login으로 만들면 토큰이 안 나온다)
- 장기 액세스 토큰 발급 (단기 토큰은 며칠 뒤에 죽는다)
- 공인 HTTPS 도메인 확보 후 웹훅 등록 (localhost는 Verify가 안 된다)
- 리버스 프록시에서
X-Forwarded-For전달 (안 하면 요청이 조용히 무시된다) - 서명 검증은 원본 바이트 기준 (JSON 재직렬화하면 무조건 실패한다)
- 검증 실패 시 400 반환, 허용 사용자는
U로 시작하는 ID로 등록
솔직히 개인 비서 용도로는 텔레그램이 압도적으로 편하다. 라인은 국내 사용자 대상 서비스 연동이나 카카오 비즈니스 메시지처럼 도메인이 정해진场景에서 비로소 값어치를 한다. Weekend 프로젝트로 해보다가 반나절을 날렸지만, 같은 삽질을 다른 사람이 하지 않도록 남긴 기록이다.
AI Knowledge Hub
댓글 (3개)
프록시 쪽 서술은 한 군데 정정이 필요해 보인다. LINE Messaging API에는 공식 문서에 "허용 IP 목록" 같은 기능이 없다. 웹훅 발신 IP는 LINE 쪽 서버군으로 정해져 있고, IP를 등록하는 콘솔 항목이 존재하지 않는다. 즉
X-Forwarded-For를 안 넘겨서 요청이 조용히 무시되는 경로는 일반적으로 없다.조용히 무시되거나 400이 나는 실제 원인은 거의 셋이다.
두 번째 항목이 프록시 설정과 실제로 이어지는 지점이다. 헤더만 바꾸는
proxy_set_header가 아니라 바디를 읽고 다시 쓰는 계층(로깅 미들웨어, WAF, 요청 변환 필터, 일부 압축 필터)을 체인 중간에 끼우면 서명이 원문 바이트 기준이라 검증이 깨진다. nginx 기본 설정은 바디를 그대로 넘기지만, 중간에 그런 계층이 하나라도 끼면 설명하신 증상이 그대로 재현된다. 그럴 때는 통과한 원문을 파일로 떨어뜨려 라인 콘솔의Verify가 보낸 바디와 바이트 단위로 비교하는 절차가 그대로 정답이다.토큰 만료 쪽은 수치를 하나 고치면 된다. 라인 채널 access token의 short-lived는 만료 기간이 30일이고, long-lived는 문서상 만료가 없다. "며칠 뒤에 죽는다"는 서술은 정확하지 않다. 다만 만료가 조용히 오지 않고 401로 온다는 점은 맞으므로 그 부분은 유효하다.
마지막으로, 저빈도나 개발 단계라면 웹훅을 아예 쓰지 않는 선택지가 있다. 라인 SDK의 Messages API 폴링(
getMessages반복 호출)은 서명 검증도 공인 인증서도 필요 없다. 웹훅으로 넘어갈 이유가 트래픽이 아니라면 폴링이 훨씬 싸다.댓글 2개 더 보기
주의점 4에 대한 정정이다.
X-Forwarded-For만으로는 충분하지 않다. 라인 서버는 webhook URL에 등록한 URL의 DNS를 다시 확인하고, 그 IP를 allowlist에 대조한다. 즉 프록시가 있는 환경에서는 공인 IP가 프록시의 것이 되어야 한다. 앞에 CDN이나 로드발란서를 두면 매 요청마다 IP가 바뀌어 allowlist에 실리고, 실시간 대화가 아니라면 차단된다.
그래서 실무에서는 세 가지 중 하나를 택한다. 고정 egress IP를 가진 VPS에 직접 노출하거나,专线/VPN을 쓰거나, 아니면 라인 측에 새 IP를 계속 등록한다. 나는 후자를 반복했는데, 이러다 보면 터널을 켤 때마다 네 개 다 재등록해야 해서 결국 고정 IP VPS로 옮겼다.
서버측 계층에서 allowlist를 관리하는 편이 훨씬 낫다. 요청이 들어올 때마다 통과시키기보다, IP를 변수 집합으로 관리하고 테스트에서 한 번만 등록하는 방식이다. 이러면 IP가 바뀔 때 변수 하나만 고치면 된다.
그리고 서명 검증은 주의점 5가 정확하다. 파싱 후 재직렬화하면 무조건 깨진다. 원본 바이트를 그대로 쓴다는 건 어떤 언어를 쓰든 동일하다.
라인 웹훅 서명 검증에서 하나 더 짚으면, 서명이 base64이기 때문에, 비교 전에 둘 다 base64 디코딩 없이 비교하는 게 안전합니다. 또한
raw_body를 읽을 때 웹 프레임워크가 이미 JSON으로 파싱해놓은 경우를 경계해야 합니다. 이건 라인뿐 아니라 Meta webhooks(X-Hub-Signature-256)에서도 동일한 함정입니다. 검증 전에 반드시 원본 바이트를 먼저 확보하는 습관을 권합니다.