챗봇을 만들었으면 끝일까? 아니다. 운영이 시작이다. 사용자가 실제로 뭘 물어보는지 모르면 챗봇을 개선할 수 없다. 로그가 없으면 눈감고 운전하는 거다. 이번 글에서는 대화 로그 시스템, 문의 접수 기능, 그리고 몇 달 운영하면서 쌓인 실전 팁을 정리한다.
대화 로그 DB 테이블 설계
별도의 커스텀 테이블을 만든다. 워드프레스의 postmeta나 options 테이블에 로그를 쑤셔넣는 건 안 된다. 데이터가 쌓이면 사이트 전체가 느려진다.
function nalkkul_chatbot_create_log_table() {
global $wpdb;
$table = $wpdb->prefix . 'chatbot_logs';
if ($wpdb->get_var("SHOW TABLES LIKE '{$table}'") === $table) return;
$charset = $wpdb->get_charset_collate();
$wpdb->query("CREATE TABLE {$table} (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_message TEXT NOT NULL,
bot_response TEXT NOT NULL,
response_type VARCHAR(20) DEFAULT 'rule',
ip_address VARCHAR(45) DEFAULT '',
user_id BIGINT UNSIGNED DEFAULT 0,
response_time FLOAT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) {$charset};");
}
add_action('init', 'nalkkul_chatbot_create_log_table');
init 액션에 걸어뒀지만, 테이블이 이미 존재하면 바로 리턴한다. 매 페이지 로드마다 SHOW TABLES 쿼리가 실행되는 게 부담이면 옵션 값으로 플래그를 관리할 수도 있다. 하지만 SHOW TABLES는 충분히 빠르니까 이 정도로 충분하다.
테이블 컬럼 설계를 보면:
- user_message (TEXT) — 사용자가 입력한 원본 메시지
- bot_response (TEXT) — 챗봇의 응답. 규칙 기반이든 AI든 검색이든 전부 저장
- response_type (VARCHAR 20) — ‘rule’, ‘ai’, ‘search’, ‘error’ 네 가지. 어떤 엔진이 응답했는지 구분
- ip_address (VARCHAR 45) — IPv6까지 고려해서 45자. IPv4만 쓰면 15자로 충분하지만 미래 대비
- user_id — 로그인한 회원이면 ID 저장. 비회원은 0
- response_time (FLOAT) — AI 응답의 소요 시간(초 단위). 성능 모니터링용
- created_at — 자동 타임스탬프
로그 저장 함수
모든 대화를 기록하는 중앙 함수. AI 응답이든 규칙 기반이든 검색이든 에러든 전부 이 함수를 통해 저장한다.
function nalkkul_chatbot_log($question, $answer, $type = 'rule', $time = 0) {
global $wpdb;
$wpdb->insert($wpdb->prefix . 'chatbot_logs', [
'user_message' => mb_substr($question, 0, 1000),
'bot_response' => mb_substr($answer, 0, 2000),
'response_type' => $type,
'ip_address' => sanitize_text_field($_SERVER['REMOTE_ADDR'] ?? ''),
'user_id' => get_current_user_id(),
'response_time' => $time,
'created_at' => current_time('mysql'),
]);
}
mb_substr로 길이를 제한하는 이유: TEXT 타입이라 이론적으로 무한히 저장할 수 있지만, 누군가 악의적으로 엄청 긴 메시지를 보낼 수 있다. 질문은 1000자, 답변은 2000자로 잘라낸다.
규칙 기반 응답은 JS에서 AJAX로 로그를 보낸다:
add_action('wp_ajax_nalkkul_chatbot_log', 'nalkkul_chatbot_log_ajax');
add_action('wp_ajax_nopriv_nalkkul_chatbot_log', 'nalkkul_chatbot_log_ajax');
function nalkkul_chatbot_log_ajax() {
check_ajax_referer('nalkkul_chatbot_nonce', 'nonce');
$q = sanitize_textarea_field($_POST['question'] ?? '');
$a = sanitize_textarea_field($_POST['answer'] ?? '');
if (!empty($q)) {
nalkkul_chatbot_log($q, $a, 'rule');
}
wp_send_json_success();
}
AI 응답은 서버에서 바로 저장하지만, 규칙 기반 응답은 클라이언트(JS)에서 처리되니까 별도의 AJAX 엔드포인트가 필요하다. wp_ajax_nopriv_ 접두사는 비로그인 사용자도 사용 가능하게 한다.
관리자 로그 페이지
워드프레스 관리자 메뉴에 “챗봇 로그” 페이지를 추가한다. 여기서 대화 기록을 확인하고, 필터링하고, CSV로 내보내고, 삭제할 수 있다.
add_action('admin_menu', function() {
add_menu_page(
'챗봇 대화 로그',
'💬 챗봇 로그',
'manage_options',
'chatbot-logs',
'nalkkul_chatbot_logs_page',
'dashicons-format-chat',
58
);
});
로그 페이지 상단에 통계 카드 5개가 나온다: 전체 대화, 오늘, AI 응답, 규칙 기반, 오류. 한눈에 챗봇 상태를 파악할 수 있다.
$stats_total = $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
$stats_ai = $wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE response_type = 'ai'");
$stats_rule = $wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE response_type = 'rule'");
$stats_error = $wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE response_type = 'error'");
$stats_today = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$table} WHERE DATE(created_at) = %s",
current_time('Y-m-d')
));
AJAX로 동적 로딩 — 캐시 문제 해결
처음에는 PHP에서 로그를 직접 렌더링했다. 문제가 있었다. 워드프레스 관리자 페이지가 캐시되는 경우가 있어서 새 대화가 안 보였다. 페이지를 새로고침해도 이전 데이터가 나오는 경우도 있었다.
해결: 페이지 로드 시 AJAX로 로그를 가져오도록 바꿨다. 브라우저 포커스를 받을 때도 자동 갱신.
(function(){
var nonce = '';
var ajaxUrl = '';
function loadLogs() {
var params = new URLSearchParams(window.location.search);
var fd = new FormData();
fd.append('action', 'nalkkul_chatbot_get_logs');
fd.append('nonce', nonce);
fd.append('s', params.get('s') || '');
fd.append('type', params.get('type') || '');
fd.append('paged', params.get('paged') || '1');
fetch(ajaxUrl, {method:'POST', body:fd})
.then(function(r){return r.json();})
.then(function(res){
if(res.success) {
document.getElementById('tg-logs-tbody').innerHTML = res.data.html;
var s = res.data.stats;
var cards = document.querySelectorAll('[data-stat]');
cards.forEach(function(c){
var key = c.getAttribute('data-stat');
if(s[key] !== undefined) c.textContent = Number(s[key]).toLocaleString();
});
}
});
}
loadLogs();
window.addEventListener('focus', loadLogs);
})();
window.addEventListener('focus', loadLogs)가 핵심 트릭이다. 관리자가 챗봇 로그 탭을 열어놓고 다른 탭에서 작업하다가 돌아오면 자동으로 최신 로그를 보여준다. 새로고침 필요 없다.
서버측 AJAX 핸들러에서는 HTML을 직접 생성해서 리턴한다:
function nalkkul_chatbot_get_logs_ajax() {
if (!current_user_can('manage_options')) wp_send_json_error();
check_ajax_referer('chatbot_logs_ajax', 'nonce');
global $wpdb;
$table = $wpdb->prefix . 'chatbot_logs';
// 필터, 페이징 처리
$type_filter = sanitize_text_field($_POST['type'] ?? '');
$search = sanitize_text_field($_POST['s'] ?? '');
$page = max(1, intval($_POST['paged'] ?? 1));
$per_page = 30;
$offset = ($page - 1) * $per_page;
$where = "WHERE 1=1";
if ($type_filter) $where .= $wpdb->prepare(" AND response_type = %s", $type_filter);
if ($search) $where .= $wpdb->prepare(
" AND (user_message LIKE %s OR bot_response LIKE %s)",
"%{$search}%", "%{$search}%"
);
$logs = $wpdb->get_results(
"SELECT * FROM {$table} {$where} ORDER BY created_at DESC LIMIT {$per_page} OFFSET {$offset}"
);
// 응답 유형별 배지 색상
$badge_colors = ['ai' => '#9b59b6', 'rule' => '#27ae60', 'error' => '#e74c3c', 'search' => '#3498db'];
$badge_labels = ['ai' => 'AI', 'rule' => '규칙', 'error' => '오류', 'search' => '검색'];
// HTML 생성 + 통계 포함
wp_send_json_success(['html' => $html, 'stats' => $stats]);
}
응답 유형별로 색이 다른 배지를 붙여서 한눈에 구분할 수 있다. 보라색은 AI, 초록색은 규칙 기반, 빨간색은 오류, 파란색은 검색.
CSV 내보내기
로그를 엑셀에서 분석하고 싶을 때 CSV로 내보낸다. 한글 깨짐을 방지하기 위해 UTF-8 BOM을 파일 시작에 넣는다.
if (isset($_POST['export_csv']) && check_admin_referer('chatbot_logs_action')) {
$rows = $wpdb->get_results("SELECT * FROM {$table} ORDER BY created_at DESC", ARRAY_A);
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=chatbot_logs_' . date('Ymd') . '.csv');
$out = fopen('php://output', 'w');
fwrite($out, "\xEF\xBB\xBF"); // UTF-8 BOM — 엑셀 한글 깨짐 방지
fputcsv($out, ['ID', '질문', '답변', '유형', 'IP', '회원ID', '응답시간', '날짜']);
foreach ($rows as $r) {
fputcsv($out, $r);
}
fclose($out);
exit;
}
\xEF\xBB\xBF — 이 3바이트가 없으면 엑셀에서 CSV를 열었을 때 한글이 깨진다. UTF-8 BOM(Byte Order Mark)을 파일 앞에 넣어서 엑셀에게 “이 파일은 UTF-8이야”라고 알려준다. 사소하지만 빠뜨리면 짜증나는 부분.
문의 접수 기능 — 인라인 폼
사용자가 “문의”라고 입력하면 채팅 창 안에 문의 폼이 나타난다. 이름, 이메일, 메시지를 입력하고 보내면 관리자 이메일로 전달된다. 별도의 문의 페이지로 이동할 필요 없다.
function nalkkul_chatbot_contact() {
check_ajax_referer('nalkkul_chatbot_nonce', 'nonce');
$name = sanitize_text_field($_POST['name'] ?? '');
$email = sanitize_email($_POST['email'] ?? '');
$message = sanitize_textarea_field($_POST['message'] ?? '');
if (empty($name) || empty($email) || empty($message)) {
wp_send_json_error(['message' => '모든 항목을 입력해주세요.']);
}
$to = '[email protected]';
$subject = "[무엇이든알아보자] 챗봇 문의 - {$name}님";
$body = "이름: {$name}\n이메일: {$email}\n\n문의 내용:\n{$message}";
$headers = ['Reply-To: ' . $email];
$sent = wp_mail($to, $subject, $body, $headers);
if ($sent) {
wp_send_json_success(['message' => '문의가 접수되었습니다!']);
} else {
wp_send_json_error(['message' => '전송에 실패했습니다. 잠시 후 다시 시도해주세요.']);
}
}
Reply-To 헤더를 설정해서 관리자가 메일에서 바로 답장할 수 있게 했다. sanitize_email()로 이메일 형식을 검증하고, sanitize_textarea_field()로 HTML 태그를 제거한다. nonce 검증은 CSRF 공격을 막는다.
프론트엔드에서 폼을 동적으로 생성하는 JS:
function submitContactForm(formEl) {
var nameEl = formEl.querySelector('.tg-cf-name');
var emailEl = formEl.querySelector('.tg-cf-email');
var msgEl = formEl.querySelector('.tg-cf-msg');
var btn = formEl.querySelector('.tg-cf-submit');
var name = nameEl.value.trim();
var email = emailEl.value.trim();
var msg = msgEl.value.trim();
if (!name || !email || !msg) {
addMessage("모든 항목을 입력해주세요! 📝", 'bot');
return;
}
btn.disabled = true;
btn.textContent = '전송 중...';
var formData = new FormData();
formData.append('action', 'nalkkul_chatbot_contact');
formData.append('nonce', NONCE);
formData.append('name', name);
formData.append('email', email);
formData.append('message', msg);
fetch(AJAX_URL, { method: 'POST', body: formData })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
addMessage("문의가 접수되었습니다! 빠른 시일 내에 답변 드리겠습니다. 📬", 'bot');
nameEl.value = '';
emailEl.value = '';
msgEl.value = '';
} else {
addMessage(data.data.message || "전송에 실패했습니다. 😢", 'bot');
}
btn.disabled = false;
btn.textContent = '📧 문의 보내기';
});
}
전송 중에 버튼을 비활성화해서 중복 전송을 방지한다. 전송 완료 후 입력 필드를 초기화하고 버튼을 다시 활성화한다.
보안 체크리스트
챗봇은 외부에 노출된 엔드포인트다. 보안에 신경 써야 할 부분들:
- Nonce 검증 — 모든 AJAX 핸들러에서
check_ajax_referer()를 호출한다. CSRF 공격을 막는 기본 중의 기본. - Rate Limiting — IP당 시간당 20회. Transient API로 구현. DDoS 방어까지는 안 되지만 일반적인 남용은 막을 수 있다.
- 입력 정화 —
sanitize_textarea_field(),sanitize_text_field(),sanitize_email(). 모든 사용자 입력에 적용. - 출력 이스케이프 —
escapeHtml()으로 XSS 방지. HTML을 직접 innerHTML에 넣기 전에 반드시 이스케이프. - 관리자 권한 확인 — 로그 조회 AJAX에서
current_user_can('manage_options')체크. - SQL Injection 방지 —
$wpdb->prepare()를 사용. 직접 문자열 연결 금지.
운영 팁 — 몇 달 써보고 깨달은 것들
1. 로그를 주기적으로 분석하라
가장 자주 묻는 질문 TOP 10을 뽑아보면 패턴이 보인다. “로또 번호” 검색이 많으면 knowledgeBase에 로또 카테고리를 추가한다. “○○ 어떻게 해?” 같은 질문이 많으면 해당 서비스의 안내 문구를 개선한다. 로그가 곧 사용자 리서치다.
2. 면책 문구 처리
AI가 생성한 답변에는 부정확한 정보가 포함될 수 있다. 특히 건강, 법률, 금융 관련 질문에 AI가 답변하면 문제가 될 수 있다. 시스템 프롬프트에 “모르는 건 검색엔진을 이용하라고 안내하세요”를 넣어둔 이유다. 필요하면 AI 응답 하단에 면책 문구를 자동으로 붙이는 것도 방법이다.
3. AI 모델 선택 가이드
| 상황 | 추천 구성 | 월 비용 |
|---|---|---|
| 비용 0원으로 시작 | 규칙 기반만 (AI_ENGINE 미설정) | 0원 |
| 품질보다 비용 우선 | Ollama + qwen2.5:3b | 전기세만 |
| 균형잡힌 선택 | Ollama + qwen2.5:14b | 전기세만 |
| 품질 최우선 | Claude Haiku API | ~$5/월 |
| 최고급 | Claude Sonnet API | ~$20/월 |
4. 성능 모니터링
response_time 컬럼으로 AI 응답 시간을 추적할 수 있다. 평균이 10초를 넘기면 모델을 더 작은 것으로 바꾸거나, PC를 업그레이드해야 한다는 신호다. 사용자가 10초 이상 기다리면 이탈률이 급격히 올라간다.
전체 아키텍처 정리
5편에 걸쳐서 구현한 챗봇의 전체 흐름을 정리하면:
사용자 메시지 입력
│
▼
[JS] findResponse() — 규칙 기반 키워드 매칭
│
├── 매칭 성공 → 즉시 응답 (0.1초) + 로그 저장
│
└── 매칭 실패 → AJAX 서버 전송
│
▼
[PHP] nalkkul_chatbot_ai()
│
├── Rate Limit 체크 (IP당 20회/시간)
│
▼
[PHP] nalkkul_chatbot_search_formatted() — DB 검색
│
├── 검색 결과 있음 → 직접 포맷 후 응답 (0.3초)
│
└── 검색 결과 없음 → AI 호출
│
├── Ollama (로컬, 3~15초)
└── Claude (API, 1~3초)
│
▼
응답 + 로그 저장
│
▼
[JS] 메시지 표시 + 응답시간 표시
파일 하나(chatbot.php)에 모든 것이 담겨 있다. PHP 함수, CSS 스타일, JS 로직. 외부 의존성 없다. wp-content/mu-plugins/에 넣으면 끝이다.
앞으로의 계획
현재 구현에서 개선하고 싶은 것들:
- 스트리밍 응답 — 지금은 AI가 전체 답변을 만들 때까지 기다린다. Server-Sent Events로 토큰 단위로 스트리밍하면 체감 속도가 빨라진다.
- 벡터 검색 — MySQL LIKE 검색의 한계가 있다. “재밌는 거”로 “이상형 월드컵”을 찾지 못한다. 임베딩 기반 의미 검색을 붙이면 관련도가 올라간다.
- 사용자 피드백 — 봇 응답마다 “도움됨/안됨” 버튼을 달아서 자동으로 knowledgeBase를 개선하는 파이프라인.
- 다국어 지원 — 영어 질문도 처리할 수 있게. AI 엔진이 이미 다국어를 지원하니 UI만 바꾸면 된다.
챗봇은 한번 만들면 끝나는 프로젝트가 아니다. 로그를 보면서 계속 키워드를 추가하고, 응답을 다듬고, 모델을 교체하는 반복 작업이다. 하지만 그 과정에서 사용자가 사이트를 어떻게 쓰는지 정말 많이 배운다. 분석 도구 열 개보다 챗봇 로그 하나가 더 많은 걸 알려준다.
이 시리즈의 다른 글
- (1) 설계와 규칙 기반 엔진
- (2) 플로팅 UI와 대화 인터페이스
- (3) RAG 검색 연동으로 블로그 글 찾기
- (4) Ollama 로컬 AI 연동
- (5) 대화 로그, 문의 접수, 운영 팁 — 현재 글