개발자 소식으로 돌아가기

WhatsApp Flows를 사용하여 예약 진행하기: Node.js 백엔드 만들기

2024년 2월 27일제작:개피 G(Gafi G), 이리나 바그너(Iryna Wagner)

WhatsApp Flows를 사용하면 사용자가 WhatsApp에서 곧바로 작업을 완료하도록 지원하는 인터랙티브 메시지를 만들 수 있습니다. 플로를 사용하여 사용자 상호 작용을 위한 화면을 만들 수 있습니다. 예를 들어, 잠재 고객 정보 수집이나 리뷰 제출을 위한 간단한 입력 양식을 만들 수 있습니다. 여러 화면에 걸친 복잡한 예약 진행 플로를 설계할 수도 있습니다.

이 가이드에서는 사용자가 WhatsApp Flows를 통해 예약을 진행하도록 지원하는 Node.js 앱을 만드는 방법을 단계별로 안내합니다. 먼저 WhatsApp Business 플랫폼에서 플로를 만든 다음, 플로의 응답을 수신하고 예약을 진행하는 Webhook을 구성합니다.

필수 조건

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

WhatsApp 플로 만들기

WhatsApp 플로는 플로 빌드 도구 UI(WhatsApp 관리자를 통해 액세스) 또는 플로 API를 사용하여 만들 수 있습니다. 이 튜토리얼에서는 플로 빌드 도구를 사용합니다.

플로 만들기

WhatsApp 관리자 대시보드 왼쪽 메뉴에서 계정 도구를 선택합니다. 그런 다음 Flows를 클릭합니다.

WhatsApp 관리자 그래픽

오른쪽 상단에서 플로 만들기를 클릭합니다.

플로 만들기 그래픽

표시되는 대화 상자에서 예약 플로의 상세 정보를 입력합니다.

  • 이름 — BookAppointment 또는 원하는 이름을 입력합니다.
  • 카테고리 — 예약을 선택합니다.
  • 템플릿 — 예약하기를 선택합니다. 이 템플릿에는 예약 진행을 위한 필수 요소가 포함되어 있습니다. 이러한 필수 요소에는 예약 상세 정보 화면, 사용자 상세 정보 입력 화면, 예약 요약 화면, 회사 약관 표시 화면 등이 있습니다. 사용 사례에 맞게 템플릿을 추가로 맞춤 설정할 수 있습니다.
예약하기 그래픽

제출을 클릭하여 플로를 만듭니다.

빌드 도구 UI의 오른쪽에서 플로를 미리 볼 수 있습니다. 예약 화면에서는 사용자가 위치, 날짜와 같은 예약 상세 정보를 선택할 수 있습니다. 상세 정보 화면에서는 사용자가 정보를 입력합니다. 요약 화면에는 예약 요약이 표시됩니다. 마지막 화면에는 회사 약관이 표시됩니다.

플로를 수정하는 중에는 플로가 임시 저장본 상태로 유지됩니다. 플로는 현재 테스트 용도로 팀 내에서만 공유할 수 있습니다. 플로를 다른 공개 대상에게 공유하려면 게시해야 합니다. 단, 게시한 플로는 수정할 수 없습니다. 이 예약 플로에는 아직 엔드포인트 URL을 추가해야 하므로 임시 저장본 상태로 남겨두고 엔드포인트를 구성하는 다음 단계로 넘어가겠습니다.

플로의 엔드포인트 구성하기

WhatsApp Flows에서는 외부 엔드포인트에 연결할 수 있습니다. 이 엔드포인트는 플로를 위한 동적 데이터를 제공하며 경로를 제어합니다. 또한 플로로부터 사용자가 제출한 응답을 수신합니다.

이 가이드에서는 테스트 용도로 Glitch를 사용하여 엔드포인트를 호스팅합니다. Glitch를 사용하는 것은 전적으로 선택 사항이며, Flows를 사용하기 위한 필수 사항이 아닙니다. GitHub의 엔드포인트 코드를 복제하여 원하는 환경에서 실행할 수 있습니다.

Glitch의 엔드포인트 코드에 액세스한 다음 이를 리믹스하여 고유한 도메인을 가져옵니다. 리믹스하려면 페이지 상단에서 리믹스를 클릭합니다. Glitch 페이지 오른쪽의 입력 요소에 고유한 도메인이 자리 표시자로 표시됩니다.

다음으로 넘어가기 전에 코드를 살펴보겠습니다. src 디렉토리에는 4개의 JavaScript 파일이 있습니다(encryption.js, flow.js, keyGenerator.js, server.js). 첫 번째 진입점 파일인 server.js부터 살펴보겠습니다.

server.js

server.js 파일은 먼저 Express 애플리케이션이 express.json 미들웨어를 사용하여 수신 JSON 요청을 구문 분석하도록 구성합니다. 그런 다음 엔드포인트에 필요한 환경 변수를 읽어들입니다.

const { APP_SECRET, PRIVATE_KEY, PASSPHRASE, PORT = "3000" } = process.env;

APP_SECRET은 서명 인증에서 사용됩니다. 메시지가 WhatsApp을 통해 수신된 것인지, 즉 처리하기에 안전한 것인지 검사합니다. APP_SECRET은 .env 파일에 추가합니다.

APP_SECRET에 액세스하려면 Meta for Developers의 앱 대시보드로 이동합니다. 왼쪽 탐색 메뉴의 앱 설정 아래에서 기본을 선택합니다. 앱 시크릿 아래에서 표시를 클릭하고 시크릿을 복사합니다. 그런 다음 Glitch로 돌아가서 .env 파일을 열고 방금 복사한 시크릿의 값을 사용하여 변수 APP_SECRET을 만듭니다.

PRIVATE_KEY는 수신된 메시지를 해독합니다. PASSPHRASE는 비공개 키를 인증하는 데 사용됩니다. 비공개 키를 사용할 때는 여기에 대응되는 공개 키가 필요합니다. 공개 키는 뒷부분에서 업로드할 것입니다. 여기서는 프로덕션 계정에서 사용하는 비공개 키를 사용하지 마세요. Glitch에서의 테스트를 위한 임시 비공개 키를 만들어서 비즈니스 인프라에서 프로덕션 키 대신 사용하세요.

Glitch 터미널에서 아래 명령을 실행하여 공개-비공개 키 쌍을 생성합니다. <your-passphrase>를 지정된 비밀 문구(passphrase)로 바꿉니다. 페이지 하단에서 터미널 탭을 클릭하여 Glitch 터미널에 액세스합니다.

node src/keyGenerator.js <your-passphrase>

비밀 문구와 비공개 키를 복사하여 .env 파일에 붙여넣습니다. 왼쪽 사이드바에서 .env 파일을 클릭한 다음 상단의 ✏️ 일반 텍스트를 클릭합니다. UI에서 곧바로 수정하면 키 서식이 변경되니 주의하세요.

환경 변수를 설정한 후에는 앞에서 생성한 공개 키를 복사하여 그래프 API를 통해 공개 키를 업로드합니다.

server.js 파일에는 다음과 같은 여러 단계를 수행하는 POST 엔드포인트도 들어 있습니다.

  • 비공개 키가 있는지 확인합니다.
       if (!PRIVATE_KEY) { throw new Error('Private key is empty. Please check your env variable "PRIVATE_KEY".'); }
  • 파일 하단에 있는 isRequestSignatureValid 함수를 사용하여 요청 서명을 검증합니다.
if(!isRequestSignatureValid(req)) { // Return status code 432 if request signature does not match. // To learn more about return error codes visit: https://developers.facebook.com/docs/whatsapp/flows/reference/error-codes#endpoint_error_codes return res.status(432).send(); }
  • encryption.js 파일의 decryptRequest 함수를 사용하여 수신 메시지를 해독합니다.
      let decryptedRequest = null; try { decryptedRequest = decryptRequest(req.body, PRIVATE_KEY, PASSPHRASE); } catch (err) { console.error(err); if (err instanceof FlowEndpointException) { return res.status(err.statusCode).send(); } return res.status(500).send(); } const { aesKeyBuffer, initialVectorBuffer, decryptedBody } = decryptedRequest; console.log("💬 Decrypted Request:", decryptedBody);
  • 사용자에게 표시할 플로 화면을 정합니다. getNextScreen 함수는 뒤에서 자세히 살펴볼 것입니다.

const screenResponse = await getNextScreen(decryptedBody);

       console.log("👉 Response to Encrypt:", screenResponse);
  • 사용자에게 전송할 응답을 암호화합니다.
res.send(encryptResponse(screenResponse, aesKeyBuffer, initialVectorBuffer));

encryption.js

이 파일에는 보안을 위해 교환되는 메시지를 암호화하고 해독하는 로직이 들어 있습니다. 이 튜토리얼에서는 이 파일의 내용을 살펴보지 않습니다.

keyGenerator.js

이 파일은 앞에서 본 것처럼 비공개 키와 공개 키를 생성합니다. 이 튜토리얼에서는 encryption.js 파일과 마찬가지로 keyGenerator.js 파일의 내용을 살펴보지 않습니다.

flow.js

이 파일에는 플로 처리 로직이 들어 있습니다. 이 파일은 이름이 SCREEN_RESPONSES인 개체로 시작합니다. 개체에는 화면 ID와 각각의 상세 정보(예: 데이터 교환에 사용되는 기본 설정 데이터)가 들어 있습니다. 이 개체는 플로 빌드 도구의 "..." > 엔드포인트 > 조각 > 응답에서 생성됩니다. 이 개체에는 플로가 성공적으로 완료되면 클라이언트 기기로 전송되는 ID인 SUCCESS도 들어 있습니다. SUCCESS가 전송되면 플로가 닫힙니다.

getNextScreen 함수에는 사용자에게 표시할 플로 데이터를 엔드포인트에 알려주는 로직이 들어 있습니다. 이 함수는 해독된 메시지에서 필수 데이터를 추출하는 것으로 시작합니다.

const { screen, data, version, action, flow_token } = decryptedBody;

WhatsApp Flows 엔드포인트는 일반적으로 다음과 같은 3가지 요청을 수신합니다.

각 요청의 상세 정보는 엔드포인트 문서를 참조하세요.

이 함수는 아래 코드에 나와 있는 것처럼 if 문을 사용하여 상태 점검과 오류 알림을 처리하고 그 결과에 따라 응답합니다.

// handle health check request if (action === "ping") { return { version, data: { status: "active", }, }; } // handle error notification if (data?.error) { console.warn("Received client error:", data); return { version, data: { acknowledged: true, }, }; }
        

사용자가 플로의 행동 유도(CTA) 버튼을 클릭하면 INIT 액션이 트리거됩니다. 이 액션은 예약 화면과 데이터를 반환합니다. 또한 사용자가 모든 필드를 입력할 수 있도록 위치, 날짜, 시간 드롭다운을 비활성화합니다.

예를 들어, 날짜 드롭다운은 위치 드롭다운이 작성된 경우에만 활성화됩니다. 필드의 활성화 및 비활성화는 data_exchange 요청이 수신된 경우에 처리됩니다.

// handle initial request when opening the flow and display APPOINTMENT screen if (action === "INIT") { return { ...SCREEN_RESPONSES.APPOINTMENT, data: { ...SCREEN_RESPONSES.APPOINTMENT.data, // these fields are disabled initially. Each field is enabled when previous fields are selected is_location_enabled: false, is_date_enabled: false, is_time_enabled: false, }, }; }

data_exchange 액션의 경우, switch case 문을 통해 화면 ID에 따라 어떤 데이터를 전송할지 정해집니다. 화면 ID가 APPOINTMENT이면 앞에 나온 드롭다운이 모두 선택된 경우에만 드롭다운 필드가 활성화됩니다.

// Each field is enabled only when previous fields are selected is_location_enabled: Boolean(data.department), is_date_enabled: Boolean(data.department) && Boolean(data.location), is_time_enabled: Boolean(data.department) && Boolean(data.location) && Boolean(data.date)

DETAILS 화면의 경우, SCREEN_RESPONSES.APPOINTMENT.data 개체에서 데이터 개체 속성의 제목(위치, 부서 등)이 추출됩니다. 이 코드는 유효한 매칭이 존재한다고 가정하기 때문에 매칭되는 개체가 없는 경우 오류가 발생할 수 있습니다.

이제 위치 개체 인스턴스를 살펴보겠습니다. 특정 위치 개체의 선택 여부는 배열에 들어 있는 개체의 id 속성을 data.location의 값에 매칭하여 정해집니다.

const departmentName = SCREEN_RESPONSES.APPOINTMENT.data.department.find( (dept) => dept.id === data.department ).title; const locationName = SCREEN_RESPONSES.APPOINTMENT.data.location.find( (loc) => loc.id === data.location ).title; const dateName = SCREEN_RESPONSES.APPOINTMENT.data.date.find( (date) => date.id === data.date

).title;

그런 다음 값이 연결되고 응답으로 반환되어 SUMMARY 화면이 렌더링됩니다.

const appointment = `${departmentName} at ${locationName} ${dateName} at ${data.time}`; const details = `Name: ${data.name} Email: ${data.email} Phone: ${data.phone} "${data.more_details}"`; return { ...SCREEN_RESPONSES.SUMMARY, data: { appointment, details, // return the same fields sent from client back to submit in the next step ...data, }, };
        

클라이언트에서 SUMMARY 화면이 제출되면 클라이언트 기기로 SUCCESS 응답이 전송되어 플로가 완료로 표시됩니다. flow_token은 사용자에게 플로를 전송할 때 설정할 수 있는 고유한 식별자입니다.

// send success response to complete and close the flow return { ...SCREEN_RESPONSES.SUCCESS, data: { extension_message_response: { params: { flow_token, }, }, }, };

TERMS 화면에는 교환해야 하는 데이터가 없으므로 이 화면은 엔드포인트가 처리하지 않습니다.

플로에 엔드포인트 추가하기

Glitch 페이지 상단에서 kebab 메뉴 아이콘을 클릭하고 링크 복사를 선택하여 URL을 복사할 수 있습니다. 오른쪽 상단에서 공유를 클릭하여 링크를 가져올 수도 있습니다.

플로 에디터로 이동합니다. 에디터 상단에 표시되는 갈색 배너에서 설정을 클릭합니다.

엔드포인트 URI, 비즈니스 전화번호, Meta for Developers의 앱을 구성할 수 있는 팝업이 표시됩니다. 필수 구성을 적용한 후에는 상태 점검을 수행합니다. 먼저 인터랙티브 미리 보기를 실행하고 인터랙티브 미리 보기 설정의 첫 번째 화면에서 데이터 요청 아래에서 데이터 요청을 선택합니다. 이렇게 하면 엔드포인트를 사용할 수 있고 상태 점검이 구현되었는지 검증한 후 첫 번째 화면의 데이터를 가져오는 요청이 엔드포인트로 전송됩니다.

그런 다음 미트볼(...) 메뉴를 클릭하고 게시를 선택하여 플로를 게시합니다. 이렇게 하면 게시에 앞서 엔드포인트를 검증하기 위해 action === "ping"과 함께 상태 점검 요청이 엔드포인트로 전송됩니다.

엔드포인트 그래프

플로 테스트하기

구성을 완료한 후에는 WhatsApp 빌드 도구 UI에서 다시 한번 인터랙티브 미리 보기를 토글하여 플로를 테스트합니다. 표시되는 팝업에서 전화번호를 선택하고 첫 번째 화면에서 데이터 요청 아래에서 데이터 요청을 선택합니다. X 아이콘을 클릭하여 플로를 닫으면 CTA 버튼을 사용하여 플로 테스트를 새로 시작할 수 있습니다.

로그 탭을 클릭하여 Glitch 로그를 엽니다. 지우기를 클릭하여 로그를 지웁니다. 그런 다음 WhatsApp 빌드 도구 UI 미리 보기로 돌아갑니다. 플로 미리 보기를 클릭합니다. 다음과 같은 화면을 볼 수 있습니다.

플로 미리 보기 그래픽

이제 Glitch 로그로 돌아갑니다. Decrypted Request 아래에서 INIT 액션, 플로 토큰 및 기타 상세 정보를 볼 수 있습니다. 부서 드롭다운이 선택되면 사용자 플로로 전송되는 Response to Encrypt도 볼 수 있습니다.

해독된 요청 그래픽

부서를 선택합니다. 그러면 is_location_enabledtrue로 설정되고 액션이 data_exchange로 변경되는 것을 볼 수 있습니다.

데이터 교환 그래픽

계속해서 플로를 사용해보며 Glitch 로그에서 데이터가 바뀌는 것을 살펴봅니다. 사용자가 모바일 기기에서 플로와 상호 작용할 때도 이와 비슷한 로그가 생성됩니다.

다음 섹션에서는 사용자가 예약을 진행하면 사용자에게 확인 메시지를 전송하는 Webhook을 만들어봅니다.

Webhook 설정하기

사용자가 플로를 완료하면 플로를 완료로 표시하는 메시지가 구독된 Webhook으로 전송됩니다. 이 Webhook을 사용하여 사용자에게 채팅 메시지를 통해 예약이 성공했음을 알립니다. 엔드포인트의 경우와 마찬가지로 Glitch를 사용하여 테스트해보겠습니다. 여기에서 코드와 리믹스를 확인할 수 있습니다.

Glitch를 사용하는 것은 전적으로 선택 사항이며, Flows를 사용하기 위한 필수 사항이 아닙니다. GitHub의 엔드포인트 코드를 복제하여 원하는 환경에서 실행할 수 있습니다.

환경 변수 설정하기

환경 변수를 설정하려면 Glitch에서 .env 파일을 엽니다. VERIFY_TOKEN을 원하는 문자열로, FLOW_ID를 플로우의 ID로, GRAPH_API_TOKEN을 WhatsApp Business 계정의 액세스 토큰으로 설정합니다. 왼쪽 탐색 메뉴의 WhatsApp 섹션 아래에서 API 설정을 클릭하여 Meta for Developers의 앱 대시보드에서 액세스 토큰을 확인할 수 있습니다.

API 설정 그래픽

렌더링되는 페이지에서 임시 액세스 토큰 카드 아래의 복사 버튼을 클릭합니다. .env 파일에 키를 붙여넣습니다.

Meta 대시보드에서 Webhook 구독하기

Meta for Developers 계정에서 왼쪽 탐색 패널에 있는 WhatsApp 아래의 구성 메뉴를 클릭합니다.

구성 그래픽

Webhook 카드에서 수정을 클릭합니다. 표시되는 대화 상자의 콜백 URL 필드에 복사한 Glitch URL을 붙여넣고 그 뒤에 /webhook을 추가합니다. 확인 토큰 필드에 .env 파일의 VERIFY_TOKEN 변수에서 확인한 토큰을 추가합니다. 작업을 마쳤으면 확인 및 저장을 클릭합니다. 대화 상자가 닫히고 기본 화면이 표시됩니다. 관리를 클릭하고 메시지 필드를 선택합니다. 이제 Webhook이 준비되었습니다.

Webhook 코드 살펴보기

이 코드에는 POST /webhookGET /webhook, 이렇게 2개의 경로가 있습니다. GET 경로는 제공된 토큰을 미리 정의된 확인 토큰과 비교 검사하고 적절한 상태 코드와 질문 토큰으로 응답함으로써 Webhook 확인 요청을 처리합니다.

const verify_token = process.env.VERIFY_TOKEN; // Parse params from the webhook verification request let mode = req.query["hub.mode"]; let token = req.query["hub.verify_token"]; let challenge = req.query["hub.challenge"]; if (mode && token) { if (mode === "subscribe" && token === verify_token) { console.log("WEBHOOK_VERIFIED"); res.status(200).send(challenge); } else { res.sendStatus(403); } }

POST /webhook 경로는 수신 Webhook 알림을 처리합니다. Webhook 요청은 서로 다른 페이로드를 가질 수 있습니다. 아래 코드는 값이 정의되지 않았을 경우에 대비하여 요청 필드에 안전하게 액세스하여 메시지와 비즈니스 전화번호를 읽습니다.

const message = req.body.entry?.[0]?.changes[0]?.value?.messages?.[0]; const business_phone_number_id =

req.body.entry?.[0].changes?.[0].value?.metadata?.phone_number_id;

그런 다음 수신 요청이 'appointment'라는 단어를 포함하는 "text" 유형의 메시지에 대한 요청인지 검사합니다. 메시지에 이 단어가 포함되어 있으면 사용자에게 플로가 전송됩니다. 플로 메시지는 flow_action: "data_exchange"와 함께 전송됩니다. 즉, 플로는 실행되었을 때 엔드포인트에 INIT 요청을 전송하여 초기 화면과 데이터를 가져옵니다.

if ( message.type === "text" && // for demo purposes, send the flow message whenever a user sends a message containing "appointment" message.text.body.toLowerCase().includes("appointment") ) { // send flow message as per the docs here https://developers.facebook.com/docs/whatsapp/flows/gettingstarted/sendingaflow#interactive-message-parameters await axios({ method: "POST", url: `https://graph.facebook.com/v18.0/${business_phone_number_id}/messages`, headers: { Authorization: `Bearer ${GRAPH_API_TOKEN}`, }, data: { messaging_product: "whatsapp", to: message.from, type: "interactive", interactive: { type: "flow", header: { type: "text", text: "Hello there 👋", }, body: { text: "Ready to transform your space? Schedule a personalized consultation with our expert team!", }, footer: { text: "Click the button below to proceed", }, action: { name: "flow", parameters: { flow_id: FLOW_ID, flow_message_version: "3", // replace flow_token with a unique identifier for this flow message to track it in your endpoint & webhook flow_token: "<FLOW_TOKEN_PLACEHOLDER>", flow_cta: "Book an appointment", flow_action: "data_exchange", }, }, }, }, }); } ...

수신 메시지 유형이 "text"가 아닌 경우, 코드는 메시지 유형이 "interactive"인지 검사합니다. 인터랙티브 유형 "nfm_reply"는 수신 메시지가 플로 응답임을 나타냅니다. 그런 다음 사용자에게 'You’ve successfully booked an appointment' 메시지를 전송합니다.

... if ( message.type === "interactive" && message.interactive?.type === "nfm_reply" ) { // send confirmation message await axios({ method: "POST", url: `https://graph.facebook.com/v18.0/${business_phone_number_id}/messages`, headers: { Authorization: `Bearer ${GRAPH_API_TOKEN}`, }, data: { messaging_product: "whatsapp", to: message.from, text: { body: "You've successfully booked an appointment" }, }, }); } ...

그런 다음 수신 메시지를 읽음으로 표시하여 사용자에게 파란색 체크 표시를 표시합니다.

... // mark incoming message as read await axios({ method: "POST", url: `https://graph.facebook.com/v18.0/${business_phone_number_id}/messages`, headers: { Authorization: `Bearer ${GRAPH_API_TOKEN}`, }, data: { messaging_product: "whatsapp", status: "read", message_id: message.id, }, }); ...
        

사용자 경험

이 예제에서는 사용자가 비즈니스 전화번호로 'appointment'라는 단어가 포함된 메시지를 전송하고 플로 메시지를 수신합니다. 비즈니스는 다른 상호 작용이 이루어진 후에 또는 메시지 템플릿으로서 플로를 전송할 수도 있습니다.

사용자는 예약 진행을 위한 CTA 버튼이 포함된 플로 메시지를 수신하며, 여기에서 상세 정보를 입력할 수 있습니다. 그런 다음 플로가 완료되면 확인 메시지를 수신합니다.

사용자에게 플로 전송하기 그래픽

이 가이드에서는 매끄러운 예약 진행을 위한 WhatsApp 플로를 설정하는 방법을 알아보았습니다. 플로 빌드 도구 UI를 사용하여 사용자에게서 예약 상세 정보를 수집하는 양식을 만들었습니다.

플로를 사용하면 예약 진행을 위해 사용자를 외부 웹사이트로 리디렉션할 필요가 없기 때문에 사용자 경험이 향상됩니다. 사용자는 간단한 프로세스를 통해 WhatsApp에서 곧바로 예약을 완료할 수 있습니다. WhatsApp Flows는 예약 진행 외에도 고객 서비스 피드백을 수집하거나 사용자가 프로모션 또는 메일링 리스트에 가입하도록 지원하는 용도로도 사용할 수 있습니다. WhatsApp Flows는 외부 API나 엔드포인트에 있는 다른 앱에 유연하게 연결됩니다.

플로 빌드 도구 UI를 사용하면 WhatsApp Flows를 손쉽게 만들 수 있습니다. 또한 플로 API를 사용하여 프로그래밍 방식으로 플로를 만들 수도 있습니다. 자세한 내용은 WhatsApp Flows 문서를 참조하세요.