Earn this, Earn it.
[TIL] 주간 회고 (4월 둘째 주) 본문
요즘 들어 블로그 포스팅을 올릴 엄두가 안 나고 있다.
매주 주말 주간 회고를 작성하긴 하는데, 밤에 작성해서 그런지 감성적인 글이 되어버려서 자꾸만 사적인 공간에 보관되고 있다.
퀄리티는 조금 떨어지더라도 차라리 매일 배운 내용을 간단히 정리하는 글을 포스팅하며 정리해야겠다.
Agora SDK로 화면 공유 구현하기
아무래도 협업 툴에서는 화면 공유가 필요하다. 말로 설명하는 것보다 직접 보여주면서 설명하는 것이 여러모로 효율적이기 때문이다.
이전 프로젝트(타닥타닥)에서 화면 공유 로직을 구성해본 바 있다. 그 당시에도 화면 공유는 생각해야 할 것들이 조금 있었는데 이번 프로젝트에서는 그 때의 경험을 바탕으로 다양한 상태 변화에 제대로 대응하기 위해 신경을 썼다.
특히 까다로웠던 점은 상태 관리이다.
예시를 들자면, 화면 공유 버튼을 누르면 바로 화면 공유가 시작되는 것이 아니다. 사용자는 자신이 공유할 화면을 선택하여 확인을 누를 수 있고 취소를 누를 수도 있다. 또한 화면 공유를 취소하려면 기존의 버튼을 누를 수도 있고 공유 중지를 클릭할 수도 있다. 이는 코드 상에서 모든 상태를 통제하는 것이 아닌 브라우저에서 지원하는 화면 공유를 이용하기 때문이다. 말로만 설명하면 이해하기 어려우니 바로 코드로 보자.
사용자가 화면 공유 중에 '공유 중지' 버튼을 눌러 로컬에서 화면 공유를 껐다고 가정해보자.
이를 따로 통제해주지 않는다면 상대방은 일정 시간 멈춘 화면을 보고 있어야 한다. (소켓을 기반으로 통신을 하기 때문에 일정시간 로컬에서 응답이 없으면 연결이 끊어지므로)
하지만 보다 반응성 좋게 처리해주는 것이 우리의 일 아닌가!
따라서 나는 아고라 SDK가 지원해주는 track-ended라는 이벤트를 이용하기로 하였다.
이는, 어떤 이유로든 공유되는 비디오트랙이 끝나면 콜백을 실행시켜주는 이벤트이다.
$screenTrack.on('track-ended', async () => await screenShareUnpulish());
여기서 화면 공유를 닫을 때 필수적으로 고려해야할 점은,
로컬에서 해당 비디오트랙을 close하는 것 뿐 아니라 서버에 publish되고 있는 현재 트랙의 구독도 취소해야된다는 것이다. close는 로컬의 비디오 공유 중지를 트리거하기 위함이고 unpublish는 상대방에게 알리기 위함이다.
const screenShareUnpulish = async () => {
await $client.unpublish($screenTrack);
$screenTrack.close();
screenTrack.set(null);
};
그래야 다른 클라이언트에서 unpublish 이벤트를 받게 되고 해당 유저의 비디오트랙이 끊어진 것을 감지할 수 있기 때문이다. 그리고 그 이후 다른 동작을 실행하면 된다.
$client.on('user-published', async (user, mediaType) => {
await $client.subscribe(user, mediaType);
console.log('subscribe success');
if (mediaType === 'audio') {
const remoteAudioTrack = user.audioTrack;
const userId = decodeNickname(user.uid);
remoteAudioTracks.update((obj) => {
obj[userId] = remoteAudioTrack;
return obj;
});
remoteAudioTrack.play();
}
if (mediaType === 'video') {
const remoteVideoTrack = user.videoTrack;
const userId = decodeNickname(user.uid);
const remotePlayerContainer = document.querySelector('#screen-share-div');
screenDescription.set(userId);
remoteVideoTrack.play(remotePlayerContainer);
}
// 여기서 unpublished 이벤트를 받게 된다.
$client.on('user-unpublished', async (user, mediaType) => {
remoteAudioTracks.update((obj) => {
delete obj[decodeNickname(user.uid)];
return obj;
});
await $client.unsubscribe(user);
또한 마지막으로 해당 컴포넌트에서 벗어났을 때 (뒤로가기, 브라우저 닫기 등) unpublish를 실행시켜주기 위해 onMount의 클린업 함수를 이용한다.
// 화살표함수라 중괄호를 생략할 수도 있지만 가독성을 위해 남겨두었다.
onMount(() => {
return () => screenShareUnpulish();
});
4월5일. 문제가 생겼다.
비디오 채널을 unpublish할 때의 이벤트를 음성 채널과 구별하지 않아서
화면 공유를 껐을 때 그 사람의 음성도 unpublish돼서 음성이 들리지 않는 이슈가 생겼다.
따라서 비디오 채널을 구분하여줬다.
$client.on('user-unpublished', async (user, mediaType) => {
if (mediaType === 'audio') {
remoteAudioTracks.update((obj) => {
delete obj[decodeNickname(user.uid)];
return obj;
});
await $client.unsubscribe(user);
}
else if (mediaType === 'video' && $screenDescription === decodeNickname(user.uid)) {
screenDescription.set('');
}
});
하지만 여기서 끝이 아니다.
누군가 화면 공유를 하고 있는데 다른 사람도 화면 공유를 하려고 할 때도 처리해줘야한다.
이 부분에 대해서는 추가로 논의가 필요한 부분이니 조금 찝찝하지만 다음 주에 고민해보자.
SVG끼리의 충돌 검사를 어떻게 구현할까? 더 좋은 방법이 있을까?
내가 SVG끼리의 충돌 검사를 하려는 이유는 간단하다.
캐릭터 끼리의 충돌과 캐릭터와 맵에 있는 오브젝트 간의 충돌 때문이다.
캐릭터간 마주쳤을 때 어떤 인터랙션을 넣고 싶기도 하거니와
캐릭터가 맵에서 어떤 오브젝트를 만났을 때 생기는 인터랙션(바다에서는 헤엄치고, 땅에서는 걷는 등)을 구현하고 싶었다.
4년 전 업데이트가 끊긴 라이브러리긴 한데, svg-intersections라는 라이브러리가 있다.
이는 두 개의 도형이 서로 만나는지 판단해주는 알고리즘이 포함되어 있는 라이브러리이다.
직접 사용해보니 왜 안 쓰이는지 알 것 같았다.
직관적으로 이해는 쉬우나, svg 코드를 직접 불러와 사용하기보다는 값을 입력해줘야 하기 때문에 한계가 극명했다.
path-intersection이라는 라이브러리도 있었으나 여전히 svg를 다루기에는 불편하였다.
여기서 실행시간이 늘어지면 프레임이 밀리는 현상이 발생할 수 있다.
최대한 간단하고 최소의 시간으로 처리할 수 있는 로직이 필요했다.
처음에는 위에서 얘기한 svg-intersections를 써서 svg의 path간 교차 여부에 따라 판단하려 했다.
문제는 어떤 영역을 구분하기 어렵다는 것이다.
위의 그림처럼 만났을 때는 판단이 가능하다.
그런데 사각형이 원 안에 들어가서 교차점이 생기지 않는 경우는 어떻게 판단할 것인가?
나는 밖에서 선을 통해 안으로 들어오는 경우나 그 반대의 경우의 상태를 바꿔주는 방식을 먼저 떠올렸다.
하지만 이 방식은 여러 버그를 일으킬 우려가 있다.
들어오려다가 나가는 경우 (ex. 바다->선->바다)는 판단하지 못한다. 오히려 바다인데 땅이라고 판단할 수 있다.
따라서 다른 방안이 필요했다.
svg끼리의 교차점...교차점...교차점?
불현듯 고등학교 수학 시간 때 주구장창 구했던 교차점 문제들이 떠올랐다.
굳이 svg-intersections라는 라이브러리를 쓸 것도 없이 내가 도형의 교차여부를 판단하는 함수를 만들면 되는거 아닌가?
따라서 나는 맵을 유심히 봤다.
땅의 모양은 전부 타원형이었다. 피그마에서 맵에 타원으로 표시하여 중심과 장축, 단축의 길이를 쉽게 구할 수 있었다.
이를 이용해 다음과 같이 하드코딩했다..
const obstacles = [
{ cx: 252, cy: 313, a: 132, b: 68 },
{ cx: 252, cy: 1216, a: 132, b: 68 },
{ cx: 807, cy: 778, a: 390, b: 168 },
{ cx: 1152, cy: 280, a: 130, b: 63 },
{ cx: 1533, cy: 1070, a: 235, b: 97 },
{ cx: 2014, cy: 352, a: 164, b: 72 },
{ cx: 2394, cy: 1261, a: 168, b: 67 },
{ cx: 2531, cy: 778, a: 380, b: 168 },
{ cx: 2910, cy: 387, a: 159, b: 74 },
{ cx: 3313, cy: 1006, a: 198, b: 86 }
];
이후 타원의 방정식을 이용하기로 한다.
타원의 방정식은 ((x-x0)/a)^2 + ((y-y0)/b)^2 = 1 인데,
만약 이 값이 1보다 작다면 내부에 위치하는 것이고 1보다 크다면 외부에 위치한다.
따라서 1보다 작거나 같은 경우 섬 내부의 땅을 밟았다고 생각하면 됐다.
const isInsideEllipse = (cx, cy, a, b, x, y) => {
const equation = Math.pow((x - cx) / a, 2) + Math.pow((y - cy) / b, 2);
if (equation <= 1) return true;
else return false;
};
따라서 위와 같이 간단한 함수만으로 내부와 외부를 구별할 수 있게 되었다.
즉, svg-intersections와 같은 라이브러리가 더이상 필요하지 않았고
불필요한 연산과 소스코드 양을 줄일 수 있어서 더 효과적이었다.
물론 땅이 매우 많아지는 경우에는 점점 비효율적이 된다.
그 때에는 땅을 일일이 계산하기보다 다른 방식이 필요할 것이다.
또한, 하드코딩된 부분을 어떻게 시스템화할 것인지는 더 고려해봐야한다.
타원, 원, 사각형 뿐만 아니라 다양한 모양에 대해 어떻게 대응할 것인지도 고려해봐야한다.
이 부분에 대해서는 계속해서 고민해볼 계획이다.
'[개발 공부]' 카테고리의 다른 글
사내 테크 블로그에 글 게재하기 (feat. 카카오워크 음성채팅 웹 개발기) (0) | 2023.02.11 |
---|---|
[TIL] 기술 부채 청산하기 - 3월 둘째 주 (0) | 2022.03.13 |
[네트워크] Socket 라이브러리 관점에서 TCP/IP 데이터 송수신 (0) | 2022.02.26 |
[TIL] 기술부채 청산하기 - 2월 셋째 주 (2) | 2022.02.20 |
[읽고 정리하기] 함수형 자바스크립트 - 2장(1) (4) | 2022.02.02 |