라인 메신저는 국내에서 가장 많이 쓰이는 메시징 앱 중 하나다. 하지만 대부분의 개발자들은 LINE Messaging API가 존재하는지도 모르거나, 있다는 걸 알아도 어디서부터 시작해야 할지 막막해한다. 공식 문서는 일본어나 영어로만 되어 있고, 한국 예제는 거의 없다. 이번에는 LINE 봇을 직접 만들면서 API 인증 방식부터 webhook 수신, 자동 응답 처리까지 실무에 필요한 모든 것을 정리해서 설명하겠다.

 

1단계: LINE 비즈니스 계정과 채널 만들기
LINE Messaging API를 사용하려면 먼저 LINE Business Center에 가입해야 한다. 일반 라인 계정으로는 안 되고, 반드시 비즈니스 계정이어야 한다.

비즈니스 센터에 로그인한 후 "채널 만들기"를 클릭하고, 채널 유형에서 "Messaging API"를 선택한다. 채널 이름, 카테고리, 설명을 입력하면 채널이 생성된다. 생성 후 즉시 채널 설정 페이지에서 중요한 두 가지 정보를 복사해야 한다.

첫 번째는 "Channel Access Token"이고, 두 번째는 "Channel Secret"이다. 이 두 값은 API 인증과 webhook 서명 검증에 사용되므로 절대로 공개해서는 안 된다. 나중에 PHP 코드에서 환경변수로 관리하게 된다.

 

Webhook URL 설정
채널 설정 페이지의 "메시지 수신 설정" 섹션에서 "Webhook URL"을 입력한다. 이 URL은 사용자가 봇에게 메시지를 보낼 때 LINE 서버가 webhook 형태로 POST 요청을 보내는 엔드포인트다. 예를 들어 `https://yourdomain.com/line-webhook.php`처럼 작성한다. https 프로토콜이어야 하고, http는 작동하지 않는다.

 

2단계: PHP에서 Webhook 수신 처리
LINE 서버는 사용자의 메시지, 이벤트 등을 webhook POST 요청으로 보낸다. PHP에서는 이 요청을 받아서 파싱해야 한다.
<?php
// ✗ 잘못된 방식: 원본 요청을 그대로 다루기
$message = $_POST['events'][0]['message']['text'];
echo $message; // 작동하지 않음
?>
LINE Messaging API는 JSON 형식의 요청 바디를 POST로 보낸다. `$_POST` 슈퍼글로벌로는 접근할 수 없고, `php://input`에서 읽어야 한다.
<?php
// ✓ 올바른 방식: php://input에서 JSON 읽기
$json = file_get_contents('php://input');
$request = json_decode($json, true);

if (isset($request['events'][0])) {
    $event = $request['events'][0];
    if ($event['type'] === 'message') {
        $userMessage = $event['message']['text'];
        echo "받은 메시지: " . $userMessage;
    }
}
?>
이제 JSON을 정상적으로 파싱할 수 있다. 다음은 webhook 서명을 검증하는 단계다.

 

3단계: Webhook 서명 검증 (보안 필수)
LINE 서버는 모든 webhook 요청에 "X-Line-Signature" 헤더를 포함시킨다. 이것은 요청이 정말 LINE 서버에서 온 것인지 확인하는 HMAC SHA256 서명이다. 악의적인 사용자가 가짜 요청을 보내는 것을 방지한다.
<?php
// ✗ 잘못된 방식: 서명 검증 없이 그냥 처리
$json = file_get_contents('php://input');
$request = json_decode($json, true);
// 바로 처리... 이건 보안 위험!
?>
<?php
// ✓ 올바른 방식: HMAC SHA256 서명 검증
$channelSecret = getenv('LINE_CHANNEL_SECRET'); // 환경변수에서 읽기
$signature = $_SERVER['HTTP_X_LINE_SIGNATURE'] ?? '';
$json = file_get_contents('php://input');

// HMAC SHA256 계산
$hash = hash_hmac('sha256', $json, $channelSecret, true);
$expectedSignature = base64_encode($hash);

if ($signature !== $expectedSignature) {
    http_response_code(401);
    die('Invalid signature');
}

$request = json_decode($json, true);
echo "서명 검증 완료";
?>
이 검증을 먼저 수행하지 않으면 악의적인 요청에 응답할 수 있다.

 

4단계: 메시지 이벤트 구분 및 처리
LINE webhook에는 메시지, 팔로우, 언팔로우 등 여러 이벤트 타입이 있다. 각각에 맞는 처리를 해야 한다.
<?php
function handleLineWebhook() {
    $channelSecret = getenv('LINE_CHANNEL_SECRET');
    $signature = $_SERVER['HTTP_X_LINE_SIGNATURE'] ?? '';
    $json = file_get_contents('php://input');
    
    // 서명 검증
    $hash = hash_hmac('sha256', $json, $channelSecret, true);
    $expectedSignature = base64_encode($hash);
    if ($signature !== $expectedSignature) {
        http_response_code(401);
        return;
    }
    
    $request = json_decode($json, true);
    
    foreach ($request['events'] as $event) {
        $type = $event['type'];
        
        switch ($type) {
            case 'message':
                handleMessage($event);
                break;
            case 'follow':
                handleFollow($event);
                break;
            case 'unfollow':
                handleUnfollow($event);
                break;
        }
    }
    
    http_response_code(200);
}

function handleMessage($event) {
    $messageType = $event['message']['type'];
    
    if ($messageType === 'text') {
        $text = $event['message']['text'];
        $userId = $event['source']['userId'];
        // 텍스트 메시지 처리
        sendTextMessage($userId, "받은 메시지: " . $text);
    }
}

function handleFollow($event) {
    $userId = $event['source']['userId'];
    sendTextMessage($userId, "팔로우 감사합니다!");
}

function handleUnfollow($event) {
    // 언팔로우는 userId를 받을 수 없음
    // 로그만 기록
}

handleLineWebhook();
?>

 

5단계: 메시지 발송 - Reply API vs Push API
LINE에 메시지를 돌려보내는 방법은 두 가지다. Reply API는 webhook 이벤트에 대한 응답으로 보내고, Push API는 언제든 능동적으로 보낼 수 있다.
구분Reply APIPush API
용도사용자 메시지에 답장능동적 알림/메시지
매개변수replyToken 필요userId 필요
제약최대 1회, 3개 메시지제한 없음 (quota 있음)
비용무료요금제에 따라 과금

대부분의 봇은 Reply API를 먼저 사용한다. 이벤트에 포함된 `replyToken`을 사용해서 답장하기 때문이다.

<?php
function sendTextMessage($userId, $text, $replyToken = null) {
    $accessToken = getenv('LINE_CHANNEL_ACCESS_TOKEN');
    
    if ($replyToken) {
        // Reply API 사용 (이벤트 응답)
        $url = 'https://api.line.biz/v2/bot/message/reply';
        $body = json_encode([
            'replyToken' => $replyToken,
            'messages' => [
                [
                    'type' => 'text',
                    'text' => $text
                ]
            ]
        ]);
    } else {
        // Push API 사용 (능동적 발송)
        $url = 'https://api.line.biz/v2/bot/message/push';
        $body = json_encode([
            'to' => $userId,
            'messages' => [
                [
                    'type' => 'text',
                    'text' => $text
                ]
            ]
        ]);
    }
    
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/json',
            'Authorization: Bearer ' . $accessToken
        ],
        CURLOPT_POSTFIELDS => $body
    ]);
    
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($httpCode !== 200) {
        error_log("LINE API Error: " . $response);
        return false;
    }
    
    return true;
}
?>
이 함수는 Reply API와 Push API 둘 다 지원한다. `replyToken`이 있으면 Reply API를 사용하고, 없으면 Push API를 사용한다.

 

실전 예제: 메시지 자동 응답 봇
이제 webhook에서 받은 메시지에 자동으로 응답하는 완전한 예제를 보자.
<?php
// line-webhook.php
require_once __DIR__ . '/line-bot.php';

function main() {
    $channelSecret = getenv('LINE_CHANNEL_SECRET');
    $accessToken = getenv('LINE_CHANNEL_ACCESS_TOKEN');
    
    if (!$channelSecret || !$accessToken) {
        http_response_code(500);
        die('Missing environment variables');
    }
    
    $signature = $_SERVER['HTTP_X_LINE_SIGNATURE'] ?? '';
    $json = file_get_contents('php://input');
    
    // 서명 검증
    $hash = hash_hmac('sha256', $json, $channelSecret, true);
    $expectedSignature = base64_encode($hash);
    if ($signature !== $expectedSignature) {
        http_response_code(401);
        die('Invalid signature');
    }
    
    $request = json_decode($json, true);
    
    foreach ($request['events'] ?? [] as $event) {
        if ($event['type'] !== 'message') continue;
        if ($event['message']['type'] !== 'text') continue;
        
        $userMessage = $event['message']['text'];
        $replyToken = $event['replyToken'];
        $userId = $event['source']['userId'];
        
        // 간단한 키워드 응답
        $response = matchKeyword($userMessage);
        
        $bot = new LineBot($accessToken);
        $bot->reply($replyToken, $response);
    }
    
    http_response_code(200);
}

function matchKeyword($message) {
    switch (trim($message)) {
        case '안녕':
        case '안녕하세요':
            return "안녕하세요! 무엇을 도와드릴까요?";
        case '도움말':
            return "안녕, 도움말, 시간 명령어를 사용할 수 있습니다.";
        case '시간':
            return "현재 시간: " . date('Y-m-d H:i:s');
        default:
            return """ . $message . "" 명령어는 모르겠습니다.";
    }
}

main();
?>
<?php
// line-bot.php - LINE Bot 헬퍼 클래스
class LineBot {
    private $accessToken;
    private $apiUrl = 'https://api.line.biz';
    
    public function __construct($accessToken) {
        $this->accessToken = $accessToken;
    }
    
    public function reply($replyToken, $text) {
        $url = $this->apiUrl . '/v2/bot/message/reply';
        $body = json_encode([
            'replyToken' => $replyToken,
            'messages' => [
                ['type' => 'text', 'text' => $text]
            ]
        ]);
        
        return $this->sendRequest($url, $body);
    }
    
    public function push($userId, $text) {
        $url = $this->apiUrl . '/v2/bot/message/push';
        $body = json_encode([
            'to' => $userId,
            'messages' => [
                ['type' => 'text', 'text' => $text]
            ]
        ]);
        
        return $this->sendRequest($url, $body);
    }
    
    private function sendRequest($url, $body) {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                'Authorization: Bearer ' . $this->accessToken
            ],
            CURLOPT_POSTFIELDS => $body,
            CURLOPT_TIMEOUT => 10
        ]);
        
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($httpCode !== 200) {
            error_log("LINE API Error ({$httpCode}): " . $response);
            return false;
        }
        
        return true;
    }
}
?>

 

주의사항: 환경변수 설정

✗ 잘못된 방식: 코드에 직접 토큰 작성

<?php
$accessToken = "직접 적은 토큰값";
// 이렇게 하면 git에 커밋될 때 토큰이 노출됨!
?>

✓ 올바른 방식: 환경변수에서 읽기

<?php
$accessToken = getenv('LINE_CHANNEL_ACCESS_TOKEN');

// .env 파일 (git에 커밋하지 말 것)
// LINE_CHANNEL_ACCESS_TOKEN=xxxxxx
// LINE_CHANNEL_SECRET=yyyyy
?>
Nginx나 Apache에서는 다음처럼 환경변수를 설정한다.
# .htaccess (Apache)
SetEnv LINE_CHANNEL_ACCESS_TOKEN "your_token_here"
SetEnv LINE_CHANNEL_SECRET "your_secret_here"
또는 PHP-FPM의 pool 설정 파일에 추가한다.
# /etc/php-fpm.d/www.conf
env[LINE_CHANNEL_ACCESS_TOKEN] = your_token_here
env[LINE_CHANNEL_SECRET] = your_secret_here

 

흔한 실수
실수원인해결책
Webhook URL이 http로 설정됨LINE은 https만 허용반드시 https 사용
서명 검증 없이 처리보안 미흡HMAC SHA256 검증 필수
json_decode 결과를 바로 배열로 접근null 체크 부족isset() 또는 null-safe operator 사용
replyToken 없이 Reply API 호출이벤트 파싱 오류webhook 스키마 다시 확인
토큰을 코드에 하드코딩보안 위험환경변수 사용

 

다음 단계
이제 기본적인 메시지 응답은 가능하다. 다음으로 할 수 있는 것들은 다음과 같다.

- Flex Message: JSON 기반 리치 메시지(버튼, 이미지, 텍스트를 조합한 카드 형식)
- Quick Reply: 빠른 응답 버튼(사용자가 선택할 수 있는 옵션)
- Bot API 통합: 실제 데이터베이스나 외부 API와 연동한 동적 응답
- Group/Room 메시지 처리: 개인 채팅 외에 단체 채팅 지원

LINE Messaging API는 생각보다 강력하고 유연하다. webhook 서명 검증과 토큰 관리만 제대로 하면, 실무에서 바로 사용할 수 있는 안정적인 봇을 만들 수 있다. 이 글의 webhook 수신과 Reply API 부분을 참고해 간단한 응답형 봇부터 시작하면, 추후에 더 복잡한 기능을 추가하기도 쉬워질 것이다.