우리 서비스의 라이브 스트리밍 기능은 라이브 화면 위에서 결제가 모달 방식으로
결제 수단 선택 -> 결제 검증 -> 결제 완료 과정이 거쳐져야한다.
즉, 사용자의 UX를 고려했을 때 Player 위에서 결제가 하나의 흐름으로 진행되어야한다.
기존 결제 방식은 토스페이먼츠 위젯을 사용하고 있었다.(Player가 아닌 일반 구매 페이지)
최대한 기존 코드를 활용하고 pg사를 바꾸는 게 부담이 됐다. 그리고 ui/ux적으로도 토스 페이먼츠가 좋아보였고 또한 sdk에 대한 설명이나 상담이 가능한 discord 채널도 존재해 개발에 어려움이 있더라도 참고 할 수있는 레퍼런스나 수단이 다양하다고 느꼈다.
Player가 중단되면 안된다
일단 토스 페이먼츠 위젯은 결제 요청 후 결과 값을 Redirect Or Promise 방식으로 받을 수 있다.

기존 우리 서비스는 Redirect 방식으로 결제 기능이 구현되어 있었다.
결제가 성공하고 Redirect URL로 리다이렉트가 되면 amount/orderId/paymentKey를 쿼리 파라미터로 받아온다.
이를 가지고 토스 confirm api를 호출하는 구조이다.
그런데 Player를 렌더링하는 url에서 벗어나 confirm api를 호출하게 되면
당연히 Player가 끊기게된다. 물론 successUrl을 Player를 보여주는 페이지로 설정하더라도 결국 새로고침이 되는 것이기 때문에 Player를 다시 연결하며 Player가 중단되게 된다.
대신 Promise 방식은??
물론 이 방법을 사용하면 PC에서는 구현 가능하다
하지만 모바일은 Promise 방식을 지원하지 않는다.
브랜드 페이만 사용
사실 이건 개발로 해결한다기보다 사용자의 결제 선택의 폭을 줄이는 방법이다.
브랜드페이는 모바일도 Promise 방식이 가능하기에 Player가 중단되지 않을 수 있다. 하지만 이건 초기 기획 의도와 어긋나고 좋은 해결방법이 아닌 거 같아 제외하였다.
새 창에서 결제 진행
이 부분은 토스 커뮤니티에 문의 드리고 답변 받은 방법이다.
(카카오에서도 이렇게 한다고 말씀하셨다)
개발 팀에서도 그 전에 이렇게도 한 번 구현해보자 했어서
우선적으로 이 방법으로 개발을 진행하게 됐다.
우려했던 부분은 모바일에서는 새 창이 아닌 탭 형식으로 열리기 때문에 다시 Player로 돌아왔을 때 Player 연결을 유지해줄지가 관건이었다.
플로우는 이렇다.
- Player에서 주소/상품 등 결제에 필요한 정보를 확인/수정
- 결제 클릭 시 window.open을 통해 새 창(새 탭)에서 토스 페이먼츠 위젯 연결
- 결제 요청 후 기존 processing을 담당하는 url로 이동
- 해당 url에서 toss api 호출 후 성공 시 window.close
- 결제 완료 정보는 Player에서 모달 방식으로 제공
새 창을 열어 진행하는 부분은 문제가 한 가지 있었다.
결제에 필요한 정보를 어떻게 전달할 것인가?
기존 결제 코드에서는 결제에 필요한 모든 정보를 전역상태값으로 가지고 있었다.
그런데 새 창으로 열렸을 때는 이 값을 사용할 수 가 없다.
전역상태를 사용하지 못하니 다양한 방법을 고려했다.
먼저 window.postMessage를 사용하여 구현했다.
이는 부모 창과 자식 창간의 데이터를 교환할 수 있는 window 메서드인데 이를 사용하면 간단하게 구현이 가능했다.
하지만 문제는 부모 창에서 자식 창으로 postMessage를 할 때 모바일 환경에서 null로 그 값이 들어가는 오류가 있었다.
이유는 대표적으로 다음과 같다.
window.open으로 연 팝업에서 postMessage 통신 시 흔하게 발생
iOS, 특히 사파리 or WebView 제한
window 객체가 완전히 초기화 되기 전 postMessage 호출
cors 정책 등
객체가 완전히 초기화 되지 않아 발생하는 오류라면 해결이 가능하겠지만
브라우저의 문제라면 해결이 어려워보였다. 또한 애초에 모바일에서 막는 방법을 거슬러 가능하게 하는 것도 역설같았다.
두 번째로 생각한 방법은 localstorage를 이용하는 것이다.
처음엔 sessionStorage가 좀 더 낫지 않을까 (휘발성) 했는데
팝업은 별도의 세션을 갖기에 공유가 불가능했다.
단, 이 방법의 단점은 당연히 보안이다. 자식 팝업에서 부모 창으로 데이터를 보낼 때는 중요 정보가 없어서 문제가 없었다.
부모 창에서 자식 팝업으로 데이터를 보낼 때는 민감정보가 포함되어있어
이게 걸렸다.
세 번째로 생각한 방법은 백엔드 API를 한 번 거치는 것이다.
이 방법이 가장 좋아보였다.
사실 나의 생각으론 백엔드 API를 거쳐 개발하는 게 가장 베스트 같아 보였지만
일단은 mvp로 빠르게 보고가 들어가야하는 프로젝트이기 때문에 우선적인 방법으로 개발을 진행하였다.
localStorage에 바로 중요 정보를 저장하기보단 한번 암호화를 거쳐 저장하자고 팀장님께서 말씀해주셨고 localStorage와 crypto.js를 사용하여 데이터 송신을 개발했다.
window.eventListener로 Storage에 대한 이벤트를 감지 할 수 있어서 이를 활용했다.
일단 1차 mvp가 끝나고 리펙토링 단계에 들어서면 부모창에서 자식창으로 데이터를 보낼 때 API를 거치고 자식창에서 부모창으로 데이터를 보낼 때 localStorage를 거치는 방법이 좋아보인다. 다양한 방법이 있겠지만 이렇게 했을 때 무리가 없을 거 같다.
그럼에도 가끔 결제가 길어진다거나 했을 때 Player가 간헐적으로 모바일 환경에서 중단될 때가 있었다. 이는 Player가 멈추는 현상이다.
이 부분은 ivs 라이브러리 내에 player 객체의 getStatus, load, play 메서드를 사용해 해결했다.
Player가 애초에 구매 과정이 아니더라도 상태가 Playing이 아니라면 reload 되어야한다고 생각했다. player 객체의 상태는 다음과 같이 다양하다.
IDLE: 플레이어가 초기화되었지만 아직 소스가 로드되지 않은 상태
BUFFERING: 플레이어가 재생할 데이터를 버퍼링하고 있는 중
READY: 플레이어가 재생할 준비가 완료된 상태 (버퍼가 충분히 쌓임)
PLAYING: 현재 스트림이 재생되고 있는 중
ENDED: 스트림 재생이 끝난 상태 (VOD나 종료된 라이브)
ERROR: 오류가 발생한 상태 (예: 네트워크 문제, 소스 불일치 등)
위 상태 정보를 2초마다 체크하고 만약 Playing 상태가 아니면 reload 하도록 개발하였다. 단, Buffering 상태는 제외하였다. 오히려 Buffering 상태까지 reload 해보니 reload 횟수가 많아져 오히려 Buffering 상태는 두는게 더 좋은 ux를 제공한다 생각했다.
결과적으로 player가 중단되지 않게 결제 완료까지 가능하도록 개발할 수 있었다 🚀