개발자 소식으로 돌아가기

Creating Surveys with WhatsApp Flows

2024년 3월 6일제작:Gafi G & Iryna Wagner

WhatsApp Flows는 비즈니스가 고객 데이터를 수집하는 방식을 최적화 및 간소화합니다. 조직에서는 고객과의 상호 작용으로부터 손쉽게 정형화된 정보를 수집할 수 있고, 고객은 WhatsApp에서 긍정적인 사용자 경험을 만끽할 수 있습니다. WhatsApp Flows는 잠재 고객 확보 데이터 수집, 설문조사 진행, 고객의 예약 지원, 고객 질문과 우려 사항 제출 등의 작업에 활용할 수 있습니다.

가장 좋은 점은 복잡한 애플리케이션과 백엔드를 빌드하지 않고도 고객에게 이 모든 옵션을 제공할 수 있다는 것입니다. 프런트엔드로는 WhatsApp을 사용하는 동시에, 응답을 JSON 메시지로 캡처하고, 정보를 처리하고 필요한 데이터를 가져오도록 Webhook을 구성하면 됩니다.

이 튜토리얼에서는 가상의 기업이 WhatsApp에서 Webhook을 사용하여 고객 설문조사를 설정하는 사례를 살펴봅니다. 설문조사에서는 기존 고객과 잠재 고객에게 더 나은 서비스를 제공하는 데 도움이 되도록 고객이 기업을 발견한 방법과 고객이 선호하는 관광 유형 등의 피드백을 수집합니다.

WhatsApp Flows를 사용하여 설문조사 진행하기

필수 조건

튜토리얼을 따라서 진행하려면 다음이 필요합니다.

절차

  1. Flask 앱을 만듭니다.
  2. WhatsApp Flows API를 사용하여 Flows를 만들고 게시하는 Python 코드를 작성합니다. 이 Python 코드는 게시된 Flow를 클라우드 API를 사용하여 전송하는 작업도 수행합니다.
  3. 채팅 메시지를 수신 대기하는 Webhook을 만듭니다.
  4. 애플리케이션을 실행합니다.

프로젝트를 미리 보려면 완성된 코드를 봅니다.

WhatsApp Flows API를 사용하여 설문조사 만들기

Flow는 Flow Builder UI 또는 Flows API를 사용하여 만들 수 있습니다. 이 튜토리얼에서는 Flows API를 사용하여 프로그래밍 방식으로 설문조사를 설정합니다.

서버의 다이내믹 데이터를 사용하는 Flow를 빌드하려면 설문조사를 자체 서버에 연결하는 엔드포인트를 만듭니다. 엔드포인트를 사용하여 Flow 화면 간의 이동 로직을 제어하고, 서버의 Flow 데이터를 입력하고, 사용자 상호 작용에 따라 화면 구성 요소를 표시하거나 숨길 수 있습니다.

여기에서 다루는 설문조사 Flow 예시의 경우 Flow와 서버 사이에서 다이내믹 데이터 교환이 이루어지지 않으므로 엔드포인트를 사용하지 않습니다. 설문조사의 정보를 수집하는 데는 채팅 Webhook을 사용합니다. 추가로 WhatsApp 관리자에서 Flow를 메시지 템플릿에 연결할 수 있습니다.

Flask 앱 만들기

먼저 Flows API와 상호 작용하는 Flask 앱을 만듭니다. 터미널에서 다음 명령을 실행하여 가상 환경을 만듭니다.

python -m venv venv
        

그런 다음 아래 명령을 사용하여 환경을 활성화합니다.

source venv/bin/activate
        

다음으로, 아래 명령을 사용하여 필수 패키지를 설치합니다.

pip install requests flask python-dotenv
        

이 Flask 앱을 사용하여 Flows API와 상호 작용하는 경로를 만들고, HTTP 요청을 전송하는 요청을 만들고, 환경 변수를 읽어들이는 python-dotenv를 만들 것입니다.

.env라는 환경 파일을 만든 후 다음 정보를 붙여넣습니다.

VERIFY_TOKEN =
ACCESS_TOKEN =
WHATSAPP_BUSINESS_ACCOUNT_ID =
PHONE_NUMBER_ID =
        

개발자 계정 정보의 값을 할당합니다. VERIFY_TOKEN으로는 임의의 문자열을 사용합니다. WHATSAPP_BUSINESS_ACCOUNT_IDPHONE_NUMBER_ID 변수는 Meta에 의해 자동으로 생성된, 계정의 고유한 식별자입니다. The ACCESS_TOKEN은 API 요청의 인증 및 권한 부여에 사용됩니다.

Meta 앱의 대시보드에서 이 정보에 액세스하려면 아래 스크린샷과 같이 왼쪽 탐색창에서 WhatsApp > API 설정을 클릭합니다.

API 설정 미리 보기

마지막으로, 동일한 디렉토리에서 Flow 및 Webhook을 만드는 Python 로직을 포함하는 main.py 파일을 만듭니다.

Flow 빌드하기

Flow를 빌드하려면 먼저 main.py에 다음 패키지를 추가합니다.

import os
import uuid
import requests
from dotenv import load_dotenv
from flask import Flask, request, make_response, json
        

다음으로, main.py에 다음 코드 조각을 추가하여 변수를 초기화합니다. 이 코드 조각은 Flask를 초기화하고 변수를 읽어들이는 load_dotenv() 메서드를 호출합니다.

app = Flask(__name__)

load_dotenv()
PHONE_NUMBER_ID = os.getenv('PHONE_NUMBER_ID')
VERIFY_TOKEN = os.getenv('VERIFY_TOKEN')
ACCESS_TOKEN = os.getenv('ACCESS_TOKEN')
WHATSAPP_BUSINESS_ACCOUNT_ID = os.getenv('WHATSAPP_BUSINESS_ACCOUNT_ID')
created_flow_id = ""
messaging_url = f"https://graph.facebook.com/v18.0/{PHONE_NUMBER_ID}/messages"

auth_header = {"Authorization": f"Bearer {ACCESS_TOKEN}"}

messaging_headers = {
    "Content-Type": "application/json",
    "Authorization": f"Bearer {ACCESS_TOKEN}",
}
        

그런 다음 Flow 만들기를 처리하는 다음과 같은 경로를 추가합니다.

@app.route("/create-flow", methods=["POST"])
def create_flow():
    flow_base_url = (
        f"https://graph.facebook.com/v18.0/{WHATSAPP_BUSINESS_ACCOUNT_ID}/flows"
    )
    flow_creation_payload = {"name": "<FLOW-NAME>", "categories": '["SURVEY"]'}
    flow_create_response = requests.request(
        "POST", flow_base_url, headers=auth_header, data=flow_creation_payload
    )

    try:
        global created_flow_id
        created_flow_id = flow_create_response.json()["id"]
        graph_assets_url = f"https://graph.facebook.com/v18.0/{created_flow_id}/assets"

        upload_flow_json(graph_assets_url)
        publish_flow(created_flow_id)

        print("FLOW CREATED!")
        return make_response("FLOW CREATED", 200)
    except:
        return make_response("ERROR", 500)

이 함수는 Flows 엔드포인트 (flow_base_url)을 호출하고 이름과 Flow 카테고리를 포함하는 (flow_creation_payload) 페이로드를 전달합니다. 카테고리에 사용할 수 있는 값은 SIGN_UP, SIGN_IN, APPOINTMENT_BOOKING, LEAD_GENERATION, CONTACT_US, CUSTOMER_SUPPORT, SURVEYOTHER입니다.

<FLOW-NAME>을 원하는 이름(예: survey_flow)으로 바꿉니다.

이 코드는 Flow를 만든 후 JSON 본문을 업로드하기 위한 created_flow_id를 추출합니다.

Flow의 JSON 구성 요소 업로드하기

이 코드를 사용하여 survey.json 파일을 만듭니다. 이 JSON에는 Flow의 구조가 포함되어 있습니다.

그런 다음 아래 코드를 main.py 파일에 붙여넣습니다.

def upload_flow_json(graph_assets_url):
    flow_asset_payload = {"name": "flow.json", "asset_type": "FLOW_JSON"}
    files = [("file", ("survey.json", open("survey.json", "rb"), "application/json"))]

    res = requests.request(
        "POST",
        graph_assets_url,
        headers=auth_header,
        data=flow_asset_payload,
        files=files,
    )
    print(res.json())

이 함수는 survey.json에서 Flow 자산 엔드포인트로 JSON 데이터를 업로드합니다.

아래 코드 조각에서, 사용자가 클릭 동작을 트리거하면 on-click-action이 트리거되어 페이로드에 들어 있는 데이터가 수집됩니다. "name": "complete" 필드는 Flow가 완료되었음을 나타냅니다. 즉, Flow가 종료되고 페이로드가 Webhook 서버로 전송됩니다.

...
"on-click-action": {
    "name": "complete",
    "payload": {
        "source": "${form.source}",
        "tour_type": "${form.tour_type}",
        "tour_quality": "${form.tour_quality}",
        "decision_influencer": "${form.decision_influencer}",
        "tour_guides": "${form.tour_guides}",
        "aspects_enjoyed": "${form.aspects_enjoyed}",
        "improvements": "${form.improvements}",
        "recommend": "${form.recommend}",
        "return_booking": "${form.return_booking}"
    }
}

...
        

페이로드 개체 내의 값은 (HTML의 요소 이름 형식과 비슷한) Flow 구성 요소 또는 데이터 개체에 대응될 수 있습니다. 이러한 페이로드 값에 연결된 키가 바로 name입니다. 이는 프로그래밍 언어에서 변수가 할당되는 방식과 비슷합니다.

data-source 요소에는 값의 키로 기능하는 ID도 포함되어 있습니다. 코드는 이러한 ID가 옵션으로 사용될 수 있도록 전송합니다. 예를 들어, 사용자가 아래의 data-source에 대해 Likely를 선택하면 1이 전송됩니다. 데이터가 수신되면 데이터 소스를 매칭할 수 있습니다.

...
{
    "type": "RadioButtonsGroup",
    "required": true,
    "name": "return_booking",
    "data-source": [
        {
            "id": "0",
            "title": "Very likely"
        },
        {
            "id": "1",
            "title": "Likely"
        },
        {
            "id": "2",
            "title": "Undecided"
        },
        {
            "id": "3",
            "title": "Unlikely"
        },
        {
            "id": "4",
            "title": "Very likely"
        }
    ]
}
...
        

이 Flow에는 아래와 같이 객관식 옵션을 갖는 9개의 질문이 있습니다.

설문조사 Flow

JSON 요소의 상세 정보는 개발자 문서를 참조하세요.

Flow 게시하기

다음으로, 아래 함수를 main.py 파일에 추가하여 Flow 게시 로직을 추가합니다. 게시된 Flow는 프로덕션용으로 사용할 준비가 된 것이므로 추가로 변경 사항을 적용할 수 없습니다.

def publish_flow(flow_id):
    flow_publish_url = f"https://graph.facebook.com/v18.0/{flow_id}/publish"
    requests.request("POST", flow_publish_url, headers=auth_header)
        

이 함수는 Flow ID를 전달하고 엔드포인트를 게시합니다.

Flow 전송하기

아래 함수를 main.py 파일에 붙여넣어 Flow를 WhatsApp 사용자에게 전송합니다. 이 함수는 Flow 페이로드를 전달하고 클라우드 API의 메시지 엔드포인트를 호출합니다.

def send_flow(flow_id, recipient_phone_number):
    # Generate a random UUID for the flow token
    flow_token = str(uuid.uuid4())

    flow_payload = json.dumps(
        {
            "type": "flow",
            "header": {"type": "text", "text": "Survey"},
            "body": {
                "text": "Your insights are invaluable to us – please take a moment to share your feedback in our survey."
            },
            "footer": {"text": "Click the button below to proceed"},
            "action": {
                "name": "flow",
                "parameters": {
                    "flow_message_version": "3",
                    "flow_token": flow_token,
                    "flow_id": flow_id,
                    "flow_cta": "Proceed",
                    "flow_action": "navigate",
                    "flow_action_payload": {"screen": "SURVEY_SCREEN"},
                },
            },
        }
    )

    payload = json.dumps(
        {
            "messaging_product": "whatsapp",
            "recipient_type": "individual",
            "to": str(recipient_phone_number),
            "type": "interactive",
            "interactive": json.loads(flow_payload),
        }
    )

    requests.request("POST", messaging_url, headers=messaging_headers, data=payload)
    print("MESSAGE SENT")
        

Flow 페이로드에는 Flow 상세 정보가 포함되어 있습니다. action.parameters.flow_token 필드를 사용하여 Flow 메시지의 고유한 식별자를 전달할 수 있습니다. 식별자는 Flow가 완료되면 클라이언트에서 Webhook으로 전달됩니다. 이 튜토리얼에서는 임의의 ID(uuid)를 사용합니다. 이 코드는 action.parameters.flow_action_payload.screenSURVEY_SCREEN(사용자가 action.parameters.flow_cta를 클릭하면 표시할 화면의 ID)으로 설정합니다.

Webhook 설정하기

Webhook 로직은 비교적 간단합니다. 이 로직에는 GET 요청을 처리하는 webhook_get 함수와 POST 요청을 처리하는 webhook_post 함수가 있습니다. 이 코드는 Meta 앱에 Webhook을 추가할 때 GET 요청을 사용합니다. 성공한 경우 요청의 hub.challenge를 반환합니다. POST 요청은 메시지 페이로드를 터미널에 출력합니다.

@app.route("/webhook", methods=["GET"])
def webhook_get():
    if (
        request.args.get("hub.mode") == "subscribe"
        and request.args.get("hub.verify_token") == VERIFY_TOKEN
    ):
        return make_response(request.args.get("hub.challenge"), 200)
    else:
        return make_response("Success", 403)
        

POST 요청은 메시지 페이로드를 추출하여 처리합니다. 이 코드는 메시지 페이로드만 처리하기 때문에, 메시지가 아닌 다른 페이로드가 캡처되면 오류가 발생합니다. 따라서 페이로드가 messages 본문인지 확인하는 if 문을 사용해야 합니다. messages JSON 본문이 있는지 확인한 후 messages 페이로드에 text 본문이 있는 경우에만 보낸 사람의 전화번호를 추출하는 확인이 수행됩니다.

@app.route("/webhook", methods=["POST"])
def webhook_post():
    # checking if there is a messages body in the payload
    if (
        json.loads(request.get_data())["entry"][0]["changes"][0]["value"].get(
            "messages"
        )
    ) is not None:
        """
        checking if there is a text body in the messages payload so that the sender's phone number can be extracted from the message
        """
        if (
            json.loads(request.get_data())["entry"][0]["changes"][0]["value"][
                "messages"
            ][0].get("text")
        ) is not None:
            user_phone_number = json.loads(request.get_data())["entry"][0]["changes"][
                0
            ]["value"]["contacts"][0]["wa_id"]
            send_flow(created_flow_id, user_phone_number)
        else:
            flow_reply_processor(request)

    return make_response("PROCESSED", 200)
        

이에 더해, Flow에서 응답을 추출하여 사용자에게 돌려보내는 아래와 같은 도우미 함수 flow_reply_processor를 사용해야 합니다. Flow 응답은 RadioButtonsGroups에서 데이터를 캡처할 때 선택된 옵션 ID도 포함하므로, 함수는 ID를 대응되는 문자열 값에 매칭합니다.

def flow_reply_processor(request):
    flow_response = json.loads(request.get_data())["entry"][0]["changes"][0]["value"][
        "messages"
    ][0]["interactive"]["nfm_reply"]["response_json"]

    flow_data = json.loads(flow_response)
    source_id = flow_data["source"]
    tour_type_id = flow_data["tour_type"]
    tour_quality_id = flow_data["tour_quality"]
    decision_influencer_id = flow_data["decision_influencer"]
    tour_guides_id = flow_data["tour_guides"]
    aspects_enjoyed_id = flow_data["aspects_enjoyed"]
    improvements_id = flow_data["improvements"]
    recommend_id = flow_data["recommend"]
    return_booking_id = flow_data["return_booking"]

    match source_id:
        case "0":
            source = "Online search"
        case "1":
            source = "Social media"
        case "2":
            source = "Referral from a friend/family"
        case "3":
            source = "Advertisement"
        case "4":
            source = "Others"

    match tour_type_id:
        case "0":
            tour_type = "Cultural tour"
        case "1":
            tour_type = "Adventure tour"
        case "2":
            tour_type = "Historical tour"
        case "3":
            tour_type = "Wildlife tour"

    match tour_quality_id:
        case "0":
            tour_quality = "1 - Poor"
        case "1":
            tour_quality = "2 - Below Average"
        case "2":
            tour_quality = "3 - Average"
        case "3":
            tour_quality = "4 - Good"
        case "4":
            tour_quality = "5 - Excellent"

    match decision_influencer_id:
        case "0":
            decision_influencer = "Positive reviews"
        case "1":
            decision_influencer = "Pricing"
        case "2":
            decision_influencer = "Tour destinations offered"
        case "3":
            decision_influencer = "Reputation"

    match tour_guides_id:
        case "0":
            tour_guides = "Knowledgeable and friendly"
        case "1":
            tour_guides = "Knowledgeable but not friendly"
        case "2":
            tour_guides = "Friendly but not knowledgeable"
        case "3":
            tour_guides = "Neither of the two"
        case "4":
            tour_guides = "I didn’t interact with them"

    match aspects_enjoyed_id:
        case "0":
            aspects_enjoyed = "Tourist attractions visited"
        case "1":
            aspects_enjoyed = "Tour guide's commentary"
        case "2":
            aspects_enjoyed = "Group dynamics/interaction"
        case "3":
            aspects_enjoyed = "Activities offered"

    match improvements_id:
        case "0":
            improvements = "Tour itinerary"
        case "1":
            improvements = "Communication before the tour"
        case "2":
            improvements = "Transportation arrangements"
        case "3":
            improvements = "Advertisement"
        case "4":
            improvements = "Accommodation quality"

    match recommend_id:
        case "0":
            recommend = "Yes, definitely"
        case "1":
            recommend = "Yes, but with reservations"
        case "2":
            recommend = "No, I would not"

    match return_booking_id:
        case "0":
            return_booking = "Very likely"
        case "1":
            return_booking = "Likely"
        case "2":
            return_booking = "Undecided"
        case "3":
            return_booking = "Unlikely"

    reply = (
        f"Thanks for taking the survey! Your response has been recorded. This is what we received:\n\n"
        f"*How did you hear about our tour company?*\n{source}\n\n"
        f"*Which type of tour did you recently experience with us?*\n{tour_type}\n\n"
        f"*On a scale of 1 to 5, how would you rate the overall quality of the tour?*\n{tour_quality}\n\n"
        f"*What influenced your decision to choose our tour company?*\n{decision_influencer}\n\n"
        f"*How knowledgeable and friendly were our tour guides?*\n{tour_guides}\n\n"
        f"*What aspects of the tour did you find most enjoyable?*\n{aspects_enjoyed}\n\n"
        f"*Were there any aspects of the tour that could be improved?*\n{improvements}\n\n"
        f"*Would you recommend our tour company to a friend or family member?*\n{recommend}\n\n"
        f"*How likely are you to book another tour with us in the future?*\n{return_booking}"
    )

    user_phone_number = json.loads(request.get_data())["entry"][0]["changes"][0][
        "value"
    ]["contacts"][0]["wa_id"]
    send_message(reply, user_phone_number)

After the extraction, the following send_message function sends the responses to the sender.

def send_message(message, phone_number):
    payload = json.dumps(
        {
            "messaging_product": "whatsapp",
            "to": str(phone_number),
            "type": "text",
            "text": {"preview_url": False, "body": message},
        }
    )

    requests.request("POST", messaging_url, headers=messaging_headers, data=payload)
    print("MESSAGE SENT")
        

함수는 클라우드 API 텍스트 메시지 엔드포인트를 사용하여 응답을 전송합니다.

Meta for Developers 콘솔에서 Webhook을 구성하려면 애플리케이션이 실행되고 있어야 합니다. 터미널에서 flask --app main run --port 5000 명령을 사용하여 코드를 실행합니다. 모든 것이 올바르게 설정되었다면 터미널에 * Running on http://127.0.0.1:5000 메시지가 출력됩니다.

다음으로, 터미널에서 ngrokhttp 5000 명령을 실행하여 애플리케이션에 매핑되는 URL을 가져옵니다. 링크를 복사합니다.

Meta for Developers 콘솔의 왼쪽 탐색창에 있는 WhatsApp 아래에서 구성을 클릭합니다.

WhatsApp 구성

Webhook 카드에서 수정을 클릭합니다. 그런 다음 콜백 URL 필드의 대화 상자에 복사한 URL을 넣고 그 뒤에 /webhook을 추가합니다. 확인 토큰 필드에 .env 파일의 TOKEN 변수에서 확인한 토큰을 추가합니다.

작업을 마쳤으면 확인 및 저장을 클릭합니다. 대화 상자가 닫힙니다. 관리를 클릭하고 메시지 필드를 선택합니다. 아래 이미지와 같이 콜백 URL, 확인 토큰 아래의 숨겨진 정보, Webhook 필드 아래의 메시지가 표시됩니다.

WhatsApp 구성

이제 Webhook이 준비되었습니다.

애플리케이션 실행하기

새 터미널 인스턴스에서 아래 cURL 명령을 실행하여 Flow를 만듭니다.

curl --location --request POST 'http://127.0.0.1:5000/create-flow'
        

Webhook 출력이 표시된 터미널 인스턴스에서 아래와 같은 메시지를 볼 수 있습니다.

애플리케이션 실행하기

사용자가 비즈니스의 WhatsApp 번호로 메시지를 보내면 아래 스크린샷과 같이 답장으로 Flow를 수신합니다.

Flow 설문조사 프롬프트 예시

사용자가 설문조사를 완료하면 답변이 채워진 응답을 수신합니다.

Flow 설문조사 응답 예시

사용자가 비즈니스의 번호로 보내는 모든 메시지가 Flow를 트리거합니다. 비즈니스의 사용 사례에 맞게 특정 상황에서만(예: 사용자가 비즈니스와 채팅한 경우) 설문조사 Flow가 전송되도록 코드를 맞춤 설정하세요.

결론

WhatsApp Flows는 인터랙티브 인터페이스를 통해 가상의 관광 업체를 위한 설문조사 응답과 같은 정형 데이터를 수집함으로써 사용자 경험을 향상합니다. 비즈니스는 Flask 앱을 만들고, 생성할 Python 코드를 구현하고, WhatsApp Flows API를 통해 Flows를 배포하고, 수신 메시지를 수신 대기하는 Webhook을 설정하고, 애플리케이션을 실행하여 설문조사 응답 수집을 설정하기만 하면 됩니다.

조직에서는 WhatsApp Flows를 사용하여 쉽고 빠르게 데이터를 수집함으로써 고객 상호 작용의 응답률을 높일 수 있습니다. 위와 비슷한 절차를 사용하여 자체 설문조사를 설정하고, 고객 지원을 제공하고, 고객의 예약을 지원하세요.

계속해서 WhatsApp Flows의 더 많은 기능을 살펴보세요. 지금 사용해보세요.