<UserVideo>는 제공받은 streamManager를 바탕으로 비디오를 송출합니다. 또한, useStreamManagerProperties 커스텀 훅으로부터 정보를 얻기 위해 streamManager 객체를 전달합니다.
useStreamManagerProperties는 <UserVideo>로부터 필요한 객체인 streamManager를 받으며, 이 streamManager에 대한 리스너를 등록합니다. 다양한 리스너들을 등록할 수 있지만, 현재 이 컴포넌트에서 제공해야 하는 기능을 생각했을 때 아래의 세 가지 리스너를 등록해야 한다고 판단했습니다. 이 리스너들이 호출되면, 이 커스텀 훅이 지니는 state가 업데이트됩니다.
streamPropertyChanged: 프로퍼티가 변경된 경우 호출되는데, 이 안에는 videoActive라는 프로퍼티가 있기 때문에 참가자가 캠을 켜거나 끈 경우 reason 값을 통해 videoActive가 변경되었음을 알 수 있으며, setVideoActive(true | false)를 통해 최종적으로 캠의 켜기/끄기를 반영할 수 있습니다.
publisherStartSpeaking: 참가자가 발언하기 시작해 음성이 감지되면 호출됩니다. 이 이벤트가 호출되었을 경우 state를 업데이트해 주면 됩니다.
publisherStopSpeaking: 참가자가 발언을 끝내 음성이 더 이상 감지되지 않으면 호출됩니다. 이 이벤트가 호출되었을 경우 역시 적절하게 state를 업데이트해 주면 됩니다.
useStreamManagerProperties 커스텀 훅에서 리스너가 실행되어 state가 업데이트되면, <UserVideo>에도 영향을 주게 됩니다.
<UserVideo>는 변경된 state를 기반으로 자신의 UI를 상황에 맞게 업데이트합니다.
4️⃣ 질문/답변
❓ 왜 모든 streamManager들을 통제할 수 있는 최상단의 <App> (또는 그에 준하는 커스텀 훅)에 리스너를 두지 않는 것인가요?
💡 최상단의 로직이 복잡해지고 관리가 어려워질 것이라고 판단하여 그렇게 하지 않았습니다.
리스너를 최상단에 하나 두는 경우에는, 관리해야 하는 streamManager도 여러 개일 것이고, 그에 따라 자연스럽게 배열 형태의 데이터가 형성되고, 이를 리턴하여 여러 <UserVideo>가 사용할 수 있도록 하는 구조일 것입니다. 문제는 이 방법은 1) 리스너가 클로저 내부에 고립되어 낡은 상태값만을 볼 수 있는데 / 2) 여러 상태를 참고하여 배열을 업데이트해야 하는 구도가 만들어지게 됩니다. 하나의 상태만을 참고한다면 React의 functional update를 사용해 최신의 이전 값을 가져올 수 있지만, 두 개 이상이므로 문제가 됩니다. 결론적으로 리스너에서 이전 데이터의 값만을 볼 수 있어 변경사항이 제대로 반영되지 않습니다.
useEffect를 사용해 리스너의 이벤트 하나가 실행될 때마다 streamManager 상태 배열 값 등을 최신화시킬 수 있도록 그때그때 리스너를 제거했다가 다시 생성하는 방법도 있습니다. 이 방법은 최신화된 값을 얻을 수 있지만, 로직이 복잡하며 리스너가 삭제되었다가 다시 생성되는 동안 받게 되는 이벤트가 손실될 위험이 있습니다.
useRef를 이용해 각 state마다 ref를 하나씩 선언해 ref.current를 참조하는 방법도 있습니다. ref.current는 참조가 변하지 않으므로 고립된 리스너 함수 내에서도 참조를 사용해 최신화된 값을 얻을 수 있지만, 값을 업데이트할 때마다 ref와 state 모두를 업데이트해야 하는 불편함이 있고, ref로 관리해야 하는 데이터가 한둘이 아니기에 로직이 크게 복잡해집니다.
그래서, 각 UserVideo에 커스텀 훅을 두어, 각 streamManager를 리스닝하도록 구현해, 최신화된 값을 얻는 한편 재사용성을 늘리고, 로직을 간단하게 분리시킬 수 있는 장점을 얻어내고자 이러한 선택을 했습니다.
❓ 리스너, state 사용하지 않아도 일단 이벤트가 실행되면 streamManager 객체의 값은 자동으로 변경되는데, 왜 굳이 리스너와 state를 사용해야 하죠?
💡 최신화된 정보를 성공적으로 얻기 위함입니다.
streamManager 객체의 값이 자동으로 변경되어도, 이는 streamManager의 참조가 변한 것이 아닌 단순히 그 내부의 객체의 값이 변경된 것이기 때문에, React는 이 변화를 감지할 수 없습니다. 그래서, 실제로는 객체의 값이 변경되어도 업데이트가 일어나지 않습니다.
그렇기 때문에, 리스너를 사용해 그 이벤트가 실행되는 타이밍을 입수하고, 이를 바탕으로 React가 인식할 수 있는 형태인, 바로 state를 업데이트 하는 방법을 사용하는 것입니다.
논의
논의보다는 이후 해야 할 고민을 적어둡니다.
현재 구현된 컴포넌트는 nickname 및 avatar을 prop으로 받습니다만, OpenVidu에서 참가자의 이름을 접근했을 때도 OpenVidu의 StreamManager 타입의 객체를 이용했듯이, 참가자의 이름에 접근할 때도 과연 이 객체를 이용해야 할 지가 고민되는군요. 프로필 사진도 마찬가지입니다.
회원 제도가 없는 어플리케이션이라면 OpenVidu의 튜토리얼과 비슷하게 구현하면 되지만, 우리는 사용자의 정보를 관리하고 제공해 줄 백엔드 파트의 개발자분들이 계시기 때문에, 이 정보들을 백엔드 서버로부터 내려받을 지도 모르는 일입니다.
일단은 어느 쪽인지 확실하지 않으므로, 현재의 구현 방식을 적용했습니다만, 추후 바뀔 수 있을 겁니다.
이슈 번호
작업 요약
본 PR에서는 참가자 한 명에 해당하는 회의 화면을 나타내는 컴포넌트를 신규 생성 및 구현했습니다. --
<UserVideo>
1️⃣
<UserVideo>
신규 생성 및 구현StreamManager
객체를 prop으로써 받으며, 이를 바탕으로 화면을 띄우는 것은 물론, 여러 필요한 기능들에 대한 리스너를 등록합니다.kim-aide
서비스의 로고(임시 로고)가 보여지며, 이 로고의 크기는 화면의 크기에 따라 적절하게 변화합니다.2️⃣ 라이브러리 설치
openvidu-browser
를 설치했습니다. 버전은^2.30.1
입니다.참고/인용 자료
1️⃣ 작동 화면
실제
streamManager
객체가 들어가서 작동하는 것은 아닙니다. 일부 프로퍼티만을 사용한 가짜 객체를 이용해 storybook에서 테스트한 것입니다. 해당 스토리에서는 여러 작업이 규칙성 없이 자동으로 실행되는 모습입니다.https://github.com/user-attachments/assets/b061e107-bc82-471e-b9f0-5252586e1514
2️⃣ 참고 자료
제 OpenVidu 연습용 저장소인
webrtc-frontend-practice
에서 앞서 본 기능을 실제로 테스트하기 위한 커밋입니다.OpenVidu의 공식 문서/치트시트의 정보입니다.
stream.changedProperty
에 대한 설명3️⃣ 원리 설명
UI 자체는 어렵지 않으나 라이브러리와 연관되어 있는 로직이다 보니까 라이브러리를 이해하고 이를 바탕으로 어떻게 구조를 잡아야 하는지가 훨씬 오래 걸렸던 컴포넌트입니다...! 어떻게 이 컴포넌트가 OpenVidu 라이브러리와 맞물려 작동하는 것일까요?
플로우는 아래와 같습니다. 당연하지만, 이 방법은 유일한 방법이 아니며, 수많은 방법들 중 하나라고 생각합니다.
streamManager
객체를 prop으로써 부여받습니다.<UserVideo>
는 제공받은streamManager
를 바탕으로 비디오를 송출합니다. 또한,useStreamManagerProperties
커스텀 훅으로부터 정보를 얻기 위해streamManager
객체를 전달합니다.useStreamManagerProperties
는<UserVideo>
로부터 필요한 객체인streamManager
를 받으며, 이streamManager
에 대한 리스너를 등록합니다. 다양한 리스너들을 등록할 수 있지만, 현재 이 컴포넌트에서 제공해야 하는 기능을 생각했을 때 아래의 세 가지 리스너를 등록해야 한다고 판단했습니다. 이 리스너들이 호출되면, 이 커스텀 훅이 지니는 state가 업데이트됩니다.streamPropertyChanged
: 프로퍼티가 변경된 경우 호출되는데, 이 안에는videoActive
라는 프로퍼티가 있기 때문에 참가자가 캠을 켜거나 끈 경우reason
값을 통해videoActive
가 변경되었음을 알 수 있으며,setVideoActive(true | false)
를 통해 최종적으로 캠의 켜기/끄기를 반영할 수 있습니다.publisherStartSpeaking
: 참가자가 발언하기 시작해 음성이 감지되면 호출됩니다. 이 이벤트가 호출되었을 경우 state를 업데이트해 주면 됩니다.publisherStopSpeaking
: 참가자가 발언을 끝내 음성이 더 이상 감지되지 않으면 호출됩니다. 이 이벤트가 호출되었을 경우 역시 적절하게 state를 업데이트해 주면 됩니다.useStreamManagerProperties
커스텀 훅에서 리스너가 실행되어 state가 업데이트되면,<UserVideo>
에도 영향을 주게 됩니다.<UserVideo>
는 변경된 state를 기반으로 자신의 UI를 상황에 맞게 업데이트합니다.4️⃣ 질문/답변
❓ 왜 모든
streamManager
들을 통제할 수 있는 최상단의<App>
(또는 그에 준하는 커스텀 훅)에 리스너를 두지 않는 것인가요?💡 최상단의 로직이 복잡해지고 관리가 어려워질 것이라고 판단하여 그렇게 하지 않았습니다.
streamManager
도 여러 개일 것이고, 그에 따라 자연스럽게 배열 형태의 데이터가 형성되고, 이를 리턴하여 여러<UserVideo>
가 사용할 수 있도록 하는 구조일 것입니다. 문제는 이 방법은 1) 리스너가 클로저 내부에 고립되어 낡은 상태값만을 볼 수 있는데 / 2) 여러 상태를 참고하여 배열을 업데이트해야 하는 구도가 만들어지게 됩니다. 하나의 상태만을 참고한다면 React의 functional update를 사용해 최신의 이전 값을 가져올 수 있지만, 두 개 이상이므로 문제가 됩니다. 결론적으로 리스너에서 이전 데이터의 값만을 볼 수 있어 변경사항이 제대로 반영되지 않습니다.useEffect
를 사용해 리스너의 이벤트 하나가 실행될 때마다streamManager
상태 배열 값 등을 최신화시킬 수 있도록 그때그때 리스너를 제거했다가 다시 생성하는 방법도 있습니다. 이 방법은 최신화된 값을 얻을 수 있지만, 로직이 복잡하며 리스너가 삭제되었다가 다시 생성되는 동안 받게 되는 이벤트가 손실될 위험이 있습니다.useRef
를 이용해 각 state마다ref
를 하나씩 선언해ref.current
를 참조하는 방법도 있습니다.ref.current
는 참조가 변하지 않으므로 고립된 리스너 함수 내에서도 참조를 사용해 최신화된 값을 얻을 수 있지만, 값을 업데이트할 때마다ref
와state
모두를 업데이트해야 하는 불편함이 있고,ref
로 관리해야 하는 데이터가 한둘이 아니기에 로직이 크게 복잡해집니다.그래서, 각
UserVideo
에 커스텀 훅을 두어, 각streamManager
를 리스닝하도록 구현해, 최신화된 값을 얻는 한편 재사용성을 늘리고, 로직을 간단하게 분리시킬 수 있는 장점을 얻어내고자 이러한 선택을 했습니다.❓ 리스너, state 사용하지 않아도 일단 이벤트가 실행되면
streamManager
객체의 값은 자동으로 변경되는데, 왜 굳이 리스너와 state를 사용해야 하죠?💡 최신화된 정보를 성공적으로 얻기 위함입니다.
streamManager
객체의 값이 자동으로 변경되어도, 이는streamManager
의 참조가 변한 것이 아닌 단순히 그 내부의 객체의 값이 변경된 것이기 때문에, React는 이 변화를 감지할 수 없습니다. 그래서, 실제로는 객체의 값이 변경되어도 업데이트가 일어나지 않습니다.그렇기 때문에, 리스너를 사용해 그 이벤트가 실행되는 타이밍을 입수하고, 이를 바탕으로 React가 인식할 수 있는 형태인, 바로 state를 업데이트 하는 방법을 사용하는 것입니다.
논의
논의보다는 이후 해야 할 고민을 적어둡니다.
nickname
및avatar
을 prop으로 받습니다만, OpenVidu에서 참가자의 이름을 접근했을 때도 OpenVidu의StreamManager
타입의 객체를 이용했듯이, 참가자의 이름에 접근할 때도 과연 이 객체를 이용해야 할 지가 고민되는군요. 프로필 사진도 마찬가지입니다.