두 번째 프로젝트가 시작되는 첫 날, 동기들끼리 아이디어를 소개하는 시간과 기업에서 프로젝트 소개하는 시간을 가졌다. 프로젝트에 들어가기 훨씬 이전부터 프로젝트는 무조건 기업협업으로 해야겠다고 생각했다. 기업협업의 매리트는 실무경험을 맛 볼 수 있다는 점, 이력서에 수강생들과 한 프로젝트보다는 기업과 했다는 점이 돋보인다는 점이다. 이런 매리트를 나만 생각해 둔 건 아니었다. 정원보다 초과한 지원자수로 코드스테이츠 측에서 설문조사를 통해 가려내겠다고 공지했다. 예전 같았으면 자기소개서 혹은 지원동기 등에 쓸 자신이 없었는데 이머시브 과정을 수강하고 first project를 진행하면서 겪은 일들이 많다보니 할 말이 많아졌다! 고심해서 작성하다보니 제출시간 1분 전에 끝내고 후다닥 보냈다. 이상하게도 내가 붙을 거란 자신감이 있었다. 그리고 정말로 현실로 이뤄졌다.
스트리밍과 실시간 채팅을 구현하기 위해 socket.io를 사용했다. 초반 1주일을 새로운 스택을 공부하는데 시간을 투자한다고 socket.io를 공부를 열심히 했는데 정작 프로젝트에서 다뤄볼 일은 극히 적었다. 백앤드라면 안정적인 서비스를 제공할 필요가 있기 때문에 typescript를 사용했고 typescript와 함께 사용하기 좋은 ORM 으로 TypeORM을 사용했다. 이번에도 선 태스트 후 구현을 하기 위해 mocha/chai를 사용했다. 하지만 첫 번째 프로젝트보다는 테스트가 어려웠다. 테스트 파일은 js로 작성하고 실제 api 구현할 때는 ts로 작성했기 때문이다. 아무래도 typescript, typeorm, socket.io 그리고 스트리밍 까지 새롭게 배우는 과정이 있기 때문에 러닝커브를 고려해서 테스트 작성에 신경을 덜 쓰고 싶었다. js가 편하니 금방 작성할 거라 생각했다. 하지만 ts와 js와 혼용해서 쓰니 작동이 생각대로 잘 되지 않았다. 급하게 ts로 변환하려고 했으나 수많은 에러가 나와서 포기했다. https 문제를 한 번 겪고 나서 session대신 JWT를 사용했고 그 덕분에 소셜로그인을 빠르게 구축할 수 있었다. 클라이언트와 서버 모두 빠르게 배포를 한 덕분에 마지막에 배포를 못한 채로 발표를 하는 불상사를 막았다(물론 잔 버그가 많지만).
백앤드 , 발표
[google-auth-library](http://socket.io)
를 사용하여 구글 토큰ID 검증 후 JWT Token 발급 스트리밍 자체가 낯설어서 초반에 많이 해맸다. 기업에서 스쳐가듯 언급한 DASH
용어를 구글링해서 dash.js 라이브러리와 MPEG-DASH 프로토콜을 찾았다. 프로젝트에 바로 쓰기 전 샘플로 이것저것 만져보면서 대략적으로 아래와 같이 구현하면 되지 않을까 나름대로 생각해보고 문서화하여 help-desk를 통해 도움을 구했다.
<!DOCTYPE html>
<html>
<head>
<title>Adaptive Streaming in HTML5</title>
<style>
video {
width: 80%;
height: 80%;
}
</style>
</head>
<body>
<h1>Adaptive Streaming with HTML5</h1>
<video data-dashjs-player autoplay src="<https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd>" controls>
<script src="<http://cdn.dashjs.org/v3.1.0/dash.all.min.js>"></script>
</body>
</html>
- 클라이언트 : dashjs cdn과
<video>
태그의 src에 mpd파일이 있는 엔드포인트(url)을 사용하면 스트리밍이 자동으로 구현된다
- 서버 : FFMPEG를 사용하여 mpd 파일들을 생성한다. 이때 다양한 해상도 제공을 위해 여러 mpd 파일들을 생성한다.
- DB 혹은 AWS의 S3에 mpd 파일들을 저장한다.
- 클라이언트의 요청에 따라 mpd 파일을 제공해준다.
위를 통해 생각한 로직은 아래와 같다.
- 클라이언트가 videoId를 params에 담아 요청한다.
- 서버는 params에 담긴 videoId를 통해 DB 혹은 S3에서 엔드포인트의 마지막 mpd 파일 명을 보내준다.
- 클라이언트는 서버로 부터 받은 mpd 파일이름과 S3의 주소와 조합하여 mpd 파일을 실행시킨다.
[결론] help-desk와 기업측의 도움을 받아 스트리밍 구현을 무사히 마쳤다. MPEG-DASH 대신 HLS 프로토콜을 사용하고 FFMPEG로 화질별 .m3u8파일(영상 조각에 대한 정보)과 .ts파일(영상 조각)을 생성 후 S3에 올렸다. dash.js 대신 video.js를 사용했다. video.js는 .m3u8파일을 해석하여 자동으로 시간에 맞춰 .ts파일을 가져오고 인터넷 환경에 따라 화질을 자동으로 변환해줬다.
가령 10분 지점에 놓인 컨트롤러 위치를 20분 지점에 옮겼을 때, 다른 클라이언트들도 20분 지점으로 비디오 재생시점이 옮겨져야 한다.
이를 구현하기 위해 생각한 방법은 video.onseeked
를 통해 얻은 video.currenTime
을 소켓통신으로 서버에 전달하여 소켓과 연결된 모든 클라이언트에 video.currentTime
을 공유하여 싱크를 맞추는 것이다.
[결론] video.js를 사용했기 때문에 currentTime을 가져오는 방식만 달라졌을 뿐 전체적인 로직은 같다.
const player = videojs(videoPlayerRef.current);
player.controlBar.progressControl.on('click', () => {
socket.emit('sendChangeSeeked', { currentTime: player.currentTime() });
});
이 문제를 해결하기 위해 세 가지 방법을 생각해봤다.
이 방법들 중에 어떤 방법이 나을 지 고민을 했고 하나하나 따졌다.
세 번째가 제일 나은 방법이라고 생각을 했지만 구현과정을 어떻게 해야할 지 감을 잡지 못했다.
방에 속한 모든 소켓들을 접근하는 것이 첫 번째 과제였다. 콘솔로그와 디버깅으로 io.sockets.sockets에서 확인할 수 있었다. 하지만 매번 room에 해당하는 소켓을 찾는 번거로움이 있었다.
공식문서를 열심히 들여다보니 실마리를 찾았다. io.of(네임스페이스이름).in(룸이름).clients((에러,네임스페이스의 룸에 해당하는 클라이언트들)=>{ })
socket.io에서 clients 메서드를 제공해주고 있었다.
다음 과제는 임의의 유저 하나에게만 트리거를 시키는 일이다. 이 또한 공식문서에서 방법을 찾을 수 있었다.
위 두 과제를 토대로 아래와 같은 코드를 작성할 수 있었다.
트리거 로직을 그림으로 표현하면 이렇다.
(1) 유저가 url로 스트리밍 화면에 진입하자마자 socket.io로 currentTime
을 요청하는 이벤트를 발동시킨다.
(2) 소켓서버는 임의의 유저(clients 0번째 유저)에게 currentTime
을 알려달라는 이벤트를 트리거 시킨다.
(3) 임의의 유저는 <video>
태그에서 currentTime
값을 받아와 소켓서버에 전달한다.
(4) 임의의 유저로부터 얻은 currentTime
을 다시 url로 진입한 유저에게 전달함으로써 영상 싱크 문제를 해결했다.
express를 먼저 구축한 상태에서 다른 팀원이 작성한 socket 코드를 합치려다 에러가 발생했다.
// server.ts
import app from './index';
import { debugHTTP } from './utils/debug';
/* Server Activation */
const server = app.listen(app.get('port'), () => {
debugHTTP(`server listening on PORT ${app.get('port')}`);
});
export default server;
import SocketIO from 'socket.io';
import server from './server';
const io = SocketIO(server);
listen 전에 socket.io 셋팅을 끝내면 작동이 잘 된다.
// socket.ts
import http from 'http';
import SocketIO from 'socket.io';
import app from './index';
import { debugINFO } from './utils/debug';
const server = http.createServer(app);
const io = SocketIO(server);
io.on('connection', (socket) => {
debugINFO('an user connected');
socket.emit('message', 'welcome!');
});
export default server;
//server.ts
import server from './socket';
import { debugHTTP } from './utils/debug';
/* Server Activation */
server.listen(4000, () => {
debugHTTP('server listening on PORT 4000');
});
First Project처럼 모각코를 하면서 진행을 했다. 팀원으로 참여해서 코드 작성에 더 집중할 수 있었다. 다만 백앤드 포지션이 나뿐이라 처음에는 많이 걱정했다. 혼자서 백앤드쪽을 담당할 수 있을까?하며. 다행히 팀원 한 분이 풀스택으로 뛰어줘서 부담이 줄었다. 스트리밍 자체가 코드스테이츠 엔지니어분들께도 낯설었기 때문에 크게 도움을 받지 못했다. 다행히 기업측에서 힌트를 주셔서 잘 해나갔다. 마지막 데모데이에 발표를 담당했다. 유튜브로 박제되기 때문에 더 긴장했다. 대본을 보지 않고 발표할 수 있도록 10번 이상 연습했다. 발표는 무사히 치렀는데 질의응답에서 예상한 질문과 다른 질문들이 쏟아져나왔고 조금 횡설수설하게 답변한 점이 없잖아 있다. 하하
- Task 카드를 적절히 나눴다. 스프린트에 끝내야 할 과제는 제때에 다 구현할 수 있었다.
- 크롬 확장프로그램인 TurboTimer
를 사용해서 정확한 consumed time을 측정했다. 써보니 너무 편리해 동기들한테도 알려줬다!(좋은 건 나누는 사람)
- 노션 툴을 사용해서 레퍼런스와 문제해결과정을 정리했다. 덕분에 코드 구현할 때 빠르게 참고하여 적용할 수 있었다. Final Project 후기를 쓸 때도 편하다!
- 첫 번째 프로젝트보다 한 게 너무너무 없다. 페이지는 3개 이고 내가 담당한 건 API 구현 뿐이다. 4주 프로젝트로 내세우기엔 부족한 점이 많다.
- 테스트 작성을 js로 해서 제대로된 end to end 테스트가 되지 못해 아쉽다. typeorm에서 test 전용 DB설정에 어려움을 겪었다. 결국 첫 번째 프로젝트처럼 dev 버전에 몽땅 처리했다.
- HTTPS를 이번에는 꼭 구현하리라 마음먹었는데 생각처럼 되지 않았다. 도메인도 구매했는데 속상하다. 개인프로젝트에서 꼭 반영할 생각이다.
- 팀원들의 공로를 늦게서야 인정했다. 사소한 부분이라도 칭찬을 듬뿍해줄 생각이었는데 새로운 스택 공부 때문에 정신이 없던 탓에 잘 하지 못했다. 그래서 끝무렵에 그동안 스쳐가듯이 받았던 고마운 점 그리고 훌륭했던 점을 한분 한분께 알려드렸다.
프로젝트 수고하셨어요! 수료 축하드려요! 앞으로도 화이팅!