본문 바로가기

문돌이 존버/카카오 챗봇 스터디

카카오 i 오픈 빌더를 이용한 챗봇 만들기 feat. Django

반응형

최근 카카오 i 오픈 빌더를 사용해서 챗봇을 만드는 프로젝트를 진행 중인데요. 사용자가 증상을 말하면 그에 적절한 약품을 추천하는 챗봇을 개발하고 있답니다. 스킬서버는 다른 프로젝트를 진행하며 조금은 익숙해진 장고(Django)를 사용하기로 결정했습니다. 다만, 카카오 i 오픈 빌더는 처음 써보는 것이기 때문에 어떤 구조로 이루어져있는지 파악하는 데 애를 먹었네요. 특히 이런 Json 구조는 파이썬만 해오던 저에게 어려웠습니다 ㅠㅠ

기본적인 카카오 i 오픈 빌더 사용법은 공식 문서에 잘 나와있기 때문에 따로 설명드리지 않겠습니다. 웰컴 블록, 폴백 블록, 시나리오 추가 등은 간단하기 때문에 잘 아실 것이라 생각하고 파라미터 설정이나 봇 응답 스킬데이터 사용 등에 대해 차차 설명드리겠습니다.

예시를 먼저 보여드리고 이야기를 하는 것이 좋을 듯 합니다. 아래는 스킬데이터를 사용한 버전이고 출력 형태는 카카오 i 오픈 빌더에서 제공하는 캐러셀 기본 이미지 카드형을 이용한 것입니다. 코드 하나하나에 대해 설명이 필요한 부분은 주석으로 달아놓았으니 참고 바랍니다. 먼저, 가장 기본적인 사용자 발화 내용을 읽어서 적절한 반응을 하는 코드입니다.

from django.shortcuts import render
from django.http import JsonResponse # 카카오톡과 연동하기 위해선 JsonResponse로 출력
from django.views.decorators.csrf import csrf_exempt # 보안 이슈를 피하기 위한 csrf_exempt decorator 필요
import json
from .models import *

# JsonResponse 출력 테스트용 
def keyboard(request): 
    return JsonResponse({
        'type': 'text'
    })

@csrf_exempt
def medicine(request):
    answer = ((request.body).decode('utf-8'))
    return_json_str = json.loads(answer) # json 포맷으로 카카오톡 내용 받아오기
    return_str = return_json_str['userRequest']['utterance'] # 사용자 발화 추출 
    idx = return_str.index('을')
    disease = return_str[:idx]
    manu = '제조사: '
    effect = '효능효과 알아보기'
    usage = '복용법 알아보기'
    precautions = '주의사항 알아보기'

    if return_str == disease + '을 위한 약품': # 사용자 발화 예상
        can = TBL_MEDICINE_INFO.objects.filter(disease=disease) # 장고와 연동된 MySQL에서 데이터 가져오기
        return JsonResponse({
                 "version": "2.0",
                 "template": {
                 "outputs": [
                {
                     "carousel": { # 스킬가이드에 나온 캐러셀 형태
                         "type": "basicCard", # 기본형 선택 (<->비즈니스형도 존재)
                         "items": [
                {
                             "title": can[0].medicine, # 제목
                             "description": manu + can[0].manufacturer, # 설명
                             "thumbnail": { # 썸네일 이미지
                                 "imageUrl": can[0].imgurl
              },
                             "buttons": [ # 버튼
                {
                                 "action": "message", # 동작 형태(텍스트 출력)
                                 "label": "효능효과", # 버튼 이름
                                 "messageText": can[0].medicine + ' ' + effect
                },
                {
                                 "action": "message",
                                 "label": "복용법",
                                 "messageText": can[0].medicine + ' ' + usage
                },
                {
                                 "action": "message",
                                 "label": "주의사항",
                                 "messageText": can[0].medicine + ' ' + precautions
                }
              ]
            },
            {
                             "title": can[1].medicine,
                             "description": manu + can[1].manufacturer,
                             "thumbnail": {
                                 "imageUrl": can[1].imgurl
              },
                             "buttons": [
                {
                                 "action": "message",
                                 "label": "효능효과",
                                 "messageText": can[1].medicine + ' ' + effect
                },
                {
                                 "action": "message",
                                 "label": "복용법",
                                 "messageText": can[1].medicine + ' ' + usage
                },
                {
                                 "action":  "message",
                                 "label": "주의사항",
                                 "messageText": can[1].medicine + ' ' + precautions
                }
              ]
            },
            {
                             "title": can[2].medicine,
                             "description": manu + can[2].manufacturer,
                             "thumbnail": {
                                 "imageUrl": can[2].imgurl
              },
                             "buttons": [
                {
                                 "action": "message",
                                 "label": "효능효과",
                                 "messageText": can[2].medicine + ' ' + effect
                },
                {                "action": "message",
                                 "label": "복용법",
                                 "messageText": can[2].medicine + ' ' + usage
                },
                {
                                 "action":  "message",
                                 "label": "주의사항",
                                 "messageText": can[2].medicine + ' ' + precautions
                }
              ]
            }
          ]
        }
      }
    ]
  }
})

장고는 CSRF 문제를 잘 처리하고 있는 프레임워크인데요. CSRF란 Cross-site request forgery의 약자로, 웹사이트 취약점 공격 중 하나를 가리킵니다. 장고는 보안 문제를 위해 토큰(token)을 전달해줘야 POST 방식에 의한 요청을 정상적으로 수행하게 되는데, 저는 데코레이터(decorator) 메서드인 @csrf_exempt 를 사용하여 토큰이 굳이 필요하지 않게 처리했습니다.

물론, 이 방식이 편리하지만 보안 측면에서는 문제가 발생할 수 있어 실서비스를 배포할 경우 주의해야겠죠. 저는 실서비스 배포가 아닌 개발 공부다 보니 그냥 넘어가겠습니다. csrf token을 설정하여 보안 이슈를 해결하는 방법은 장고 공식 문서에 잘 나와있으니 참고하면 되겠습니다.

아래는 또 다른 방식으로 카카오톡 대화 내용을 받아오는 코드인데요. 위에서 사용한 ['userRequest']['utterance']가 아니라 ['action']['detailParams']를 통해 필요한 부분을 추출하는 것입니다. 

@csrf_exempt
def recommend(request):
    answer = ((request.body).decode('utf-8'))
    return_json_str = json.loads(answer)
    return_act = return_json_str['action']['detailParams'] # 파라미터 설정에 따라 전달되는 부분
    symptom = return_act['symptom_name']['value'] # symptom_name 파라미터의 내용 가져오기

    if symptom:
        return JsonResponse({
            'version': '2.0',
            'template': {
                'outputs': [{
                    'simpleText': {
                        'text': '{}을 겪으시는군요.\n 약과 음식 중 무엇을 추천해드릴까요?'.format(symptom) 
                        }
                    }],
                'quickReplies': [{ # 위의 메시지 하단에 뜨는 타원형 형태의 버튼 
                    'label': '약품',
                    'action': 'message',
                    'messageText': '{}을 위한 약품'.format(symptom)
                    },
                    {
                    'label': '음식',
                    'action': 'message',
                    'messageText': '{}을 위한 음식'.format(symptom)
                    }]
                }
            })

여기서 주의해서 봐야할 것은 ['symptom_name'] 부분인데 이는 제가 별도로 설정한 파라미터입니다. 카카오 i 오픈 빌더 내 시나리오를 설정하는 화면에서 "파라미터 설정"을 볼 수 있는데요. 기본적으로 카카오 i 오픈 빌더에서 제공하는 엔티티(해외도시, 통화 이름, 날짜 등)에 따라 파라미터를 설정할 수도 있지만 원하는 엔티티가 없을 경우 스스로 만들 수 있습니다.

카카오 i 오픈 빌더에서 기본적으로 제공되는 엔티티

제가 원하는 파라미터를 만들기 위해선 엔티티 등록을 먼저 해주어야 하고, 이를 바탕으로 "파라미터명"과 "값"을 설정하면 카카오톡 내 사용자의 발화 중 해당하는 파라미터가 있으면 캐치해서 전달해줍니다. 엔티티 값의 경우, 파라미터명 앞에 기호 $를 붙이는데요, 이외에 특정 값, 고정된 값 등 다양한 방식이 있으니 공식 문서를 참고해주세요. 

마지막으로 "봇 응답" 중 스킬데이터를 사용하지 않고도 코드를 작성하여 설정한 변수를 가져올 수 있는데요. 말로만 설명하면 이해가 힘드니 바로 예제를 보여드리겠습니다. 

봇 응답 포맷은 위와 같이 "텍스트", "이미지", "카드", "커머스", "리스트", "스킬데이터"로 다양하게 존재합니다. 이것저것 시도하다보니 발견한 스킬데이터를 사용할 때의 단점은 여러 개의 메시지를 출력할 수 없다는 것입니다. 봇 응답은 총 3가지 응답을 출력할 수 있는데, 위에 보시다시피 "첫 번째 응답 - 텍스트형" 처럼 두 번째, 세 번째 응답은 다른 형태가 가능합니다. 하지만 스킬데이터를 사용할 경우 첫 번째 응답만 출력 가능하고 두 번째, 세 번째 응답을 추가할 수 없습니다.

카카오 i 오픈 빌더에 공식 문의를 한 결과, 스킬데이터를 사용하면서 가능한 복수 응답 출력 방법을 가르쳐주는 대신 웹훅 객체(webhook)을 이용한 프론트 응답형식을 소개했습니다. 저도 고집을 부리지 않고 웹훅 객체를 사용하기로 결정했습니다. 사용 방법은 아래 코드를 보면 알 수 있습니다.

@csrf_exempt
def showmap(request):
    answer = ((request.body).decode('utf-8'))
    return_json_str = json.loads(answer)
    return_act = return_json_str['action']['detailParams']
    location = return_act['location']['value'] # 기본으로 제공되는 위치 엔티티 사용
    
    if location:
        return JsonResponse({
            'version': '2.0',
            'data': {
                'location': 'https://m.map.kakao.com/actions/searchView/?q=' + location + '%20약국#!/all/map/place'
                }
            })

위 2가지 코드와 다른 점은 return JsonResponse 뒷부분입니다. 'template', 'outputs' 등 대신 매우 간단하게 'data' 파라미터와 내부 변수('location')만 설정해주면 웹훅 객체를 사용할 수 있습니다. 이때 변수명은 파라미터명과 동일하게 맞출 필요 없이 본인 마음대로 설정하셔도 됩니다. 저의 경우 파라미터명과 똑같은데 'location' 대신 'region', 'mytown' 등 다양하게 선언할 수 있습니다. 

이후에 카카오 i 오픈 빌더로 돌아와 {{#webhook.변수명}} 형태로 해당 파라미터의 내용을 받아오면 되겠습니다. 'data' 파라미터 내부에 여러 개의 변수를 설정할 수 있으니 개수는 자유롭게 결정하시면 됩니다. 웹훅 객체를 사용하면 응답방식, 버튼 등 모든 파라미터를 포함하여 코드를 작성해줘야 하는 스킬데이터보다 편하긴 합니다.

저는 카카오톡 발화 내용을 가져올 때 코드에 나타난 2가지 방식 이외의 다른 파라미터를 사용하지 않았습니다. 즉 ['userRequest']['utterance']와 ['action']['detailParams'] 만 사용하여 정보를 가져오고 적절한 반응을 출력한 것이죠. 사람마다 필요한 파라미터도 다르고, 스킬데이터를 사용할 경우 단순 텍스트를 출력할 것이냐, 이미지 카드를 사용할 것이냐, 버튼을 몇 개 사용할 것이냐 등 모두 다르기 때문에 구체적 사항은 역시 카카오 i 오픈 빌더 공식 문서를 참고하셔야 합니다. 제가 제공한 코드는 '이런 형태로 작성해야 하는구나' 정도로만 생각하시고 세부 사항은 본인의 프로젝트에 맞게 커스터마이징 하시길 바랍니다. 

728x90
반응형