Channel Talk
안녕하세요. 채널톡 프론트엔드 엔지니어 에이든입니다.
채널톡의 핵심 철학은 Customer-Driven, 고객 중심 입니다. 이는 개발자가 제품을 구현할 때도 고스란히 적용이 되는데요. 이번 글에서는 웹 기반의 메신저 SDK를 만들 때 유저 경험을 위해 고민했던 부분들을 몇 가지 사례와 함께 살펴보려 합니다.
채널 프론트(Channel Front)는 기업과 유저를 연결하는 올인원 AI 메신저입니다. 실시간 고객 상담뿐만 아니라 자동화 챗봇, 마케팅, 미트 등 다양한 기능을 제공하고 있는데요. 현재 보고 계신 채널톡 홈페이지의 우측 하단에서도 만나볼 수 있지요. 채널톡이 제공하는 JavaScript SDK를 사용하면 기업(이하 “고객사”)이 운영하는 웹사이트에 채널 프론트를 손쉽게 설치할 수 있습니다.
일반적인 웹서비스를 개발할 때와 비교했을 때, SDK의 경우 문제를 해결하기 위해 고민하는 지점이 약간 다르다고 느꼈습니다. 두 사용자 집단, 즉 ‘메신저를 사용하는 유저의 입장’과 ‘SDK를 사용하는 고객사의 입장’을 함께 고려하여 경험을 설계해야 하기 때문입니다.
메신저를 사용하는 유저의 입장에서 어떤 것을 고려해야 할까요?
문의하는 데 어려움이 없도록 직관적인 사용성을 제공하는 것이 중요하다고 생각합니다. 더불어 현대의 웹 구동 환경은 다양한 디바이스와 브라우저의 조합으로 구성되어 있으므로, 최대한 많은 유저에게 버그 없이 유려한 경험을 제공하는 것도 필요합니다.
한편 SDK를 사용하는 고객사의 입장에서는 어떤 것을 고려해야 할까요?
고객사의 웹사이트가 SDK로부터 최소한의 영향 만을 받도록 해야 합니다. 웹사이트의 스타일이 변경되어서는 안 되고, 로딩 속도가 크게 느려져서도 안 됩니다. 또한 메신저가 고객 상담의 창구 역할을 하므로, 결과적으로는 메신저 자체의 사용성과 접근성이 모두 고객사의 SDK 사용 경험에 영향을 준다고 할 수 있겠습니다.
관련 사례를 본격적으로 살펴보겠습니다.
채널 SDK는 정말 다양한 웹사이트 환경에서 실행됩니다. Shopify나 Cafe24와 같은 웹 빌더사 뿐만 아니라 고객사에서 자체적으로 개발한 수많은 웹사이트와 웹뷰(WebView) 위에서 SDK는 일관된 UI를 제공해야 합니다.
워낙 환경이 다양하다 보니 고객사 웹사이트에 설정된 스타일로부터 영향을 받는 일이 종종 있었습니다. 이러한 문제를 방지하기 위해 채널 SDK는 shadow DOM을 활용하여 UI 요소를 렌더링하고 있습니다.
shadow DOM은 캡슐화되어 분리된 DOM을 HTML에 부착하는 방법을 제공하는 Web API입니다. 외부 DOM의 스타일 규칙과 완전히 독립된 자체 스타일시트를 가지고 있기 때문에, 고객사 웹사이트로부터 스타일이 오염되는 것을 방지할 수 있습니다.
출처: MDN Web Docs - Using shadow DOM
고객사 웹사이트에 아래와 같은 CSS 스타일이 명시되어 있다 해도 shadow DOM 내부의 엘리먼트는 영향을 받지 않는 것이지요.
또한 shadow DOM 내부의 엘리먼트는 일반적으로 querySelector
로부터 검색되지 않기 때문에, 고객사 웹사이트가 동적으로 전역 엘리먼트를 조작하는 로직으로 인해 의도치 않게 사이드 이펙트가 발생하는 것을 방지할 수 있습니다.
고객사 웹사이트로부터 영향을 받아서도 안 되지만, 반대로 영향을 줘서도 안 됩니다. 메신저를 띄우기 위해서는 웹사이트의 DOM을 조작하는 과정이 필요한데, 이때 사이드 이펙트가 발생하지 않도록 신중히 처리해야 합니다. 웹사이트가 사용 중인 외부 라이브러리와 충돌을 일으킬 가능성도 항상 고려해야 합니다.
채널 SDK는 모바일 환경에서도 일관된 스케일로 메신저를 보여주기 위해 Viewport 메타 태그를 사용하고 있습니다.
메신저가 열리는 순간에 viewport를 조정하기 위해서는 고객사 웹사이트에 메타 태그를 삽입하는 것이 불가피한데요. 섣불리 조작했다가는 예상치 못한 사이드 이펙트가 발생할 가능성이 있습니다.
실제로 특정 고객사에서 React의 stylesheet 관련 라이브러리를 사용하고 있었는데, 메타 태그 간의 순서가 바뀌자 스타일이 깨지는 사례가 있었습니다. (이러한 인과 관계를 예측해서 대응하기란 쉽지 않을 듯합니다.) 또 다른 사례로 viewport 메타 태그가 아예 없는 웹사이트의 경우, 새로운 메타 태그를 삽입하게 되면 이를 다시 제거해 주는 것만으로는 원래의 viewport 상태로 돌아가지 않습니다. 이렇듯 고객사 웹사이트의 메타 태그를 문제없이 다루는 일은 생각보다 고려할 부분이 많은 작업입니다.
타사의 메신저 SDK는 viewport를 어떻게 다루고 있을까요?
D사, F사, G사, T사는 웹사이트의 viewport 메타 태그를 건드리지 않는 대신, 모바일에 최적화된 UI를 보여주지 않고 있습니다. 따라서 고객사 환경에 따라 메신저의 UI가 의도한 것에 비해 훨씬 작아 보일 수 있습니다.
출처: Apple Documentation Archive - Configuring the Viewport
I사, R사는 모바일 환경에서 최적화된 UI를 보여주기 위해 웹사이트의 viewport 메타 태그를 조작합니다. 다만 몇몇 사이드 이펙트가 존재하는데요.
I사는 메신저가 열릴 때 웹사이트의 기존 메타 태그를 스택에 저장한 후 제거합니다. 그리고 viewport 메타 태그를 새로 추가합니다.
이후 메신저가 닫힐 때, 스택에 담아놨던 웹사이트의 기존 viewport 메타 태그를 다시 추가합니다. 다만 이전에 새로 추가했던 메타 태그는 따로 제거해 주지 않습니다.
메신저가 여러 번 열렸다 닫히길 반복하면, 제거하지 않은 메타 태그가 축적되어 쌓이게 되면서 웹사이트의 기존 viewport 상태로 원복되지 않는 현상이 발생하기도 합니다. (동시에 여러 viewport 메타 태그가 추가되는 경우, 가장 마지막에 위치한 메타 태그의 효과가 적용되기 때문입니다.)
한편, R사는 메신저를 열 때 웹사이트에 viewport 메타 태그를 새로 추가하고, 닫을 때 해당 메타 태그를 따로 제거해 주지 않습니다.
추가한 viewport 메타 태그의 효과는 그대로 남아있게 되며, 웹사이트의 viewport는 content=“width=device-width”
인 상태로 변하게 되는 현상이 발생합니다.
--
채널 SDK는 아래 조건을 지켜 viewport를 다루고 있습니다.
메신저를 닫았을 때 웹사이트는 기존과 동일한 메타 태그를 가지고 있어야 합니다.
메신저를 닫았을 때 웹사이트는 기존과 동일한 viewport 상태로 보여야 합니다.
채널 SDK는 ViewportService
라는 Class로 내부 상태와 메서드를 관리합니다. 메신저가 열리는 순간 viewport 메타 태그를 조작하며, 웹사이트에 viewport 메타 태그가 이미 존재하는지 여부에 따라 다르게 처리해 주고 있습니다.
viewport 메타 태그가 존재하는 경우, content
를 저장한 후 메신저를 위한 content
로 대체합니다. 최대한 고객사 웹사이트의 메타 태그에 변화가 없도록 하기 위해 메타 태그를 추가/제거하지 않고 content
만 갈아 끼우고 있습니다. (content
만 변경해도 새로운 viewport 상태가 적용됩니다.)
다만 웹사이트에 viewport 메타 태그가 아예 존재하지 않는 경우에는 메타 태그를 새로 추가합니다.
이후 메신저가 닫히면, 메타 태그를 원래 상태로 돌려놓습니다. 메타 태그를 새로 추가했는지, content
값만 변경했는지에 따라 원복 과정을 다르게 처리합니다.
메타 태그를 새로 추가했었던 경우, 메타 태그를 제거하고 default viewport 메타 태그를 추가합니다. 기존에 메타 태그가 없었기 때문에, 모바일에서의 default width인 980px(#)을 viewport로 설정해 주는 것입니다.
반면 content
값만 변경한 경우는 원래의 content
로 갈아 끼워 원래의 viewport 상태로 돌려놓습니다. 단, 유효한 content
값이 아니었던 경우는 viewport 메타 태그가 없었던 상태와 동일하므로 default viewport를 적용합니다.
이렇듯 채널 SDK는 고객사 웹사이트의 DOM 구조가 변경되는 것을 지양하는 방식으로 viewport 상태를 유지함으로써 예상치 못한 사이드 이펙트를 최소화할 수 있습니다.
최근 저는 SDK를 사용하며 마주칠 수 있는 다양한 에러 상황에서의 경험 개선 작업에 참여했습니다. 유저나 고객사 입장에서 모호하게 느껴질 수 있는 부분을 없애고, 에러 처리에 대한 팀 내부적인 기준을 확립했는데요.
에러 처리는 얼핏 사소하게 느껴질 수 있지만, 유저에게 좋지 않은 경험을 줄 수 있어 중요하게 고려해야 합니다. 현재 어떤 상황에 처해있는지를 명확하게 전달하고, 해결하기 위한 방법을 제대로 안내하지 않으면 유저는 부정적인 감정을 갖고 서비스를 이탈할 수 있습니다.
예시: 끔찍한 기분을 안겨주는 에러 메시지
채널 프론트팀은 일반적으로 좋은 에러 처리의 요건이라 알려진 규칙들을 따르되, 고객사와 유저의 에러 도달 여정과 경험에 조금 더 초점을 맞춰 에러의 처리 방향을 결정하고자 했습니다. 두 가지 사례를 통해 그 과정을 소개해 보고자 합니다.
채널 SDK는 웹사이트에 방문한 유저를 자체적으로 분류하여 관리하고 있습니다. 고객사 웹사이트에서 로그인이나 회원가입을 하면 멤버 유저로, 이외에는 익명 유저 등으로 분류되고 있는데요.
만약 계정이 삭제된 유저가 동일한 세션을 가지고 메신저에 접속한다면 어떤 화면을 보여줘야 할까요?
기존에는 고객사에 의해 차단된 유저와 동일한 조치를 취하고 있었습니다. 서버 입장에서 보면 인증 토큰에 해당하는 유저를 찾을 수 없는 것이므로, “만료되었거나 잘못된 접근”으로 판단하여 접근을 제한했던 것이죠.
저는 CX팀의 도움을 얻어 ‘고객사가 유저를 언제, 왜 삭제하는지’에 대해 조사해 보았습니다. 그 결과 다음과 같이 삭제 경로가 추려졌습니다.
고객사 웹사이트에서 유저가 회원을 탈퇴했을 때
고객사에서 유저가 회원 탈퇴 시 SDK 상의 유저 정보를 함께 삭제하도록 구현해 놓은 경우입니다.
카페24를 통해 웹사이트를 빌드한 경우 회원 탈퇴 시 자동으로 SDK 유저가 삭제됩니다.
고객사에서 판단했을 때 같은 사람으로 보이는 유저가 여러 명 있을 때
초반에 SDK를 테스트하기 위해 생성한 유저를 정리할 때
고객사가 유저를 삭제하는 이유를 살펴보니, 유저를 차단할 때와는 확연히 다른 의도를 가지고 있음을 알 수 있었습니다. 유저의 접근을 막는 것이 아니라, 오히려 원활히 서비스를 이용할 수 있도록 안내해 줄 필요가 있었던 것이죠. 따라서 아래와 같은 방향으로 에러를 처리하기로 했습니다.
에러 페이지를 없애고 유저의 접근을 허용합니다.
“삭제된 유저”, “잘못된 접근”과 같은 안내 문구를 사용하지 않습니다.
Navigating 버튼을 통해 새로 문의할 수 있는 통로를 마련합니다.
채널 SDK는 객관식 챗봇인 '서포트봇'을 열 수 있는 기능이 있습니다.
만약 운영이 중지된 서포트봇의 id를 인자로 넘기면 일반 유저챗을 대신 보여주도록 처리하고 있었습니다. 유저가 굳이 에러 페이지를 맞닥뜨리는 상황을 만들지 않는 것이 더 좋은 경험이 될 것으로 판단했던 것이었죠.
서포트봇과 일반 유저챗
그런데 고객사는 어떤 경로를 통해 SDK에 중지된 서포트봇의 id를 인자로 넘기게 되었을까요? 인터뷰와 피드백을 통해 의견을 수렴해 본 결과, 대체로 실수에 의한 경우가 많은 것으로 나타났습니다.
“실수로 SDK에 잘못된 서포트봇 id를 넣게 되었어요.”
“이전에 잘 운영하고 있던 서포트봇을 어느 순간 중지하게 되었는데, 해당 서포트봇을 여는 SDK 코드는 제거하지 않아서 생긴 문제였어요.“
잘 운영되는 줄 알았던 서포트봇이 (실제로는 에러 상황임에도) 일반 유저챗을 보여주고 있었기 때문에, 고객사 입장에서는 오히려 혼란스럽게 느껴지는 경우도 있었습니다.
“서포트봇을 설정한 페이지를 새로 오픈했는데, 일반 유저챗으로만 문의가 들어오는 거에요. 이상하다고 생각하면서도 뭐가 문제인 줄 몰랐어요. ‘마케팅 효과가 없었나 보다’라고만 생각했죠.”
“직접 테스트를 해볼 겸 들어가 봐도 에러라고 명확하게 알려주지 않았기 때문에, 무언가 잘못되었다고 느끼기가 쉽지 않았어요.”
나름대로 유저의 경험을 고려한 에러 처리였지만, 그것이 때로는 고객사의 상황 파악과 빠른 대응을 가로막을 수도 있던 것입니다. 상담 리소스를 줄이기 위해서, 마케팅 성과를 측정하기 위해서, 고객의 피드백을 수집하기 위해서 설정한 서포트봇이 잘 운영되고 있는지 명확하게 안내해 주는 것은 유저의 경험만큼이나 중요한 부분일 것입니다. 결국 아래와 같이 에러 처리 방향을 변경했습니다.
에러 페이지를 보여주고, ‘잘못된 접근’ 혹은 ‘관련 설정이 잘못되었음’을 안내합니다.
유저에게는 Navigating 버튼을 통해 새로 문의할 수 있는 통로를 마련합니다.
이와 같은 사례들을 통해 같은 상황에 대해서도 유저와 고객사의 경험이 확연히 다를 수 있다는 것, 양쪽의 입장을 함께 고려해야 한다는 점을 배우게 된 계기가 되었습니다.
채널톡은 현재 약 16만 개의 브랜드에서 사용 중이며, 매일 7000만 개 이상의 메시지를 처리하고 있습니다. 정말 다양한 환경에서 채널 SDK가 실행되고 있는 만큼, 채널 프론트팀은 많은 기기와 브라우저에서 유저에게 안정적인 경험을 제공하는 것을 중요한 과제로 생각하고 있습니다.
프론트엔드 개발자라면 공감하시겠지만, 크로스 브라우징은 참 까다로운 친구입니다. 명세대로 동작하지 않는 경우도 허다하고, 테스트해 봐야 할 환경도 너무 많죠.
이번에는 최근 릴리즈된 '미트' 기능을 개발하면서 겪은 크로스 브라우징 이슈들, 그중에서도 기기/권한과 관련된 몇 가지를 소개해 보려 합니다.
미트는 고객사와 유저가 음성 및 화상으로 소통하거나 화면을 공유할 수 있는 서비스입니다.
미디어(마이크, 카메라)에 대한 권한을 조회하는 가장 일반적인 방법은 getUserMedia
메서드를 사용하는 것입니다. getUserMedia
를 실행하면 브라우저는 유저에게 미디어 권한 허용을 요청하는 프롬프트를 띄우게 되며, 유저가 요청을 거부하거나 이전에 이미 거부한 적이 있다면 DOMException
이 발생합니다.
한편 미트는 권한 요청에 대해 아래와 같은 기획을 두고 있었습니다.
음성으로 소통할 수 있는 음성 미트인 경우 마이크에 대한 권한을 요청합니다.
화상으로 소통할 수 있는 영상 미트인 경우 마이크, 카메라에 대한 권한을 요청합니다.
위 기획에 따르면 음성 미트인 경우 카메라에 대해 권한을 요청하지 않으며, 그럼에도 카메라에 대한 권한을 조회할 수 있어야 합니다. 권한이 거부된 미디어가 있으면 이를 UI 상으로 나타내줘야 했기 때문입니다. 즉 getUserMedia
를 사용하지 않고 권한을 조회해야 하는 요구사항이 생긴 것입니다.
채널 SDK에서는 enumerateDevices
메서드를 활용해 이를 해결했습니다. enumerateDevices
는 현재 사용 가능한 미디어 입력/출력 장치를 조회하는 메서드인데요.
미디어에 대한 권한이 있을 때
미디어에 대한 권한이 없을 때
미디어에 대한 권한이 있는 경우에만 장치에 대한 정보(deviceId
, label
, groupId
등)가 담겨있는 것을 알 수 있습니다. 이 점을 활용하여 getUserMedia
호출 없이 권한 여부를 판단하고 있습니다.
단, 기기와 브라우저에 따라 예외 케이스가 존재하기 때문에 이를 처리해 줄 필요가 있습니다.
iPhone, iPad에서 동작하는 브라우저 앱이거나 Safari 브라우저인 경우, 미디어(마이크나 카메라) 중 하나의 권한만 허용해도 다른 미디어의 기기 정보를 모두 조회할 수 있습니다.
따라서 권한이 거부된 경우는 getUserMedia
의 실행 결과 발생하는 DOMException
을 보고 판단할 수밖에 없습니다.
다만 이들 브라우저는 이전에 권한을 허용/거부한 적이 있어도 페이지에 입장할 때마다 권한을 다시 요청한다는 특성이 있으므로, getUserMedia
를 실행하기 전까지는 권한이 없는 것으로 판단할 수 있습니다.
Firefox 브라우저의 경우 권한 여부와 별개로 기기의 deviceId
, groupId
를 제공하며 label
정보는 제공하지 않습니다.
하지만 FireFox도 페이지에 입장할 때마다 권한을 다시 요청하므로 getUserMedia
실행 전까지는 권한이 없는 것으로 판단할 수 있습니다.
참고로 Permissions API의 경우 모든 브라우저에서 지원하지 않기 때문에 사용하지 않았습니다. Firefox 브라우저와 웹뷰 안드로이드 등에서 microphone
, camera
permission을 지원하지 않습니다. (Permissions API Browser compatibility)
유저가 브라우저의 프롬프트를 통해 미디어에 대한 권한을 허용하더라도, OS 권한이 거부된 경우 해당 미디어를 사용할 수 없습니다. 일반적으로 OS 권한은 아래와 같이 데스크탑 기기의 설정 앱을 통해 허용/거부할 수 있는데요.
OS 권한이 거부된 경우도 마찬가지로 getUserMedia
를 실행했을 때 DOMException
이 발생합니다. 문제는 안타깝게도 브라우저 권한이 거부되어 있는지, OS 권한이 거부되어 있는지 명확하게 알려주지 않는다는 것입니다.
Chrome과 Edge, Whale 브라우저의 경우 브라우저와 OS의 권한 거부를 오직 에러의 메시지를 통해 구분할 수 있습니다.
브라우저 권한 거부 시: NotAllowedError: Permission denied
OS 권한 거부 시: NotAllowedError: Permission denied by system
Safari 브라우저의 경우 브라우저와 OS의 권한 거부 시 에러가 동일하게 내려오기 때문에 이 둘을 구분할 수 없습니다.
브라우저 권한 거부 시: NotAllowedError:The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission
OS 권한 거부 시: NotAllowedError:The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission
Firefox 브라우저의 경우 OS 권한이 거부되면 아예 다른 종류의 에러가 내려오네요 😅
브라우저 권한 거부 시: NotAllowedError: The request is not allowed by the user agent or the platform in the current context
OS 권한 거부 시: NotFoundError: The object can not be found here
결국 위 케이스를 모두 고려하기 위해, 환경에 따라 브라우저와 OS의 권한 거부를 각기 다른 조건으로 구분하고 있습니다.
(추가로, 브라우저와 OS 권한이 모두 거부되어 있는 경우 브라우저 에러가 내려온다는 점도 참고해 주세요!)
Safari 브라우저에서 getUserMedia
를 실행할 경우, 기존에 재생하고 있던 미디어의 오디오 볼륨이 갑자기 커지거나 작아지는 문제를 발견했습니다. 예를 들어 iPhone의 Youtube Music 앱으로 음악을 듣던 중 Safari 브라우저로 미트에 접속하는 순간 음악 소리가 갑자기 커질 수 있는 것이죠. 배포 전에 발견해서 다행이었지만 유저에게 좋지 않은 경험일 것이 분명했기 때문에 문제를 꼭 해결해야 했는데요.
우선 원인을 파악하기 위해 조사해 보니 WebKit 엔진의 버그 리포트에 관련된 내용이 2개 정도 올라와 있는 것을 발견했습니다.
Bug 243897 - Volume of playing video increases when capturing microphone with getUserMedia()
Bug 236219 - [iOS] Volume of non MediaStreamTrack-based audio reduces when capturing microphone
“Before mic capture, the user has the volume at a comfortable level. Then when they start mic capture, the user gets blasted by loud audio at first, then audio that is just below a comfortable level until mic capture ends. Then the volume reverts back to a comfortable level again.”
직접적인 원인을 당장 고칠 수는 없는 상황이므로, 우회하여 해결할 방안을 찾을 수밖에 없었습니다.
채널 SDK는 미트 접속 시 소리 없는 음성 파일을 재생하여 기존에 재생 중인 미디어를 중지시키는 방식으로 위 문제를 해결하고 있습니다. getUserMedia
버그는 여전히 존재하지만, getUserMedia
가 실행되기 전에 미디어를 중지시켜 버그가 발생하는 상황 자체를 막는 것이 의도입니다.
다만 이 대응에는 한 가지 추가로 고려해야 하는 점이 있습니다. 대다수 브라우저 정책상, 유저의 액션(클릭, 탭 등) 없이는 음성 파일을 재생할 수 없게 되어있습니다. 따라서 유저의 액션을 받을 수 있는 UI가 필요하고, 해당 UI의 event handler를 통해 음성 파일을 재생해야 하는 것이지요.
미트는 첫 입장 시 아래와 같은 UI를 통해 유저의 액션을 받습니다. '탭하여 음소거 해제'라는 문구로 자연스럽게 유저의 액션을 받고, 유저가 화면을 탭할 경우 내부적으로는 소리 없는 음성 파일을 재생함과 동시에 미디어 권한 요청 프로세스를 진행하게 됩니다.
지금까지 채널 프론트팀이 유저의 경험을 위해 고민했던 몇 가지 사례를 함께 살펴보았습니다.
채널 프론트팀은 제품을 만들 때 유저와 고객사의 경험을 높은 우선순위로 고려하여 개발하고 있습니다. 단순히 기획에 따라 구현하는 것에 그치는 것이 아니라, 사용자 경험을 심도 있게 고민하면서 문제를 발견하고 적극적으로 개선안을 제시하며 제품을 발전시켜 나가고 있습니다.
이번 글에서는 언급하지 않았지만, SDK의 로딩 속도(리소스 최적화, 캐싱, 번들 사이즈)와 안정성(E2E 테스트, 디버깅 시스템) 등 도전적이고 다양한 테스크를 경험할 수 있는 팀입니다. 프론트엔드 엔지니어로서 성장할 기회가 많은 채널 프론트팀에 많은 관심과 지원 부탁드립니다!
[이런 글도 추천드려요]
We Make a Future Classic Product
채널팀과 함께 성장하고 싶은 분을 기다립니다