LINE 메신저를 통해 고객과 직접 소통하고 싶다면 LINE Messaging API는 피할 수 없는 선택지다. 다만 대부분의 개발자들은 공식 문서를 읽어도 Webhook 구조, 시그니처 검증, 메시지 전송까지 온전히 구현하지 못하고 중간에 포기한다. 이번에는 LINE 채팅봇을 PHP에서 처음부터 끝까지 구현하는 방법을 정리해서 소개하겠다. 계정 생성부터 실제 메시지 수신/발송까지 단계별로 따라가면 누구나 자신만의 LINE 봇을 만들 수 있을 것이다.
LINE Messaging API를 사용하려면 먼저 LINE Developers에서 비즈니스 계정을 만들어야 한다. 로그인 후 콘솔로 이동하고 새 프로바이더를 생성한다. 그 다음 프로바이더 내에서 Messaging API 채널을 추가한다.
채널이 생성되면 기본 설정 탭에서 두 가지 중요한 값을 얻을 수 있다.
첫 번째는 Channel Access Token이다. 이 토큰으로 LINE 사용자에게 메시지를 보낸다. 토큰 표시 버튼을 클릭해서 복사해둔다.
두 번째는 Channel Secret이다. Webhook 요청이 정말 LINE 서버에서 온 것인지 검증할 때 필요한 시크릿 키다. 역시 복사해서 안전하게 보관한다.
마지막으로 Messaging API 설정 탭에서 Webhook URL을 설정해야 한다. 당신의 서버 공개 URL을 입력하면(예: https://yourserver.com/line-webhook.php) LINE 서버가 메시지를 수신할 때마다 이 URL로 POST 요청을 보낸다.
Webhook 보안의 핵심은 LINE 서버에서 온 요청이 정말 LINE인지 확인하는 것이다. LINE은 모든 Webhook 요청의 X-Line-Signature 헤더에 HMAC SHA256 서명을 포함시킨다. 당신의 Channel Secret으로 요청 본문을 암호화한 결과와 비교해서 일치하면 진짜 LINE이다.
✗ 잘못된 코드(시그니처 검증 없음):
<?php
$json = file_get_contents('php://input');
$events = json_decode($json, true);
// 시그니처 확인 안 함 - 보안 위험!
foreach ($events['events'] as $event) {
if ($event['type'] === 'message') {
// 메시지 처리
}
}
?>
✓ 올바른 코드(시그니처 검증 포함):
<?php
$channel_secret = 'YOUR_CHANNEL_SECRET';
$json = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_LINE_SIGNATURE'] ?? '';
// HMAC SHA256로 시그니처 생성
$hash = hash_hmac('sha256', $json, $channel_secret, true);
$calculated_signature = base64_encode($hash);
// 헤더의 시그니처와 비교
if ($signature !== $calculated_signature) {
http_response_code(401);
exit('Invalid signature');
}
// 여기부터가 안전한 처리
$events = json_decode($json, true);
echo json_encode(['status' => 'ok']);
?>
결과: 요청 헤더의 시그니처와 계산한 값이 일치하면 200 OK를 반환해서 LINE에 정상 처리를 알린다. 불일치하면 401 Unauthorized를 보내서 의심스러운 요청을 거절한다.
시그니처 검증을 통과한 후에는 events 배열에 담긴 여러 종류의 이벤트를 처리한다. 가장 흔한 건 message 타입인데, 사용자가 채팅창에 텍스트나 이미지를 보냈을 때 발생한다.
<?php
$channel_secret = 'YOUR_CHANNEL_SECRET';
$access_token = 'YOUR_CHANNEL_ACCESS_TOKEN';
$json = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_LINE_SIGNATURE'] ?? '';
$hash = hash_hmac('sha256', $json, $channel_secret, true);
$calculated_signature = base64_encode($hash);
if ($signature !== $calculated_signature) {
http_response_code(401);
exit('Invalid signature');
}
$events = json_decode($json, true);
foreach ($events['events'] as $event) {
// 메시지 수신 이벤트
if ($event['type'] === 'message') {
$user_id = $event['source']['userId'];
$message_type = $event['message']['type'];
if ($message_type === 'text') {
$user_message = $event['message']['text'];
$reply_token = $event['replyToken'];
// 사용자 메시지 처리
handle_text_message($user_id, $user_message, $reply_token, $access_token);
}
}
// 팔로우 이벤트 (사용자가 봇을 추가했을 때)
elseif ($event['type'] === 'follow') {
$user_id = $event['source']['userId'];
send_message($user_id, 'LINE 봇을 팔로우해주셔서 감사합니다!', $access_token);
}
}
function handle_text_message($user_id, $message, $reply_token, $access_token) {
// 단순 반복 응답
$response_text = '당신이 보낸 메시지: ' . $message;
// replyToken 사용 (3초 내에만 유효)
reply_message($reply_token, $response_text, $access_token);
}
function reply_message($reply_token, $text, $access_token) {
$url = 'https://api.line.me/v2/bot/message/reply';
$data = [
'replyToken' => $reply_token,
'messages' => [
[
'type' => 'text',
'text' => $text
]
]
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $access_token
],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code !== 200) {
error_log('LINE API Error: ' . $response);
}
}
function send_message($user_id, $text, $access_token) {
$url = 'https://api.line.me/v2/bot/message/push';
$data = [
'to' => $user_id,
'messages' => [
[
'type' => 'text',
'text' => $text
]
]
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $access_token
],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10
]);
curl_exec($ch);
curl_close($ch);
}
http_response_code(200);
?>
결과: 사용자가 텍스트를 보내면 replyToken을 이용한 빠른 응답(reply_message)으로 3초 내에 답장한다. 또는 특정 시간 이후에 메시지를 보낼 때는 Push API(send_message)를 사용한다.
이제 간단한 주문 처리 봇을 만들어보자. 사용자가 "주문"이라고 입력하면 버튼 메시지(Template)를 보내서 선택지를 제시한다.
<?php
$channel_secret = 'YOUR_CHANNEL_SECRET';
$access_token = 'YOUR_CHANNEL_ACCESS_TOKEN';
$json = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_LINE_SIGNATURE'] ?? '';
$hash = hash_hmac('sha256', $json, $channel_secret, true);
$calculated_signature = base64_encode($hash);
if ($signature !== $calculated_signature) {
http_response_code(401);
exit('Invalid signature');
}
$events = json_decode($json, true);
foreach ($events['events'] as $event) {
if ($event['type'] === 'message' && $event['message']['type'] === 'text') {
$reply_token = $event['replyToken'];
$user_message = $event['message']['text'];
if ($user_message === '주문') {
// 버튼 템플릿 메시지 전송
$url = 'https://api.line.me/v2/bot/message/reply';
$data = [
'replyToken' => $reply_token,
'messages' => [
[
'type' => 'template',
'altText' => '주문 메뉴를 선택하세요',
'template' => [
'type' => 'buttons',
'title' => '주문 메뉴',
'text' => '원하는 메뉴를 선택하세요',
'actions' => [
[
'type' => 'message',
'label' => '커피',
'text' => '커피 주문'
],
[
'type' => 'message',
'label' => '음료수',
'text' => '음료수 주문'
],
[
'type' => 'message',
'label' => '디저트',
'text' => '디저트 주문'
]
]
]
]
]
];
send_line_message($url, $data, $access_token);
} elseif (strpos($user_message, '주문') !== false) {
// 사용자가 "커피 주문" 같은 메시지를 보냈을 때
$response_text = '주문이 접수되었습니다. ' . $user_message;
$url = 'https://api.line.me/v2/bot/message/reply';
$data = [
'replyToken' => $reply_token,
'messages' => [
[
'type' => 'text',
'text' => $response_text
]
]
];
send_line_message($url, $data, $access_token);
}
}
}
function send_line_message($url, $data, $access_token) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $access_token
],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
curl_close($ch);
if ($http_code !== 200) {
error_log('LINE API Error [' . $http_code . ']: ' . $response);
error_log('Curl Error: ' . $curl_error);
}
}
http_response_code(200);
?>
결과: 사용자가 "주문"을 입력하면 3개의 버튼이 보여진다. 각 버튼을 클릭하면 그 텍스트가 다시 메시지로 전송되고, 봇이 "주문이 접수되었습니다"라고 반응한다.
✗ 잘못된 것 1: replyToken 만료 시간 무시
replyToken은 Webhook 요청을 받은 후 3초 이내에만 사용 가능하다. 데이터베이스 조회나 외부 API 호출이 오래 걸리면 이 시간을 초과해서 에러가 난다.
✓ 올바른 것: 3초 내에 응답할 수 없다면 즉시 replyToken으로 "처리 중입니다" 같은 임시 메시지를 보낸 후, 나중에 Push API로 최종 결과를 보낸다.
✗ 잘못된 것 2: 시그니처 검증 생략
많은 개발자들이 테스트 편의상 시그니처 검증을 주석 처리하고 그냥 두는데, 이렇게 하면 악의적인 요청으로 봇을 오작동시킬 수 있다.
✓ 올바른 것: 반드시 X-Line-Signature를 항상 검증하고, 불일치하면 401을 반환한다.
✗ 잘못된 것 3: Access Token을 소스 코드에 직접 삽입
GitHub 같은 공개 저장소에 Push할 때 실수로 Access Token이 노출되면 누구나 당신의 봇으로 메시지를 보낼 수 있다.
✓ 올바른 것: 환경 변수나 .env 파일에서 토큰을 로드한다(getenv() 사용).
<?php
// .env 파일에서 로드 (예: LINE_CHANNEL_SECRET=xxx)
$channel_secret = getenv('LINE_CHANNEL_SECRET');
$access_token = getenv('LINE_CHANNEL_ACCESS_TOKEN');
?>
✗ 잘못된 것 4: 모든 Webhook 요청에 대해 메시지 회신
사용자가 메시지를 보낸 후 Webhook 검증 요청, follow 이벤트 등 여러 종류의 요청이 들어온다. 모두에 회신하면 필요 없는 메시지가 난발된다.
✓ 올바른 것: 각 이벤트 타입(message, follow, unfollow, postback 등)을 구분해서 필요한 경우에만 메시지를 보낸다.
✗ 잘못된 것 5: cURL 타임아웃 설정 안 함
LINE API 응답이 늦거나 네트워크 문제가 있을 때 PHP 스크립트가 무한 대기할 수 있다.
✓ 올바른 것: CURLOPT_TIMEOUT을 10초 정도로 설정해서 응답이 없으면 강제로 종료한다.
LINE Messaging API 연동은 시그니처 검증, Webhook 구조, 메시지 템플릿을 이해하면 생각보다 간단하다. replyToken의 만료 시간, Access Token 보안, 이벤트 타입 구분 같은 작은 주의사항들이 모여서 안정적인 봇을 만드는 차이가 된다. 이 글의 코드를 기반으로 점진적으로 기능을 추가하면서 자신만의 LINE 봇을 완성해보자. 다음 단계로는 Flex Message 템플릿(더 복잡한 레이아웃), 사용자 프로필 조회, 상태 메시지 전송 등으로 확장할 수 있을 것이다.