
회사에서 Zoom으로 학생들 영어 강의를 하는데, 동 시간대에 진행되는 수업들을 한 눈에 보고 관리할 수 있는 관제 페이지를 만들어달라는 요청이 들어오게 됐어요. 실시간으로 진행되고 있는 강의에 대한 정보들을 서버에서 받아온 뒤, 프론트에서 Zoom SDK를 사용하여 여러 개의 강의를 동시에 띄워주면 되는 메커니즘을 가진 프로젝트였어요.
맨땅에 헤딩을 하지 않기 위해 공식 문서에서 잘 만들어져 있는 예시 프로젝트를 찾는 것이 우선이었으며, 큰 어려움 없이 좋은 소스를 찾을 수 있었어요.
프론트엔드 코드에서는 서버에서 받아온 미팅 정보들을 사용하여 Zoom SDK를 실행시키는 로직이 주로 담겨있었어요. 프로젝트에서 Zoom SDK를 실행시키는 방법 두 가지를 제공하는데, 하나는 최상단 HTML의 리액트 루트와 같은 위계에 <div>를 생성한 후 전체 화면에서 렌더링하는 Client View이며, 다른 하나는 프로젝트 내 원하는 위치를 지정하여 렌더링을 할 수 있는 Component View였어요. 우리 프로젝트에서는 기능이 추가되는 것을 고려하고, 자유로운 배치를 위하여 Component View 방식으로 렌더링하는 것을 채택하게 됐어요.
또한, 백엔드 코드에서는 SDK 실행에 필요한 값들을 암호화해서 내려주는 로직 정도만 포함되어 있어 상대적으로 가벼웠어요.
리액트 프로젝트 내 Zoom 미팅을 임베드 시키기 위해서는 몇 가지 선행작업이 필요했어요. 우선, 여느 SDK 사용법과 마찬가지로 인가 키 값을 발급받아야 했어요.
발급 방법은 아래와 같아요.
Zoom App Marketplace 링크에 접속한 후,
오른쪽 상단의 Build App을 통해 App을 하나 생성해요.
앱을 성공적으로 생성하고 나서 Manage 창으로 들어가서 생성한 앱에 대한 상세 페이지로 들어가면 아래와 같이 Client ID와 Client Secret 키가 발급되어진 것을 볼 수 있어요. Client ID는 프론트엔드와 백엔드 두 곳 모두 사용되며, Client Secret은 백엔드에서 암호화할 때 사용되는 키 값이에요.
추가로, Features > Embed 탭에서 Meeting SDK의 Embed 여부 정도만 활성화시켜주면 세팅은 끝이 나요.
설정까지 완료했으니 들뜬 마음으로 앱을 실행시켜 실제로 미팅 임베드가 잘 되는지 테스트 해 볼 차례예요.
강의 관제 시스템의 성격에 맞도록 미팅을 개설한 계정이 아닌 관제만 진행할 계정 하나를 두고, 해당 계정으로 여러 미팅에 참여하도록 아키텍처를 구상했어요. 즉 SDK를 발급한 계정을 관제 계정으로 설정하고, SDK를 발급받지 않은 개인 계정으로 개설한 강의에 참여하도록 테스트를 진행하였어요.
하지만, 역시나 한 번에 성공하지 못하고 에러와 마주하게 됐어요.
Error 6601 refers to a specific scenario.
서치하여 원인을 분석해보니, 아래와 같은 분석을 찾을 수 있었어요.
If you did not publish your SDK app, and are trying to join a meeting which is created by a user on an external account, you will get this error.
If you try to join a meeting created by a user, who is using the same account as the SDK, you should not get this error.
즉, SDK Key가 매핑되지 않은 계정의 미팅에 참여하려고 할 때 발생하는 에러였어요. 미팅에 참여하기 위해서는 미팅을 호스팅하는 계정에서 SDK Key를 발급받아야 하며, SDK Key를 발급받은 계정과 해당 계정이 호스팅하는 미팅은 서로 페어되어 사용되어야 하는 구조죠.
우리 프로젝트로 예시를 들면, SDK 키를 발급받은 관제만 하기로 했던 계정에서 개설한 미팅에만 참여가 가능한, 즉 내가 개설한 강의에 내 아이디로만 참여가 가능한 상황이며, 가정대로 프로젝트를 실행했더니 정상적으로 렌더링되는 것을 확인하였어요.
이제 가장 중요한 여러 개의 미팅에 동시에 참여하는 테스트를 진행할 차례예요. 이것이 성공하면 프로젝트는 성공할 확률이 높아지며, 이것이 실패하면 반대로 실패하게 돼요.
두 개의 미팅에 참여하는 것이 성공하면 n 개의 참여도 성공할 것이라는 가정을 세우고, 두 개의 계정에서 각각 SDK 키를 발급하여 필요한 정보들을 서버에서 내려주도록 설정하였어요. 이어서 SDK 키를 발급한 두 계정에서 각각 하나의 미팅을 개설하여 참여한 후, 리액트 프로젝트에서 두 개에 동시에 참여하도록 ref를 연동한 <div> 두 개에 Component View 렌더링을 시도했어요.
결과는 처참했죠. 또 실패를 마주하게 됐어요.
{ "type":"INVALID_OPERATION","reason":"Duplicated join operation" }
하나의 프로젝트 내에 두 개 이상의 Zoom SDK 인스턴스를 실행시킬 수 없다는 내용의 이슈였어요. 프로젝트를 계속 이어가야 될 지 말아야 될 지 결정해야 하는 순간이 왔지만, 포기하려던 찰나에 합법적인 꼼수 하나가 머리에 스쳐 지나갔어요.
하나의 프로젝트에 두 개 이상의 인스턴스 실행이 불가능하지만, 여러 개의 프로젝트에 인스턴스 하나씩 실행은 가능하다는 가정을 둘 수 있었어요. 이에 따라, same-origin의 <iframe>을 여러 개 두고, 각 <iframe>마다 인스턴스 하나씩 실행시키는 방법을 시도해봤으며, 놀랍게도 가정대로 두 개의 미팅을 동시에 참여하는데 성공했죠.
위 코드에서는 meetingId만 전달하는 URL을 통해 전달하는 방식으로 되어있지만, 미팅에 필요한 정보들을 query parameter로 모두 전달한 후 파싱하여 사용하는 방법도 생각할 수 있어요.
Zoom SDK의 Component View는 아래와 같이 세 가지 타입을 지원해줘요.
아무 설정도 하지 않은 상황에서는 Default로 Speaker View로 렌더링이 돼요. 우리가 만들고 있는 강의 관제 시스템의 특성 상 참가자 모두의 화상 화면을 보는 것이 적합하기 때문에 Gallery View 혹은 Ribbon View를 채택하는 편이 적합하였어요.
init() 함수의 option에 defalutViewType에서 이를 선택할 수 있었기에 Gallery View를 설정한 후, 프로젝트를 실행하였어요.
하지만, 예상과 달리 계속 Speaker View로 렌더링되며, Gallery View로 렌더링이 되지 않는 이슈가 발생하게 됐어요.
스터디를 한 결과, Gallery View와 Ribbon View는 SharedArrayBuffer를 클라이언트에서 사용할 수 있는 상태가 되어야 적용되는 것이었어요. 공식 문서에서 친절히 실행하고 있는 클라이언트에서 SharedArrayBuffer를 사용할 수 있는 상태를 만드는 것에 대한 가이드를 제공해주었어요.
클라이언트 사이드 렌더링을 하고 있는 CRA 프로젝트를 다루고 있는 우리의 상황에 맞는 솔루션은 세 번째인 serviceworker를 실행시켜 SharedArrayBuffer를 사용할 수 있는 상태로 만드는 것이었어요. 가이드에서 제공하고 있는 깃허브 코드를 우리 프로젝트 최상단 HTML에서 다운로드하여 활성화를 시켜주었어요.
그 결과 정상적으로 SharedArrayBuffer를 사용할 수 있는 상태가 되었으며, Gallery View로 렌더링이 되는 모습을 볼 수 있었어요.
하지만, 정상적으로 동작하는 줄 알았던 프로젝트가 배포 환경에서는 여전히 Speaker View로 렌더링되는 현상이 발견됐어요. 알고보니, 로컬에서 잠시 테스트를 위해 활성화 시켰던 구글의 SharedArrayBuffer 토큰이 html <meta> 태그를 지웠음에도 localhost:3000에서 활성화 되고 있었기 때문에 로컬 환경에서만 Gallery View가 보였던 것이었죠.
서버에서 COEP와 관련된 정보를 직접 헤더에 담아서 내려주는 것이 정통한 방법이기 때문에 프론트에서 관련된 로직을 거두고, 서버에서 내려주는 방법을 채택했더니 배포 환경에서도 제대로 동작하는 것을 확인하였어요. 추가로, 로컬 환경에서는 배포 서버에서 헤더가 내려오지 않기 때문에 따로 설정을 해줘야 했어요.
여러 개의 미팅 정보를 서버에서 내려받은 후, 미팅을 동시에 띄우기까지는 문제없이 성공했으나, 일부 참여가 불가능한 미팅이 존재했어요. 에러 메시지가 명확하지 않아서 원인을 파악하기까지 시간이 좀 걸렸으나, 패스워드에 잘못되었다는 에러 메시지를 받게 됐어요.
실패한 미팅들에 대한 데이터들을 파악한 결과, 패스워드에 +, %가 포함될 경우 미팅이 실패한다는 가정을 세울 수 있었어요. 평소에는 랜덤으로 줌에서 만들어 준 패스워드를 사용했으나, 임의로 미팅을 생성하고 패스워드에 +, %를 포함하여 개설한 결과 가정대로 미팅 참여에 실패한 것을 확인할 수 있었어요.
대체로, URL에 포함되면 띄어쓰기나 특수문자 앞에 사용되는 문구로 사용되는 두 문자이기에 프론트엔드 엣지에서 encodeURIComponent, decodeURIComponent를 사용하여 처리를 해봤으나, 서버 Response를 받은 이후, Zoom SDK Join을 하는데까지 잘못 전송되는 케이스는 없으므로, 해당 방법으로 문제 해결이 난항을 겪었죠.
결국, Zoom 세팅에 있는 ‘비밀번호에 특수문자 한 개 이상 포함’ 항목의 체크를 해제하면서 문제를 해결하는 방법을 택하게 되었고, 모든 미팅들이 정상적으로 렌더링되는 결과를 얻게 됐죠.
전체 화면에서 보여야 하는 미팅 화면을 작은 화면으로 압축하여 렌더링 하기 때문에 화면 비율이 깨지는 현상이 발생하였고, 이를 위한 개선작업이 필요했죠.
Meeting SDK에 참여 시 기준이 되는 요소의 width, height를 사이즈로 지정하고, 화면의 크기가 변경되면 변경된 크기에 맞춰 Meeting SDK의 크기도 변경되도록 설정해줬어요.
Meeting SDK 크기 대응
그 외, 연결 상태 변화에 따른 화면 분기, 입장 시 음소거, 필요없는 툴바 버튼 삭제 등의 프로젝트에서 필요한 지엽적인 코드 구현을 하면서 프론트엔드에서 준비할 요소들을 마무리할 수 있었어요.