# LiveMD 면접준비 (2021.04.01)
## 공모전 관련 기사
`포폴 만들때 참고 ㄱㄱ`
https://www.itbiznews.com/news/articleView.html?idxno=25913
https://www.digitaltoday.co.kr/news/articleView.html?idxno=256201
http://www.epnc.co.kr/news/articleView.html?idxno=111031
## 프론트엔드 관련 면접에 도움될 URL
https://github.com/JaeYeopHan/Interview_Question_for_Beginner/tree/master/FrontEnd
https://github.com/Febase/FeBase
https://velog.io/@yaytomato/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0
## 😎 현재 LiveMD의 문제점
http://pplus.co.kr/news/?mod=document&uid=102
### 백엔드
1. 서버 이중화(failover, 로드밸런싱)
> 현재 LiveMD는 EC2하나에 NginX 웹 서버와 나머지 모든 서버가 올라가있으므로 서버 이중화에 대한 고민은 없는 상황이다.
> 서버 이중화의 목적은 크게 `2가지`가 있다.
> 1. Failover(시스템 대체 작동)
> - `Failover`는 평소 사용하는 서버와 그 서버의 클론 서버를 가지고 있다가 사용 서버가 장애로 사용이 어렵게 되었을 경우, 클론 서버로 그 일을 대신 처리하게 해서 `무정지 시스템을 구축`하게 해 주는 것을 의미한다.
> 2. LoadBalance(부하분산, 부하균형)
> - `LoadBalance`는 두 개 이상의 서버가 일을 `분담처리` 하여 서버에 가해지는 부하를 분산시켜주는 것을 의미한다.
active-active 구조 / active-standby 구조
#### 그렇다면 서버 이중화를 할 수 있는 방법은?
> `AMI`(Amazong Machine Image)로 EC2 서버를 복제하고,`ELB`(Elastic Load Balancing)를 사용하여 서버 이중화를 사용할 수 있다. => 성능개선(Scale Up) , 부하분산(Scale out)
> `AutoScailing`을 통해서 ELB를 자동화 할 수 있다.
> 트래픽이 많아지면 자동으로 서버를 생성하고 트래픽이 적어지면 서버를 중단한다.
> https://opentutorials.org/course/608/3008 (참고)
#### 고민..
- MSA 아키텍처 안에서 서버 이중화란 말은 무엇일까?
- 이중화의 개념과 마이크로서비스의 개념이 뭐가 다른지?
- 서버 이중화는 모놀리식에만 해당하는거 아닐까?
> 병선 - 서버 이중화에 대한 정의는 모놀리식 서비스에서 적용할 수 있는 단어로 생각할 수 있다. 서버 이중화의 목적중 하나인 Scale out(로드밸런서)은 MSA 아키텍처에서 API gateway로 자동적으로 적용되는 부분일 수 있다. 그렇다면 MSA 서비스에서는 서버 이중화라는 단어가 관련없는 것인가?
> 내 생각에는 아니다. MSA는 모놀리식 서비스가 모여서 하나의 모놀리식 서비스로 제공되는 개념으로 생각한다면 MSA 서비스 안에서도 서버 이중화는 적용할 수 있다고 생각한다. 즉 MSA 서비스에서는 작은 서비스가 하나의 소규모 팀 단위로 독자적으로 개발이 진행되는데, 소규모 팀 단위에서 진행하는 작은 서비스에 충분히 서버 이중화를 적용할 수 있다고 생각한다.
> 성원 - 서버 이중화와 MSA는 서로 관련없는 다른 개념이다. 둘이 연관지어서 생각할 이유가 없다. 서버 이중화는 부하 분산과 failover에 목적을 두고 있고, MSA는 기존의 모놀리식 아키텍쳐에 대한 문제점을 보완하기 위한 개념이다.
- 그렇다면 LiveMD에서는 NginX가 곧 API 게이트웨이 역할을 하고 있는 것인가?
> LiveMD 에서는 NginX 리버스 프록시가 곧 API 게이트웨이 역할을 하고 있다. MSA 서비스에서는 API gateway 기술이 거의 필수적으로 들어간다. 하지만 더 나은 서비스 개발 관점에서 생각해본다면 AWS에서 제공하는 API gateway 서비스를 사용하는 것도 생각해 볼 수 있다.
- 서버 이중화와 데이터베이스 이중화 차이? (둘 다 active-active, active-standby 개념이 나옴)
> 둘 다 부하 분산과 failover에 목적을 두고 있다. 이중화를 하게 되면 데이터의 정합성을 고민해야 한다.
3. scale out 시 서버에서 관리하는 유저 정보들을 어떻게 관리해야 할지
> 1. **Sticky Session** - 특정 사용자는 세션이 유지되는 동안 특정 서버에만 요청을 유지한다. 이것은 세션이 유지되는 동안 동일한 서버만을 사용하기 때문에 정합성 이슈에는 자유로워질 수 있다. 하지만 고정된 세션을 사용한다는 것은 특정 서버에 트래픽이 집중될 위험이 존재한다. 특정 서버가 장애가 발생할 경우, 해당 서버는 사용자 정보를 잃어버릴 수 있다. 이는 가용성 이슈를 발생시킨다. 또한, 다른 서버는 사용자의 정보를 갖고 있지 않기 때문에 scale out의 장점인 트래픽 분산을 완벽히 활용할 수 없다.
> 2. **세션 클러스터링** - 세션이 생성될 때마다 복제하여 각 서버의 세션 정보를 일치시켜 정합성 이슈를 해결한다. 하지만 매번 세션 객체를 복제하는데 많은 메모리가 필요하므로 오버헤드가 발생할 수 있다.
> 3. **세션 스토리지** - 세션 저장소를 별도로 두는 방법이다. 요청을 처리하는 서버에 세션 정보를 저장하는 것이 아닌 별도의 스토리지 서버에 세션 정보를 저장하는 방식이다. 요청을 처리하는 각 서버들은 세션 스토리지에 저장되어 있는 세션 정보를 읽어들여 요청을 처리한다. 이를 통해 요청을 처리하는 서버가 하나의 세션 스토리지 서버와 통신함으로써 데이터 정합성 문제를 해결할 수 있다. 또한, 세션 생성으로 인한 메모리 사용량을 줄일 수도 있다. 하지만 세션 스토리지에 장애가 발생하면 세션 스토리지에 저장되어 있는 정보를 찾을 수 없는 이슈를 발생할 수 있다. 그래서 세션 저장소 또한 backup 서버를 두어 세션을 복제해놓으면 기존에 있던 세션 스토리지가 다운 되어도 서비스를 지속할 수 있다. 이게 가장 좋은 방법인듯하다.
5. DB 데이터 저장 관련 보완해야하는 부분
6. 도커 쓰는 타당성이 부족함
7. 테스팅 (부하 테스트 등)
8. 로컬 / 개발 / 스테이징 / 운영 서버로 나누어서 작업
9. 아키텍처 그려보기
10. AWS 사용해보기
##### 👪 나중에 EC2로 우리가 해봤던거 다시 처음부터 하면 괜찮을듯!
### 프론트엔드
1. 통일성 없는 상태 관리
2. 통일성 없는 CSS 관리
3. 반응형 디자인
4. 크로스 브라우징
> 크로스 브라우징이란 웹 페이지 제작 시에 모든 브라우저에서 깨지지 않고 의도한 대로 올바르게(호환성) 나오게 하는 작업을 말한다. 이 작업이 필요한 이유는 브라우저마다 렌더링 엔진이 다르기 때문이다.
## 😎 LiveMD를 어떻게 더 좋은 서비스로 발전 시킬 수 있을까?
`MSA에서 적용할 수 있는 서버 이중화` (대용량 트래픽을 예방가능 - 병선)
> 우리 LiveMD에서는 MSA를 기반으로 구현된 프로젝트이다. MSA에서는 API Gateway를 활용하여 각각의 서버로 사용자의 요청을 하나의 EndPoint로 받아 분산시켜주는 것이 필수적이다. LiveMD에서는 NginX 웹 서버가 Reverse Proxy를 통해 API Gateway 역할을 하고 있다. 그러면 이를 통해 API 서버 별 Scale out(부하 분산)은 할 수 있다. 하지만 대용량 트래픽이 발생했을 경우, 특정 서버에만 집중적인 요청이 있다면 이 또한, Scale out(부하 분산)이 필요하다. (예를 들어, 채팅 서버에만 집중적으로 요청이 가는 경우) 이것은 어떻게 해결할까?
> 핵심은 서버 이중화이다. 그렇다면 서버 이중화는 어떻게 구현할까? MSA 를 자세히 생각해보면 하나의 서비스를 여러 개의 서비스로 잘개 쪼갠 모양이다. 잘개 쪼개진 서비스는 곧 하나의 소규모 팀이 맡아 개발 및 유지 보수를 하게 된다. 그렇다면 나눠진 서비스는 하나의 서버를 갖게되는 것이고 소규모 팀은 그 서버를 담당하는 것이다. 즉, 그 서버를 AMI를 통해 서버를 복사하고 ELB를 통해 서버 이중화를 해주면 된다.
> 이러한 방식이 곧 MSA 안에서 수행할 수 있는 서버 이중화이다.
현재 상황: ec2 하나에 도커 올리고 그 위에 모든 서버들이 각각 컨테이너 하나씩 올라가 있음
추가 부분: 이 ec2 자체를 AMI로 만들어서 ec2 두 개를 ELB를 통해 이중화를 시킨다.
> 아키텍처 그려보기 ㄱㄱ , wiki 가서 옛날 아키텍처 그림보면서 옳았나 틀렸나 판단해보기
` `
` `
` `
## LiveMD 면접 준비 키워드
## 조성원
- 리액트
- 왜 리액트를 사용했는지
- 리액트 장점 나열할 것
> 1. **컴포넌트 기반의 라이브러리이다.**
> 이것은 코드의 재사용성과 유지보수성을 증가시켜준다. 헤더, 메인, 버튼, Nav 같은 것들을 하나의 컴포넌트로 관리할 수 있다. 이를 통해 특정 부분에 이슈가 생기면 그 부분에 발생된 이슈만 코드로 해결할 수 있어서 간편하다.
> 2. **자바스크립트 기반이다.**
> 리액트는 자바스크립트 기반의 JSX(Javascript + xml)문법을 지원한다. 그래서 뷰나 앵귤러와 같이 별도의 프레임워크를 배울 필요가 없으며,자바스크립트 기본기와 HTML을 조금 아는 수준에서 리액트를 개발할 수 있다.
> 3. **단방향 데이터플로우**
> 리액트는 부모에서 자식으로 데이터를 보내는 구조이다. 데이터가 변했을 때 UI는 업데이트 된다. 하지만 리액트는 단방향이므로, UI쪽에서 데이터를 변화시키는 것이 불가능하다. 이러한 구조는 디버깅에 많은 도움을 준다. 앵귤러는 양방향 데이터플로우를 갖추고 있는데, 이러한 흐름은 이슈가 발생했을 때 데이터 흐름을 이해하는데 어려움이 있다. 결과적으로, 리액트와 같이 데이터를 관리하는 컴포넌트가 있고 데이터를 UI 컴포넌트에 전달하는 단순한 데이터 흐름은 이해하기도 쉽고 안정성이 높다.
> 4. **virtual DOM**
> 리액트는 렌더링 시 Virtual DOM, 즉 가상 DOM을 먼저 만든다. 그 후 직전 가상 DOM과 변경된 가상 DOM을 비교하여 실제 DOM에 변경이 있는 부분(하위 컴포넌트)만 적용하기 때문에 리소스 낭비를 줄일 수 있다.
> 5. **넓은 생태계를 구축하고 있다.**
> 리액트는 FaceBook에서 만든 언어이다. 그리고 많은 개발자들에게 각광받는 언어중 하나이므로, 많은 개발자들이 사용하고 있다. 그렇기 때문에 리액트를 사용하는 커뮤니티 시장은 클 수 밖에 없다. 개발자에게 있어 본인이 가진 기술의 커뮤니티가 클수록 개발에 유리하다고 생각한다.
- 리액트를 사용하면 어떠한 이점이 있는지
- 위와 동일할듯?
- SSR, CSR 차이
- https://velog.io/@namezin/CSR-SSR (참고)
- CSR은 Client Side Rendering 이고 SSR은 Server Side Rendering 이다. 즉, 이 둘은 화면을 어디서 최종적으로 만들어서 보여주느냐와 어떻게 개발되느냐에 따른 차이가 있다.
- CSR은 처음 웹 서버에 요청할 때 데이터가 없는 문서를 반환한다. 브라우저가 서버에서 HTML과 static 파일을 요청 후 로드되면 사용자와의 상호작용에 따라서 javascript를 동적으로 렌더링한다. 필요에 따라 데이터를 서버에 요청해서 받아와 렌더링한다.(REST API)
- SSR은 완전하게 만들어진 HTML 파일을 받아오고 렌더링한다. 웹 서버에 요청할 때마다 브라우저 새로고침이 일어나고 서버에 새로운 페이지에 대한 요청을 하는 방식이다. 초기 로딩 속도는 빠르지만 페이지가 바뀔 때마다 서버에 매번 요청(+새로고침)하기 때문에 트래픽, 서버 부하, UX가 떨어진다.
- 이 둘의 비교 사진

- 그렇다면 SPA는 CSR인가? => 아니다
- SPA는 서버로부터 처음에만 페이지를 받아오고 이후에는 동적으로 DOM을 구성하여 렌더링되는 화면을 바뀌게 한다.
- 바로 여기서 "동적으로 DOM을 구성하여 렌더링 되는 화면을 바뀌게 한다" 부분이 CSR이다. 다시 말하면 SPA는 처음에만 페이지를 서버에 요청하여 받아오고 이후에는 받아오지 않는다. 그 이유는 데이터가 수정되고 조회되게끔 하고자 CSR이란 방식을 채택한 것이다.
- 그렇다면 MPA( multiple page Application)는 동적이지 않은 페이지를 상황에 맞게 클라이언트에 뿌려주기 때문에 SSR 방식을 채택한 것이다. 즉, 이들은 비교 대상이 아니다. SPA, MPA는 페이지를 몇 개 쓰냐의 차이이고 CSR, SSR은 렌더링을 어디서 하냐의 차이이므로 이들은 비교 대상이 될 수 없다. 방식의 차이일뿐.
- AJAX는 어느 개념이지? (Asynchronous Javascript and XML )
- Javascript를 통해서 서버에 데이터를 비동기 방식으로 요청하는 것입니다.
- SPA란?
- 말 그대로, 페이지가 하나인 어플리케이션이다. 예전에는 어떤 웹App을 만들 때, 여러 페이지로 구성된 Web을 서버에서 리소스를 전달받아와서 렌더링했다. (ex A.html, B.html, C.html 등등..) 즉, 웹 어플리케이션 뷰를 서버에서 담당했다. 하지만 App의 규모가 커지고 사용자와의 상호 작용이 많아짐에 따라, 데이터 정보 전송 과부하로 인한 속도 저하 등 문제점이 생기게 되었다. 그래서 리액트는 뷰 렌더링을 서버가 아닌 웹 브러우저가 담당한다.
- SPA는 서버에서 제공되는 페이지가 한 개이다. 예전에는 페이지를 요청할 때마다 서버로 접속하여 받아왔기 때문에, 페이지가 매번 새로고침 되었지만 리액트는 로딩을 한 번 하고 나면 브라우저 내에서 나머지 페이지들을 정의하여 보여준다. 첫 번째 페이지를 받아온 후 다른 페이지로 이동할 때는 서버에 새로운 페이지를 요청하는 것이 아닌, 새 페이지에서 필요한 데이터만 받아와서 다른 종류의 뷰를 정의한다.
- 주소에 따라 다른 뷰를 보여주는 것을 라우팅(Routing)이라 한다. 리액트에서는 react-router 라이브러리로 클라이언트 사이드에서 이뤄지는 라우팅을 간단하게 해준다. 이를 사용하면, 페이지 주소를 변경했을 때 주소에 따라 다른 컴포넌트를 렌더링해준다. 또한, 주소 정보(파라미터, URL 쿼리 등)를 컴포넌트의 props에 전달하여 컴포넌트 단에서 주소 상태에 따라 다른 작업을 할 수 있게 해준다. => history API도 공부 ㄱㄱ
- 단점도 있다. 앱의 규모가 커지면 자바스크립트 파일 사이즈가 커진다는 것이 단점이다. 유저가 실제로 방문하지 않을 수도 있는 페이지와 관련된 렌더링 관련 스크립트도 불러온다. 이는 코드 스플릿팅으로 해결할 수 있지만 신입이 할만한 사이즈는 아니다. 리액트 라우터와 같이 브라우저 측에서 자바스크립트를 사용하여 라우트를 관리하는 것의 추가적인 단점은 자바스크립트가 실행될 때까지 페이지가 비어있기 때문에, 자바스크립트 파일이 아직 캐싱되지 않은 사용자는 아주 짧은 시간동안 정의되지 않은 흰 페이지가 나타날 수 도 있는 단점이 있다. 사실 이 또한, 물론 서버사이드 렌더링을 통하여 해결 할 수 있긴하다.
- virtual DOM이 무엇인지
- 참고 : https://velopert.com/3236
- 리액트는 SPA로 개발된다. 리액트는 뷰 렌더링을 웹 브라우저에서 담당하고, 웹 브라우저 안에는 DOM이 존재한다. 이 말은 복잡한 SPA에서는 DOM 조작이 많이 발생한다는 뜻이다. 즉, 변화를 적용하기 위해 브라우저가 많은 연산을 해야한다. 이렇게 되면 전체적인 프로세스가 비효율적이게 된다.
- 이 부분에서 중요한 것이 virtual DOM이다. 만약 뷰에 변화가 있다면, 그 변화는 Real DOM에 적용되기 전에 virtual DOM에 먼저 적용시킨다. 앞단에서 변화들을 다 수용하고 난 다음에 최종적인 결과를 Real DOM에 전달해주는 역할을 하는 것이 Virtual DOM이다.
- Virtual DOM은 real DOM과 동일한 속성을 갖고 있는 복사본이다. JSX 요소를 렌더링 할 때 모든 virtual DOM이 업데이트 된다. virtual DOM이 업데이트 되면 react는 virtual DOM이 업데이트 되기 직전에 찍은 virtual DOM 스냅 샷과 비교한다. 그 후 react는 어떤 객체가 바뀌었는지 알아낸다.
- 변한 객체들만 real DOM에 업데이트 한다. 그 후 real DOM의 변화된 요소들만 화면에서 변화를 일으킨다.
- 즉, virtual DOM의 목적은 브라우저 연산의 횟수를 줄이는 것이다.
- JSX이 무엇인지
- 클래스 컴포넌트와 함수형 컴포넌트의 차이점
- state와 props의 차이점
- 상태 관리는 어떻게 했는지
- 리액트 라우터에 대한 설명
- 리액트 훅에 대한 설명, 왜 사용하는지
- hoc에 대한 설명
- hoc는 동일한 로직(코드)으로 구현된 컴포넌트 대해서 중복 사용을 방지하기 위한 컴포넌트입니다. 저희 프로젝트에서 hoc는 로그인 인증과 관련해서 사용하였습니다. 즉, hoc 컴포넌트에서 사용자가 어세스 토큰을 가지고 있냐 없냐를 판단하여 로그인 화면 혹은 로그인 성공 후의 화면으로 라우터 처리함에 목적을 두고 있습니다.
- 리액트 라이프 사이클에 대한 설명
- 리액트 함수형 컴포넌트와 클래스 컴포넌트 차이에 대한 설명
- webRTC
- webRTC가 뭔지?
- WebRTC는 웹 브라우저 간에 플러그인의 도움 없이 서로 통신할 수 있도록 설계된 API이다. 음성통화, 영상 통화, P2P 파일 공유등으로 활용될 수 있다.
- `getUserMedia` : 오디오와 비디오 미디어를 가져온다. (예: 장치의 카메라와 마이크), 음성과 영상 가져오기
- `RTCPeerConnection` : 피어 간 오디오, 비디오 통신을 활성화한다. 두 피어 간의 효율적인 데이터 스트리밍을 처리하는데 사용된다. (유저간 음성과 영상 전송하기 )
- `RTCDataChannel` : 피어 간 양방향 임의 데이터 통신을 허용한다. 웹소켓과 동일한 API를 사용하며 매우 낮은 레이턴시를 보인다. (유저간 데이터 전송하기)
- WebRTC 연결 흐름 이미지

- WebRTC를 이해하기 위해 필요한 개념들
- 1. NAT(Network Address Translation, 네트워크 주소 변환)
- 각 기기에도 자신만의 이름이 있다. 그것이 바로 IP이고 이 IP는 고정 IP, 유동 IP로 나뉘어서 실제 고유의 값일 수도 있고 아닐 수도 있다. 더 나아가서는 회사망/내부망(LAN)은 Private IP이기 때문에 다른 네트워크 (구글 홈페이지 접속, 일반적인 웹 사이트 접속) 등 에서는 통용되지 않는다. 그렇기 때문에, 우리가 통산적인 네트워크에서 데이터를 주고 받기 위해서는 Public IP가 필요하다.
- **NAT**은 Private IP를 Public IP로 1대1 대응시켜 변환하는 장치를 말한다. 또한, IP주소를 재기록하면서 라우터를 통해 네트워크 트래픽을 주고받는 매커니즘이다.
- WebRTC 통신은 P2P방식으로 서로 데이터를 주고 받아야 하기 때문에 보내고 받는 Peer의 정보(Public IP)를 알고 있어야 한다. 하지만 NAT 환경은 WebRTC 통신에 많은 문제를 야기시킨다. STUN, TURN 서버를 사용해서 문제를 해결할 수 있다.
- 2. ICE (Interactive Connectivity Establishment)
- **ICE**는 두 단말이 서로 통신할 수 있는 최적의 경로를 찾을 수 있도록 도와주는 프레임워크이다.(즉, 클라이언트의 모든 통신 가능한 주소를 식별하는 것이다.)
- STUN, TURN 서버를 사용하여 최적의 경로를 찾을 수 있다.
- 모든 단말은 각자의 환경(학교 내부망, 회사 내부망, 집의 네트워크 등)이 다양하기에 Peer A에서 Peer B까지 단순하게 연결되지 않는다. 방화벽이 존재하는 환경에서는 방화벽을 통과해야하고 단말의 퍼블릭 IP가 없다면 유일한 주소값을 할당해야하고 라우터가 Peer간의 직접연결을 허용하지 않을 때는 데이터를 릴레이해야한다.
- **ICE** 프로세스를 사용하면 NAT가 통신을 위해 필요한 모든 포트를 열어두고 엔드 포인트 모두 다 연결 할 수 있는 IP주소, 포트에 대한 완전한 정보를 갖게된다. 결국 요청하는 클라이언트와 미디어 서버 사이의 연결을 통해 미디어(비디오, 음성)등을 주고 받을 수 있는 것이다.
- ICE는 혼자서 작동하지 않으며 STUN과 TURN 서버를 사용해야한다.
- ICE 후보
- 1. 본인의 Private IP
- 2. STUN 서버로 얻은 본인의 Public IP
- 3. TURN 서버로 얻은 본인의 Public IP
- 3. STUN (Session Traversal Utilities for NAT)
- 공개 주소를 발견하거나 peer간의 직접 연결을 막는 등 라우터의 제한을 결정하며 ICE를 보완하는 프로토콜이다.
- 즉, STUN 서버는 해당 Peer의 공인 IP 주소를 보내는 역할을 한다.
- STUN은 두 엔트 포인트 간의 연결을 확인하고 NAT 바인딩을 유지하기 위한 연결 유지 프로토콜로도 사용할 수 있다.
- 4. TURN
- STUN의 확장으로 NAT 환경에서 릴레이하여 통신을 하게 된다.
- NAT 보안 정책이 너무 엄격하거나 NAT 순회를 하기 위해 필요한 NAT 바인딩을 성공적으로 생성할 수 없는 경우에 TURN을 사용한다.
- TURN 서버는 인터넷망에 위치하고 각 Peer(단말)들이 사설망(Private IP) 안에서 통신한다. 각 Peer들이 직접 통신하는 것이 아니라 릴레이 역할을 하는 TURN 서버를 사용하여 경유한다.
- 즉, TURN 서버는 이러한 릴레이로부터 IP주소와 포트를 클라이언트가 취득할 수 있는 릴레이 주소를 할당한다.
- STUN vs TURN 이미지

- 왜 webRTC를 사용했는지?
- 우리 LiveMD 프로젝트에서는 화상 채팅 서비스를 구현하는 것이 필수적이다. 이를 구현하기 위한 방법을 찾아보던 중 WebRTC라는 기술을 발견하게되었다. 이미 WebRTC를 활용한 서비스의 커뮤니티 시장도 커서 초보자도 쉽게 접근할 수 있다는 생각을 했다. 또한, WebRTC는 JavaScript API를 제공하고 있고 우리는 JS에 익숙하여 webRTC를 사용하는 것이 프로젝트에 훨씬 생산적으로 개발할 수 있을것 같아 이 기술을 사용하였다.
- 또한, 별다른 플러그인 없이 쉽게 개발 환경을 구축할 수 있다는 것도 장점으로 다가왔다.
- signaling server가 뭔지?
- signaling server는 사용자 간의 WebRTC를 위한 P2P 통신을 할 때 모르는 사용자를 연결시켜주는 역할을 한다.(상대방이 누구인지 파악하는 과정)
- WebRTC의 과정을 보면
- 1. 브라우저가 사용자의 장치에 접근한다.
- 2. Signaling Server를 통해 상대방의 정보를 얻는다.
- 3. Peer to Peer Connection을 통해 통신한다.
- 아래는 과정 이미지이다.

- signaling server의 동작 원리
- 1. 통신을 원하는 사용자는 상대 사용자에게 Signaling Server를 통해 자신의 정보를 제공한다. (ICE 사용 가능)
- 2. 상대 사용자는 그 정보들에 대해 자신의 정보를 담아 답장한다. (ICE 사용 가능)
- ICE란? => Interactive Connectivity Establishment(ICE)는 브라우저가 Peer를 통한 연결이 가능하도록 하게 해주는 프레임워크이다.
- Signaling Server는 WebRTC와는 별개로 구축해야한다. WebSocket을 통해 구축할 수 있는거 같다. (LiveMD에서는 WebSocket으로 구현한것 같기도? 더 공부해야할듯 ㅇㅇ )
- simple - peer 라이브러리
- `index.js` 파일안에서 RTCPeerConnection 기능 또한 구현해주었다.
- 우리가 room.js 클라이언트에서 구현한 것은 simple-peer 라이브러리와 socket.io-client 라이브러리를 사용하여 그룹 화상채팅을 개발함. server.js에서는 signaling server를 구현하였다.
- simple - peer 라이브러리가 대신 해준 것은 RTCPeerConnection API 기능과 STUN 서버 등등 많은 일을 해줌.
- Mesh 구조 (그룹화상채팅을 구현했는데, 어떤 식으로 설계를 했는지? )
- 어떤 노드든지 다른 노드와 연결이 되어 있기 때문에 중앙 서버의 필요성이 없어진다.
- 노드가 단 하나라도 살아있다면, 전체 네트워크가 항상 작동된다.
- '살아있는' 노드들과 통신을 유지하는 과정에서 무지막지한 자원소모가 발생한다.
- 메시 네트워크를 활용하여 P2P를 구현할 수 있다.
- Socket.io (웹 소켓 과의 차이점)
- socket.io란 Web Socket을 기반으로 클라이언트와 서버의 양방향 통신을 가능하게 해주는 라이브러리이다. 방 개념을 이용해 일부 클라이언트에게만 데이터를 전송하는 브로드캐스팅이 가능하다.
- WebSocket은 양방향 소통을 위한 프로토콜이다. HTML5 웹 표준 기술이다.
- 서버에서 연결된 소켓(사용자)들을 세밀하게 관리해야하는 서비스인 경우에는 Broadcasting 기능이 있는 socket.io를 쓰는게 좋다.
- 반면 가상화폐 거래소 같이 데이터 전송이 많은 경우에는 빠르고 비용이 적은 표준 WebSocket을 이용하는게 바람직하다.
- SSL(Secure Sockets Layer) (배포도 성공했는지? -> SSL은 어떤 식으로 적용했는지?)
- 암호화된 링크를 수립하기 위한 표준 보안 기술이다.
- SSL의 장점은 사용자 데이터를 안전하게 서버에 보낼 수 있고 이 사이트가 신뢰가 가능한 사이트인지 판단할 수 있다.
- 대칭키
- 동일한 키로 암호화와 복호화를 할 수 있는 기법이다.
- 클라와 서버간 통신하기 위해서는 반드시 대칭키를 알고 있어야한다. (키 배송 문제)
- 중간에 대칭키가 유출된다면 키를 획득한 공격자는 암호화된 데이터를 해독할 수 있기 때문에 HTTPS의 필요성이 사라진다.
- 이러한 단점을 보완하기 위해 나온 방식이 공개키 기법이다.
- 공개키 (Public key)
- 공개키 방식은 대칭키 방식과 다르게 2개의 키를 가지고 시작한다.
- 그 중 하나가 공개키이고 나머지 키를 비밀키(Private Key,개인키)라 부른다.
- 비밀키는 본인만 소유하고 공개키는 타인에게 제공한다.
- 작동 원리
1. 서버에서는 비대칭 공개 키의 복사본을 브라우저로 전송한다.
2. 브라우저에서는 대칭 세션 키를 만들어 서버의 비대칭 공개 키로 암호화한 다음 이를 서버로 전송한다.
3. 서버는 대칭 세션 키를 얻기 위해 비대칭 비공개 키를 사용하여 암호화된 세션 키를 해독한다.
4. 이제 서버와 브라우저에서 대칭 세션 키를 사용하여 전송된 모든 데이터를 암호화 및 해독한다. 브라우저와 서버에만 대칭 세션 키에 대한 정보가 있으며 세션 키는 해당하는 특정 세션에만 사용되기 때문에 채널 보안이 보장된다. 다음날 브라우저가 동일한 서버에 다시 연결되면 새로운 새션 키가 생성된다.
- 
- CORS
- P2P, SFU, MCU
- https://6987.tistory.com/entry/WebRTC-%EB%AF%B8%EB%94%94%EC%96%B4-%EC%97%B0%EA%B2%B0-%EB%B0%A9%EC%8B%9D-MCU-SFU-P2P
- P2P(peer to peer) : 우리 LiveMD는 P2P 방식의 mesh 네트워크 구조를 이용하여 그룹 화상채팅을 구현하였다. P2P는 서버 비용이 들지 않는다. 그 이유는 웹 브라우저 상에서 모든 사용자의 정보를 받기 때문이다. 하지만 사용자 PC에 많은 부하를 준다.
- SFU (selective forwarding unit) : 요즘 대세다. ZOOM이 이 방식을 쓴다. 가운데에 서버를 두고 각 사용자는 그 서버에 본인의 음성과 영상 데이터를 인코딩하여 서버에 요청한다. 그리고 서버는 본인을 제외한 사용자의 데이터를 인코딩된 상태로 서버에 보내고 서버는 인코딩된 데이터를 본인에게 보낸다. 하지만 본인은 본인을 제외한 사용자만큼의 영상과 음성을 디코딩해야한다. 그래서 PC 자원 사용량이 많아지면서 회의가 어려워질 수 있지만 SVC 코덱이나 Simulcast를 사용하여 해결할 수 있다.
- MCU (multipoint control unit) : 서버에 부하가 많이 걸리고 실시간 통신이 어려워 질 수 있다. 클라이언트는 인코딩된 데이터를 서버에 보내면 서버에서 각 사용자의 데이터를 하나로 합쳐(Mixing) 인코딩된 데이터를 각 사용자에게 내려주는 구조이다. 그래서 클라이언트는 Mixing된 데이터 하나만 디코딩 하면 되기 때문에 부하를 덜어줄 수 있다.
#### WebRTC 고민
1. signaling server, STUN, TURN 서버의 관계
- STUN, TURN 서버는 사용자가 본인의 private IP를 Public IP로 변환하여 P2P 통신을 하게끔 해주는 서버이다. 즉, NAT를 도와주는 서버이다.
- 하지만 시그널링 서버는 사용자가 STUN, TURN 서버를 통해 변환받은 public IP를 활용하여 사용자 정보를 제공하는 서버이다.
- 제공받은 사용자 정보를 통해 통신할 상대방을 찾게 되고 자신의 정보를 저장하여 P2P 통신을 하는 사용자들간의 정보를 파악하는 역할을 한다.
2. 우리가 구현한 node.js 서버는 signaling server인가?
- 시그널링 서버를 구현한 것이 맞다.
- 그러면 STUN, TURN 서버는 구현할 필요가 없나? => 아니다! STUN 서버는 필수로 구현해야 한다. 우리는 simple-peer 라이브러리로 STUN 서버 이슈를 해결하였다.
3. NAT를 구현할 수 있는 기술이 Stun, Turn 서버인가?
- NAT를 구현할 수 있는 기술이 STUN, TURN이 아니라 NAT의 문제를 해결할 수 있는것이 STUN, TURN 서버이다.
- NAT는 IPv4의 이슈인 Public IP 부족 현상을 해결하기 위해 나온 것이다. 하지만 WebRTC 상에서는 개인의 Public IP를 갖고 통신을 해야하는데 NAT는 사용자에게 Private IP를 제공하기 때문에 개인의 Public IP를 알기 위해서는 STUN 서버를 사용하여야 한다.
4. WebRTC를 구현하기 위해서는 STUN 서버가 필수적인가?
- 1. 우리가 모르는 사이에 구현한 video 서버에서 STUN 서버 기능이 구현됐다. (STUN 필수) => 필수긴 하지만 우린 Simple - peer 라이브러리로 구글 STUN 서버를 이용하였다.
- 2. NAT가 구축된 환경에서 STUN 서버를 구현하지 않고 WebRTC를 구현한 것이다. (STUN 필수x) // 이거 틀림 STUN 필수
- stun <-> (A) NAT --- NAT (B) <-> stun
- **결론!!!** STUN 서버는 필수적이다.!! 우리는 STUN 서버를 simple-peer 라이브러리로 구축하였다.
5. 공유기에 STUN 서버 기능이 있는 것인가?
- 아니다!!! STUN서버는 공유기에 있는 기능이 아니다. 공유기는 NAT라고 간단히 생각할 수 있다. NAT에서 발생하는 이슈때문에 홀 펀칭이 필요하다. 이 홀펀칭을 하는 기술중에 하나가 STUN 서버이다. 우리 LiveMD에서는 simple-peer 라이브러리에 내장되어 있는 STUN 서버(구글 STUN server)를 사용하여 홀펀칭을 해결하였다.
- nginx
- 왜 nginx를 사용했는지
- nginx가 무엇인지
- nginx reverse proxy가 무엇인지
- 왜 reverse proxy를 사용했는지
- 어떻게 ssl을 적용했는지
- HTTPS
- 왜 https를 적용했는지
- http와 https 차이는 무엇인지
- https 동작 방식은 어떻게 되는지
- git-flow
- git-flow에 대해 설명하시오
- 배포
- 배포는 어떤 방식으로 진행했는지
- jira
- 애자일 스크럼
- jwt
- 로그인은 어떤 방식으로 구현했는지
- react-google-login 라이브러리를 이용하여 구글 소셜 로그인을 구현하였습니다. 로그인을 성공하게 되면 사용자 정보를 받고 auth 서버의 signin api를 호출하여 사용자 정보를 DB에 저장합니다. 그리고 response에 jwt access token을 받아 local storage에 저장하고 refresh token을 캐시로 저장합니다. 이후 refresh token의 만료 기한이 다할 때까지 access token을 이용하여 로그인할 수 있습니다. 그리고 로그인한 회원과 비로그인 회원이 진입하는 페이지가 다르도록 HOC 함수를 활용하여 인증 체크를 하였습니다.
- 왜 jwt를 사용했는지
- 쿠키, 세션 jwt에 대해 설명해보시오
- oauth2
- 왜 oauth2를 사용했는지
- oauth2에 대해 설명해보시오
- 환경 변수
- db password나 api key 같은 정보 어떻게 처리했는지
- styled component
- bootstrap
- atomic design
- yjs
- 실시간 에디터 편집 관련 이론
- websocket
- api 명세
- api 명세는 어떻게 관리했는지
- rest api
- rest api에 대해 설명해보시오
## 곽병선
1. MSA 기반 프로젝트 - 서비스 별로 서버를 나누었다.
- MSA가 뭔지?
- 왜 프론트엔드는 나누지 않았는지?
- 서비스를 나눈 기준은 뭔지?
- 서버구현을 두 개의 언어로 사용했던데 그 이유는 뭔지?
2. 애자일 개방방법론을 적용하여 프로젝트를 적용하였다.
- 애자일 개발 방법론이 뭔지?
- 그래서 어떤 식으로 프로젝트를 진행한건지 ?
- Jira software 사용하였다.(Scrum, sprint)
3. React 폴더 구조 설정 기준을 말해보아라.
- 어디를 참고해봤단 말 해도 괜찮을지 고민 ㄱㄱ
4. LiveMD 프로젝트에서 맡은 역할이 무엇인지?
- 주로 맡은 역할은 프론트 엔드였지만 교육과정에서 배운 내용을 최대한 활용하기 위해 모든 부문에 관여하였다.
5. 왜 React.js를 사용하였는가?
- 국비 지원 교육에서 배운 프레임 워크가 React.js이다. 배운 내용을 최대한 활용하고 싶었다.
6. 상태 관리 라이브러리를 MobX-state-tree로 정한 이유가 무엇인지?
- 원래 기존에는 MobX 라이브러리를 적용하고자 하였고, 교육 과정에서 배운 내용도 MobX였다. 하지만 앱이 커질 경우, 관리해야 하는 state들이 늘어나고 그에 따라 여러 개의 stores로 분리되는 것을 예방하고자 MobX-state-tree로 결정하였습니다.
7. 인코딩 vs 디코딩
### 가비아 면접 질문 정리 (출처 : 잡플래닛)
1. 진행했던 프로젝트에 대해서 설명하고 느꼈던 점들이나 개선점 등에 대해서 자유롭게 설명해보아라 (2018.02)
- 프로젝트 설명 후 사용했던 기술 스택, 관련된 내용에 추가 질문 들어옴 분위기는 편안한 편.
2. 프로젝트 관련하여 orm 질문 (2020.12)
3. 프로젝트 관련하여 테스트 코드 관련 경험 질문 (2020.12)
4. 지원한 분야에 대한 기술적인 질문 ,
5. 1분 자기소개 (이건 필수로 있는듯), 왜 이 분야에 지원하게 됐는지 , 프로젝트에서 수행한 역할 질문(2020.12)
6. React 라이프 사이클, sass 사용법, 미디어 쿼리 사용법, TS 사용시 이점 (2020.11)
7. DOM과 가상 DOM의 차이는? (2020.12)
8. 가비아 회사에 대해서 어느정도 알고 있고 알고 있는 서비스 하나 질문 (필수)
## 면접 예상 질문 프로세스
1. 1분 자기소개
- 곽병선 :
- 조성원 :
2. 자소서를 보니까 프로젝트 경험이 있는데, 혹시 기억에 남는 프로젝트가 있나?
- 작년에 파스타 공모전에 출전하여 수상을 한 프로젝트가 있습니다.
- 파스타 공모전은
-
(1) 1분 자기소개 -> (2) 프로젝트에 대한 설명과 자기가 어떤 부분에 대해 기여햇는지 -> 꼬리질문
-> 겪엇던 어려움과 그 어려움을 어떻게 해결했는지
(2)
프로젝트 주제는 실시간 소통과 마크다운 언어를 활용한 문서 공동 편집 서비스입니다. 코로나 시대에 온라인 협업이 증가하는데 실시간 소통과 공동 문서 편집 툴에 대한 번거로움을 해소하고자 기획하였습니다.
저는 이 프로젝트에서 프론트 엔드를 담당하였고, 프레임 워크로는 리액트를 사용하였고, 프로젝트의 모든 부문에 관여하였지만, 주로 맡은 기능은 실시간 소통의 그룹 화상 채팅 기능( 혹은 문서 공동 편집 기능)입니다.
### 공부 순서
MSA, 서버 이중화 => 고도화
WebRTC (NginX, reverse Proxy, SSL, Socket.io, simple-peer, mesh 네트워크)
WebRTC 코드 보면서 NginX쪽 location 코드 확인하면서 NginX, reverse Proxy 확인
JWT, OAuth2, 토큰과 세션 인증
CORS
#### MSA 고민
#### server.js (webRTC)
```javascript=
require("dotenv").config();
/* 설치한 express 모듈 불러오기 */
const express = require("express");
/* Node.js 기본 내장 모듈 불러오기 */
const http = require("http");
const cors = require('cors');
/* express 객체 생성 */
const app = express();
/* express http 서버 생성 */
const server = http.createServer(app);
/* 설치한 socket.io 모듈 불러오기 */
const socket = require("socket.io");
/* 생성된 서버를 socket.io에 바인딩 */
const io = socket(server);
const users = {}; //사용자 객체
const socketToRoom = {};
// on()은 소켓에서 해당 이벤트를 받으면 콜백함수가 실행됨
// connection이라는 이벤트가 발생할 경우 콜백함수가 실행된다.
// on은 수신, emit은 전송이라고 이해하면 된다.
app.use(cors());
io.on("connection", (socket) => {
socket.on("join room", (roomID, username) => {
console.log("join room");
const user = {
socketID: socket.id,
username: username
};
if (users[roomID]) {
const length = users[roomID].length;
if (length === 6) {
socket.emit("room full");
return;
}
users[roomID].push(user);
} else {
users[roomID] = [user];
}
// socket.id로 사용자가 현재 어느 방에 있는지 알아내기 위함 ?
socketToRoom[socket.id] = roomID;
const usersInThisRoom = users[roomID].filter(({ socketID }) => socketID !== socket.id);
console.log(usersInThisRoom);
//모든 소켓에게 전송
socket.emit("all users", usersInThisRoom);
});
// sending signal는 시그널링 서버에 본인을 제외한 사용자 정보를 시그널링 서버에 보낸다
socket.on("sending signal", (payload) => {
console.log("sending signal");
io.to(payload.userToSignal).emit("user joined", {
signal: payload.signal,
callerID: payload.callerID,
username: payload.username,
});
});
// 내가 방에 참석한 이후에 들어온 사용자의 대한 정보를 다시 signaling server에 보낸다.
socket.on("returning signal", (payload) => {
console.log("returning signal");
io.to(payload.callerID).emit("receiving returned signal", {
signal: payload.signal,
id: socket.id,
});
});
socket.on("disconnect", () => {
const roomID = socketToRoom[socket.id];
let room = users[roomID];
if (room) {
room = room.filter(({ socketID }) => socketID !== socket.id);
users[roomID] = room;
}
socket.broadcast.emit('user left', socket.id);
});
});
/* 서버를 8000 포트로 listen*/
server.listen(process.env.PORT || 8002, () =>
console.log("video server is running on port 8002")
);
```
#### Client (webRTC)
```javascript=
import React, { useEffect, useRef, useState } from 'react';
import io from 'socket.io-client';
import Peer from 'simple-peer';
import S from './style';
import { IconButton } from '@material-ui/core';
import {
VideocamRounded,
VideocamOffRounded,
MicRounded,
MicOffRounded,
} from '@material-ui/icons';
import { VIDEO_API } from '@/utils/APIconfig';
const Video = props => {
const ref = useRef();
useEffect(() => {
props.peer.on('stream', stream => {
ref.current.srcObject = stream;
});
}, []);
// 상대방 비디오
return <S.StyledVideo playsInline autoPlay ref={ref} />;
};
const Room = ({ roomID, username }) => {
const [peers, setPeers] = useState([]);
const socketRef = useRef();
const userVideo = useRef(null);
const peersRef = useRef([]);
const [isMuted, setIsMuted] = useState(true);
const [isPause, setIsPause] = useState(true);
const [disabled, setDisabled] = useState(false);
useEffect(() => {
socketRef.current = io.connect(VIDEO_API);
navigator.mediaDevices
.getUserMedia({ video: true, audio: true })
.then(stream => {
// 우리 자신의 스트림에서 얻은 스트림을 가져온다.
if (!userVideo.current) {
//userVideo없으면 아무것도 하지 않기 왜냐면 없을수도 있는데 무조건 넘겨주면 안되니까
return;
}
userVideo.current.srcObject = stream;
userVideo.current.srcObject.getAudioTracks()[0].enabled = false; // 첫 입장 시 오디오는 off.
userVideo.current.srcObject.getVideoTracks()[0].enabled = false;
socketRef.current.emit('join room', roomID, username); // ref는 우리가 방에 합류했다는 이벤트를 내보낸다.
socketRef.current.on('all users', users => {
const peers = []; // 방금 첫 사용자가 들어왔기 때문에 peers는 없는것.
users.forEach(({ socketID, username }) => {
// 서버에서 사용자들을 가져온다
const peer = createPeer(socketID, socketRef.current.id, stream); // 사용자ID(누가 전화했는지 알 수 있음)
peersRef.current.push({
// peerID 전달,
peerID: socketID, //방금 피어를 만든 사람의 소켓 ID
username: username,
peer,
});
//peers에도 ID, peer 전달
peers.push({
peerID: socketID,
username: username,
peer,
});
});
setPeers(peers);
});
socketRef.current.on('user joined', payload => {
const item = peersRef.current.find(
p => p.peerID === payload.callerID,
);
if (!item) {
const peer = addPeer(payload.signal, payload.callerID, stream); //callerID : 발신자
const peerObj = {
peerID: payload.callerID,
username: payload.username,
peer,
};
peersRef.current.push(peerObj);
setPeers(users => [...users, peerObj]);
}
});
socketRef.current.on('receiving returned signal', payload => {
const item = peersRef.current.find(p => p.peerID === payload.id);
item.peer.signal(payload.signal);
});
//user가 나갔을 때, disconnect
socketRef.current.on('user left', id => {
const peerObj = peersRef.current.find(p => p.peerID === id);
if (peerObj) {
peerObj.peer.destroy();
}
const peers = peersRef.current.filter(p => p.peerID !== id);
peersRef.current = peers.slice();
setPeers(peers);
});
})
.catch(() => {
setDisabled(true);
});
return () => {
socketRef.current.destroy();
};
}, []); //userVideo가 업데이트 되면 useEffect 실행
// 빈 배열로 해놓으면 가장 처음 렌더링 될 때만 실행되고 업데이트 할 경우에는 실행 할 필요가 없는 경우엔 함수의 두 번째 파라미터로 비어있는 배열을 넣어주면 된다.
function createPeer(userToSignal, callerID, stream) {
const peer = new Peer({
initiator: true, //요청자 이므로 true
trickle: false, // 이건 유투버도 뭔지 모른다함. 근데 대부분 이것을 false로 설정한다고 했음
stream, //영상, 음성에 대한 액세스를 요청한 스트림
});
// 본인을 제외한 사용자에 대한 정보를 sending signal(서버에 있음)에 보낸다.
peer.on('signal', signal => {
socketRef.current.emit('sending signal', {
userToSignal,
callerID,
signal,
username,
});
});
return peer;
}
function addPeer(incomingSignal, callerID, stream) {
const peer = new Peer({
initiator: false, // 요청자가 아니므로 false
trickle: false,
stream,
});
peer.on('signal', signal => {
socketRef.current.emit('returning signal', { signal, callerID });
});
peer.signal(incomingSignal);
return peer;
}
//Mic on,off 기능
const micOnAndOff = () => {
if (isMuted) {
userVideo.current.srcObject.getAudioTracks()[0].enabled = true;
setIsMuted(false);
} else {
userVideo.current.srcObject.getAudioTracks()[0].enabled = false;
setIsMuted(true);
}
};
//Video on,off 기능
const videoOnAndOff = () => {
if (!userVideo.current) return;
if (!isPause) {
userVideo.current.srcObject.getVideoTracks()[0].enabled = false;
setIsPause(true);
} else {
userVideo.current.srcObject.getVideoTracks()[0].enabled = true;
setIsPause(false);
}
};
return (
<S.RoomContainer>
<S.VideoControlBtnDiv>
<IconButton
onClick={videoOnAndOff}
aria-label="VideocamRoundedIcon"
size="medium"
disabled={disabled}
disableRipple
disableFocusRipple
>
{isPause ? (
<VideocamOffRounded
fontSize="large"
// color={disabled ? "disabled" : "secondary"}
// color={disabled ? "disabled" : "secondary"}
/>
) : (
<VideocamRounded fontSize="large" style={{ color: '#00796b' }} />
)}
</IconButton>
<IconButton
onClick={micOnAndOff}
aria-label="MicRoundedIcon"
size="medium"
disabled={disabled}
disableRipple
disableFocusRipple
>
{isMuted ? (
<MicOffRounded
fontSize="large"
// color={disabled ? "disabled" : "secondary"}
/>
) : (
<MicRounded fontSize="large" style={{ color: '#00796b' }} />
)}
</IconButton>
</S.VideoControlBtnDiv>
<S.VideoContent>
<S.VideoWrapper>
<S.StyledVideo muted ref={userVideo} autoPlay playsInline />
</S.VideoWrapper>
<S.UserName>{username}</S.UserName>
{peers.map(peer => {
return (
<div key={peer.peerID}>
<S.VideoWrapper>
<Video peer={peer.peer} />
</S.VideoWrapper>
<S.UserName>{peer.username}</S.UserName>
</div>
);
})}
</S.VideoContent>
</S.RoomContainer>
);
};
export default Room;
```