
샤피라이브는 GSSHOP 의 대표 라이브커머스 서비스입니다.
코로나 19로 언택트 시대가 성큼 다가온 지금, 우리 삶의 많은 부분이 달라졌습니다. 우리의 삶이 바뀐 만큼 소비 방식에도 큰 변화가 있었습니다. 2020년 1월을 기점으로 온라인 소비의 비중이 오프라인 소비를 넘어섰고, 점차 온라인 소비의 비중은 늘어나는 추세입니다.
라이브커머스는 오직 상업 활동에 국한되지 않는 하나의 콘텐츠이자 소위 ‘MZ세대’의 새로운 쇼핑문화로 정착되고 있습니다. 스마트폰 한 대로 모든 과정을 완료할 수 있어 편리하고,기존 홈쇼핑의 엄격한 방송 심의가 없어 소비자와 편하게 소통할 수 있는 플랫폼으로 사용되기 때문입니다.
기존 홈쇼핑이나 온라인 쇼핑몰은 일방적인 커뮤니케이션을 진행하지만, 라이브커머스는 판매자와 소비자가 함께 소통합니다. 일방적 커뮤니케이션일 때는 판매자 입장에서만 정보를 전달하지만, 양방향 커뮤니케이션에서는 소비자가 알고 싶은 부분도 실시간으로 바로 알 수 있습니다. 이것이 기존의 판매 방법보다 라이브커머스가 구매 전환율이 높은 이유입니다.
이런 양방향 커뮤니케이션을 더욱 원활히 하기 위해서는, 동영상의 낮은 지연시간(Latency)와 안정성(Stability) 이 필수적이라 볼 수 있습니다. 이를 위해서 GSSHOP의 샤피라이브 서비스는 WebRTC를 선택했고, 최근 GSSHOP 어플리케이션을 크로스플랫폼 프레임워크인 Flutter로 다시 만드는 과정에서, 샤피라이브 서비스에 WebRTC를 적용/개편하는 작업도 같이 진행할 수 있었습니다. 오늘은 제가 모바일 개발자로서, GSSHOP 어플리케이션에 WebRTC를 적용시키며 했던 경험과 지식에 대해서 이야기를 하고자 합니다.
* Flutter 어플리케이션을 만들었던 경험이 궁금하다면 여기를 클릭해 주세요!
GS Retail의 자랑인 김요한 FO Product 센터장님이 작성하신 좋은 글을 볼 수 있습니다!
WebRTC란?

간단히 요약하면, WebRTC는 미디어(음성/영상), 데이터(파일/텍스트)를 인터넷이 있는 어느곳이든 가장 빠르게 전달해줄 수 있는 기술입니다.
현재 가장 ‘핫’하게 사용되는 통신기술로, 기존에 사용하고 있던 스트리밍 방식인 RTMP, HLS(m3u8)에 비해 획기적으로 적은 지연시간 (~1초)으로 데이터를 주고받을 수 있는 기술입니다.
더불어 구글, 페이스북, 트위치 등 세계적 기업에서도 앞다투어 적용하고 있는 기술로, 안정성/범용성까지 검증된 기술이라 볼 수 있습니다.
많은 통신기술 중 왜 WebRTC 인가?
WebRTC는 여러가지 장점을 가지고 있지만, 단연 ‘저지연(Low Latency)' 때문입니다.
샤피라이브 시청자들의 시청자들의 원활한 시청경험(QoE: Quality-Of-Experience)을 위해서는, Latency가 매우 중요한 역할을 합니다.
WebRTC는 실시간(Sub-Second급)에 가까운 지연시간을 가지고 있으며, 최적의 상황에서 1초 이내의 지연율을 가지고 있는 기술입니다.

또한 Web-RTC로 구현 시, 기존 동영상서비스 대비 월등한 Latency를 보유할 수 있습니다.
- Facebook Live, 7–13 seconds
- Cable TV, 12 seconds
- Twitch stream, 10–30 seconds
- Voice chat, less than 1 second
- WebRTC, 0.2-1 second
동영상 제공 품질이 시청자에 미치는 영향은?
동영상 재생은 크게 준비 단계와, 재생단계로 구분됩니다.

- 준비단계 (Preparing Latency + Initial Buffering)
동영상이 시작되기 전, 영상이 화면에 표시될 때 까지의 시간이 포합됩니다. 이 시간이 길어지면 시청자 이탈이 발생하며, 약 10초의 지연시간이 있을 때 50%의 시청자가 이탈하는 것으로 알려져 있습니다. 이 시간이 짧을수록 좋지만, 너무 짧게 줄이면 버퍼에 충분한 데이터를 확보하지 못해서 재생중 추가 버퍼링이 발생할 확률이 높습니다. 현재 샤피라이브 서비스에는 약 1~2초의 Start Buffering이 적용되어 있습니다.
- 재생단계 (Playing Phase)
재생단계에서의 Stalling 혹은 Buffering은 시청자 이탈율을 높이며 전체 시청시간 감소를 유발할 수 있습니다. 이를 측정하는 기준이 여러가지가 있는데 그 중 하나가 Buffering Rate로 다음과 같이 정의 합니다.
Buffering Rate (%) = Buffering Time / (Total Time + Buffring Time) * 100
1% 증가할 때 마다 시청자당 시청시간이 월 평균 약 14분 정도 낮은것으로 알려져 있으며, 평균 1% 이상인 경우 저품질 서비스로 판단하고 0.4% ~ 0.1% 정도면 충분히 좋은 서비스로 판단될 수 있습니다.
일반적인 상황에서 샤피라이브는 0% 에 가까운 버퍼링을 가지고 있습니다.
GSSHOP 앱으로의 WebRTC 적용
사실, 새로운 GSSHOP 앱에 WebRTC를 적용시키는 것은 큰 도전이었습니다. 현재 큰 규모의 Flutter 앱에서 WebRTC를 안정적으로 서비스하는 전례가 없었기 때문에, 예상치 못한 에러나 기술적 난관에 봉착했을 때 혼자서 풀어나가야만 했기 때문입니다.
또한 Flutter와 같은 Cross-Platform 앱의 특성 상, 새로운 기능을 추가할 때는 항상 Android와 IOS 모두를 안정적으로 지원할 수 있는 환경을 생각해야만 합니다. 보통, 특정 하드웨어의 특정 기능(인코더/디코더)을 사용하고자 할 때는 필수적으로 두벌의 코드 작성(Android/IOS)이 강제됩니다.
그렇기 때문에 검증된 Native(Android/IOS) 의 코드로 Flutter의 PlatformView 기능을 이용해 울며 겨자먹기로 두벌의 개발을 하려던 찰나, 다행히 SIP전문 개발자가 만든 Android / IOS 소스가 완벽히 Wrapping된 오픈소스 Flutter WebRTC 라이브러리를 찾을 수 있었고, 약간의 고난은 있었지만 GSSHOP 앱에 큰 이슈 없이 올릴 수 있었습니다.

모바일 개발자 입장에서의 WebRTC
WebRTC는 분명 좋은 기술이지만, 항상 확장성 이슈나 미디어서버 작성같은 까다로운 문제가 뒤따릅니다. 그렇기 때문에 미디어 스트림을 안정적으로 제공해줄 수 있는 경험과 능력을 가진 전문팀이 필요하고, 이번 샤피라이브 프로젝트에서도 서버사이드 작업을 해줄 팀과 협업을 통해 서비스를 원활히 구현할 수 있었습니다.
보통 영상 전송을 위해서는 서버/클라이언트 모두 상당히 많은 일을 해야만 하지만, WebRTC는 비교적 많은 부분이 표준화가 되어있기에 크게 신경 쓸 부분이 없으며, Application 에서의 작업은 크게 두가지만 신경쓰면 됩니다.
바로 Signalling과 Rendering 입니다. 모바일 개발자는 서버사이드에서 제공해주는 규격에 맞춰 내용을 구현하면, 문제없이 WebRTC로 세션을 개시할 수 있고, 최종적으로는 Flutter에서 영상스트림과 음성스트림을 (Texture / Audio channel)에 Rendering 해주면 동영상을 재생할 수 있습니다.
Signalling
WebRTC를 처음 접하는 많은 개발자들이 까다로워하는 부분입니다.
WebRTC API에 의해 자동적으로 구현되지 않는 부분이며, 인터넷에 있는 ‘누군가'와 Session을 개시하기 위해서 꼭 필요한 절차입니다.
WebRTC 어플리케이션이 목적지와 'CALL'을 초기화하기 위해서 클라이언트는 다음과 같은 정보의 교환을 필요로 합니다.
- 통신을 열고 닫는데 사용되는 세션 컨트롤 메세지들.
- 에러 메세지들.
- 코덱이나 코덱 설정, 대역폭, 미디어 타입 같은 미디어 메타데이터.
- 보안 연결을 수립하기 위해 사용되는 키 데이터.
- 밖에서 보이는 것처럼 호스트의 IP 주소와 포트와 같은 네트워크 데이터.
이 정보들은 SDP(세션 기술 프로토콜)라 불리는 형태로 정제되어 다른 기기와 서로 정보를 교환하게 됩니다.

자세한 규격에 대한 내용이 궁금하신 분은 여기 를 참고 부탁드립니다.
이렇게 작성된 SDP는 아래와 같은 절차로 상호간 제안/응답할 수 있습니다.

- Alice 가 SDP 형태의 Offer 메시지를 생성합니다.
- Alice가 생성된 Offer 메시지를 본인의 LocalDescription으로 등록합니다.
- Alice가 Offer메시지를 시그널링 서버에게 전달합니다.
- 시그널링서버는 상대방 Bob을 찾아서 SDP 정보를 전달합니다.
- Bob은 전달받은 Offer메시지를 본인의 RemoteDescription에 등록합니다.
- Bob은 Answer 메시지를 생성합니다.
- 생성된 Answer 메시지를 본인의 LocalDescription으로 등록합니다.
- Bob은 Answer 메시지를 시그널링서버에게 전달합니다.
- 시그널링서버는 상대방 Alice를 찾아서 Answer 메시지를 전달합니다.
- Alice는 전달받은 Answer 메시지를 본인의 RemoteDescription에 등록합니다.
위에 같이 SDP를 서로 교환한 후, Alice와 Bob 은 서로의 주소 값을 알기 위해 ICE Candidate를 교환합니다. ICE는 Interactive Connectivity Establishment의 약자로, 이름에서 보이듯 상호간에 통신을 개시하기 위해 정의된 기술입니다. ICE는 보통 모든 네트워크 상황을 지원하기 위해 STUN과 TURN 서버와 같이 구성됩니다.
STUN, TURN, ICE 의 자세한 관계는 여기를 참고 부탁드립니다.

샤피라이브의 경우에는 위와 같은 절차로 SDP를 교환하고, ICE에게 어떤 서버에서 어떤 비디오/오디오를 받을 지 정한 다음, 사용자에게 보여줄 샤피라이브 영상을 받을 수 있었습니다.
Rendering (Video / Audio)
사용자가 보기에 동영상은 하나의 채널로 이루어진 매체이지만, 사실 내부는 Video와 Audio의 두 채널로 이루어져 있습니다. 두 채널을 적절히 열어서 Signalling 단계에서 받은 스트림을 꽂아넣으면 사용자에게 '동영상'을 보여줄 수 있는 겁니다.
원래 전통적 동영상 스트림에서는 오디오와 비디오 각 채널간 싱크를 맞추는 것도 큰 과제였지만, WebRTC 규격 아래에서는 TimeStamp도 API를 통해 제어되기 때문에 적어도 모바일 개발자는 싱크 문제에서 자유롭게 되었습니다. 모바일 클라이언트 단에서는 A/V 랜더링, 스트림을 랜더링해주는 과정에서 일어나는 Jank(Lag)의 제어, 그리고 다른 UI개발등에 더욱 시간을 쏟을 수 있게 된 것입니다.
- Video Rendering
시그널링의 어려움에 비해, 비디오 랜더링은 무척 단순합니다.
비디오 랜더링 작업의 핵심 골격만 보자면 아래 코드로 요약이 가능합니다.
RTCVideoRenderer _renderer;
RTCVideoRendererNative get videoRenderer =>
_renderer.delegate as RTCVideoRendererNative;
_signaling?.onAddRemoteStream = ((_, stream) {
_renderer.srcObject = stream;
});
Texture(
textureId: videoRenderer.textureId!,
filterQuality: FilterQuality.low,
)
1. Signalling을 통해 얻어지는 onAddRemoteStream 콜백으로부터 RTCVideoRenderer 를 얻습니다.
2. RTCVideoRenderer로부터 RTCVideoRendererNative 를 얻습니다.
3. RTCVideoRendererNative의 textureId를 Flutter 기본 위젯인 Texture 위젯에 알려줍니다
위의 3단계만 거치면 Texture Widget에서 받아온 Video Stream 을 자동적으로 랜더링 할 수 있습니다!
* 현재 샤피라이브는 양방향 중계가 아니기 때문에, Negotiation 단계에서 서버가 제공해준 규격(bitrate, encoding 규격 등)을 그대로 따르고 있습니다. 특별히 영상 규격에는 손대지 않고, 랜더링 옵션만 FilterQuality.low(높은 품질) 로 설정한 상태입니다.
When the texture is scaled, a default FilterQuality.low is used for a higher quality but slower interpolation (typically bilinear)
- Flutter.dev
- Audio Rendering
오디오 랜더링은 의외로 살짝 까다롭습니다. Audio Stream은, Negotiation 단계에서 설정만 잘 되면 WebRTC 가 정해진 채널로 자동 랜더링 해주지만, 소리채널 선택, 볼륨/음소거 제어, Audio 스트림 랜더링, Track 생명주기 관리 등 수동으로 잡아야 할 작업이 꽤 있었습니다.
1. 채널
안드로이드 기기 기준 [VoiceCall, System, Ring, Music, Alarm, Notification] 6개의 채널을 간섭 없이 제어해줘야만 원활히 사운드를 들을 수 있습니다.
원하지 않는 엉뚱한 채널로 음성이 연결될 경우, 소리제어 불가, 스테레오 사용 불가 등 원활한 음성 청취가 불가능 합니다.
(현재는 기술적 제약으로 원래 사용해야 하는 Music 채널이 아닌, VoiceCall 채널을 이용하고 있습니다. 추후 Flutter WebRTC의 Android Native function 이 수정되는 대로 GSSHOP 앱도 수정될 예정입니다.)
2. 볼륨제어
볼륨제어도 까다로운 부분이 있습니다.
원하는 채널에서의 Volume 을 조절해야 했기에 Native Method Channel 을 이용해서 기능을 구현해야만 했으며,
static Future<void> setVolume(double volume, MediaStreamTrack track) async {
if (track.kind == 'audio') {
if (kIsWeb) {
final constraints = track.getConstraints();
constraints['volume'] = volume;
await track.applyConstraints(constraints);
} else {
await WebRTC.invokeMethod(
'setVolume',
<String, dynamic>{'trackId': track.id, 'volume': volume},
);
}
}
return Future.value();
}
볼륨조절 1~15 단계의 조절은 가능했지만, 플랫폼에서의 제한으로 볼륨 0단계는 존재하지 않았기에, Mute(음소거) 기능을 위해 stream을 순간적으로 내렸다가 살려주는 기능을 을 통해 구현해야만 했습니다.
if (volume > 0) {
log(volume.toString());
_mediaStream?.getAudioTracks().first.enabled = true;
} else {
log(volume.toString());
_mediaStream?.getAudioTracks().first.enabled = false;
}
3. Track Lifecycle
오디오 채널의 기본 라이프사이클은, 개설된 스트림이 있다면 앱이 살아있는 동안 항상 유지될 수 있습니다. 앱을 보고있다가 화면이 전환되거나, 백그라운드로 간다고 해서 나오는 소리가 자동적으로 해제가 되지 않습니다.
그렇기 때문에, Flutter의 Lifecycle 관련 코드(initState, dispose 등)에 오디오 채널을 제어해 줄 수 있는 코드를 넣었고, 화면 진입/해제에 따른 오디오 채널 제어를 할 수 있었습니다.
@override
void initState() {
super.initState();
initRenderers();
}
@override
void dispose() async {
super.dispose();
if (_progressBarTimer != null) {
_progressBarTimer!.cancel();
}
await _signaling?.close();
await _remoteRenderer.dispose();
}

추후 어디에 더 적용을 시킬 수 있을까?
이번 프로젝트에서는 WebRTC의 Low-Latency 특성을 이용해서 호스트와 구매자가 실시간 소통할 수 있는 샤피라이브 서비스를 성공적으로 런칭할 수 있었습니다.
WebRTC를 이용하면 그동안 고착되어있던 커머스 환경에서 벗어나, VR(가상현실), AR(증강현실), MR(혼합현실)을 통해 구현된 가상 환경에서 물건을 구입하거나, 물류관리에서 도움을 받는 등 다양한 분야에서 사용할 방법이 있습니다.

마치며
Flutter도 WebRTC도 IT 산업에서 비교적 최신 기술로 분류되는 기술입니다. 한명의 개발자로서 최신 기술을 사용해 서비스를 구축하는 경험은 항상 꿈같은 일이며, 서비스를 구축하며 몸은 정말 힘들었지만 마음만은 프로그래밍을 처음 배우던 그때처럼 정말 행복했습니다. 새로운 도전을 하게끔 허락해주신 뉴테크본부의 이종혁 상무님, 김요한 센터장님과, 같이 정말 힘들게 프로젝트를 한 Mobile 개발팀 전원에게 감사의 인사를 전하며 글을 마칩니다.
감사합니다.

임동현 | 뉴테크본부 > Mobile 개발팀
Mobile 개발팀의 Flutter 앱 개발자 입니다.
GSSHOP 앱에서 상품과, WebRTC 플레이어를 담당하고 있습니다.
References
[1] (WebRTC Official) - https://webrtc.org/
[2] (WebRTC Standard) - https://developer.mozilla.org/ko/docs/Web/API/WebRTC_API
[3] (WebRTC Medium) - https://medium.com/exmachinagroup/the-ultra-low-latency-video-streaming-roadmap-from-webrtc-to-cmaf-5b0d8b4ceec2
[4] (WebRTC Flutter Library) - https://pub.dev/packages/flutter_webrtc
[5] (Flutter Platform View) - https://medium.com/flutter-community/flutter-platformview-how-to-create-flutter-widgets-from-native-views-366e378115b6
[6] (Video Play Principle) - https://www.huawei.com/minisite/4-5g/en/industryjsdc-j.html
[7] (국제표준 SDP 규격) - https://datatracker.ietf.org/doc/draft-nandakumar-rtcweb-sdp/?include_text=1
[8] (샤피라이브 SDP 규격) - https://airensoft.gitbook.io/ovenmediaengine/
[9] (무료이미지) - https://www.pexels.com/ko-kr/
[10] (A/V Synchronization) - https://en.wikipedia.org/wiki/Audio-to-video_synchronization
[11] (WebRTC Negotiation) - https://blog.mozilla.org/webrtc/perfect-negotiation-in-webrtc/
[12] (Native Method Channel) - https://docs.flutter.dev/development/platform-integration/platform-channels
Special Thanks To
- 클라우드팀
- 라이브커머스마케팅팀
- 영상제작2팀
- 정진영 매니저님