Rocky 's Blog

[네이버부스트캠프 웹・모바일 10기] 멤버십 20주차 회고

  • 네이버부스트캠프
  • 회고
2026. 01. 25.
게시글 썸네일

회고를 시작하며


이번 주는 어떻게 보냈는가?

이번 주에는 개발의 기반이 어느 정도 잡히면서 전반적인 작업 속도가 빨라졌다. 팀과 함께 이번 주 목표를 분명하게 정했고, 팀 회고를 통해 실제로 98% 정도 달성했다는 데 의견을 모았다. 2%는 자잘한 오류들 때문에 배제하게 되었다. 주요 기능들은 큰 문제 없이 구현을 마쳤고, 이제는 구현된 기능들을 다듬고 남은 작업들을 마무리 해야한다.

이번 주의 작업 방식은?

PR과 슬랙을 통한 소통이 이전보다 더 주기적으로 이루어졌고, 진행 상황을 묻고 공유하는 주기도 의도적으로 더 짧게 가져갔다. 덕분에 팀 내에서 진행 상황과 이슈가 빠르게 공유되었고, 서로의 작업 맥락을 이해하는 데 도움이 되었다.

무엇보다도 팀 내에서 실제로 기능을 테스트해보는 과정이 큰 도움이 되었다. 테스트를 진행하면서 부족했던 부분들을 구체적으로 발견할 수 있었고, 이를 리스트업해 다시 개발에 반영하는 개발 → 테스트 → 문제 리스트업 → 개선 이라는 사이클을 이루었다. 짧은 주기로 피드백을 하고, 개선하는 방식이 작업의 능률을 높이는 데 기여했다고 생각한다.

지난 주 회고의 피드백


팀 회고에 대한 피드백 반영

어떤 점이 특히 개선되었는가?

목표를 분명하게 세우자는 내용들을 바탕으로, 이번 주에는 구현해야 할 기능의 범위와 우선순위를 구체화했다. 데모와 오픈 스테이지에서의 시연을 하나의 목표로 잡으니, 실제 사용자 프로세스를 따라가며 중요한 흐름과 기능이 무엇인지가 더 명확해졌다.

그 결과, 어떤 작업을 어떤 순서로 진행해야 하는지 정리하기 쉬웠고, 역할 분배 역시 훨씬 수월해졌다. 우선순위가 명확해지니 불필요하게 망설이거나 고민하는 시간이 줄었고, 이에 따라 전체 개발 속도도 함께 올라갔다.

사용자 테스트를 진행하자.

사용자 테스트에 대한 논의도 있었다. 이전까지는 사용자 테스트를 진행하지 않았고, 실제로 서버가 어느 정도 트래픽까지 버틸 수 있는지에 대한 감도 없었다. 이를 개선하기 위해 이번 주에는 데모 시연을 하나의 구체적인 목표로 삼아 진행했다.

우선 팀 내부에서 서비스를 직접 사용해보며 어떤 문제가 있는지 파악했고, 발견한 이슈들을 리스트업해 정리했다. 또한, 시연 흐름을 점검하며 발생 가능한 병목 지점들을 사전에 확인했다. 덕분에 실제 시연에서 외부 참여자가 합류했을 때도 안정적인 서버 상태를 유지하며 성공적으로 데모를 마칠 수 있었다.

다만, 참여자 화면 목록이 어디에서 확인되는지 한눈에 드러나지 않는다와 같은 사용성 측면의 개선 포인트도 확인할 수 있었다. 이러한 부분은 개발자인 우리는 자연스럽게 넘어가지만, 처음 접근하는 실제 다른 사용자들에게서 나오는 소중한 의견이라고 생각한다. 이러한 내용들이 도움이 많이 된다고 생각하여 사용자 테스트를 꾸준하게 이어나가보려고 한다.

개인 회고에 대한 피드백 반영

화이트보드를 활용하겠다.

지난주 개인 회고에서 세웠던 목표 중 하나는 화이트보드를 더 적극적으로 활용해보는 것이었다. 그 이유는 머릿속에만 떠다니는 생각들을 눈에 보이는 형태로 꺼내서, 전체 흐름을 먼저 잡은 뒤 개발에 들어가고 싶었기 때문이다. 이렇게 하면 중간에 구조를 바꾸는 수정이 줄어들고, 처음부터 비교적 안정된 흐름을 그려갈 수 있을 것이라 기대했다.

이번 주에는 이 목표를 의식적으로 떠올리면서, 생각이 막힐 때마다 화이트보드를 앞에 섰다. 특히 서버와의 통신 과정이 머릿속에서 명확하게 그려지지 않을 때, 화이트보드를 자주 사용했다. 우리 서비스에서는 강의실에 입장하면 소켓이 초기화되고, mediasoup가 초기화되며, 동시에 서버에 여러 이벤트가 전달되어야 한다. 이 과정에서 어떤 데이터가 오가고, 어떤 시점에 상태를 저장해야 하는지가 복잡하게 얽혀 있었는데, 이를 단계별로 화이트보드에 풀어 적으며 정리했다.

전체 그림은 찍어두지 않아서… 중간 과정이라도 남겨본다…
전체 그림은 찍어두지 않아서… 중간 과정이라도 남겨본다…
화이트보드를 활용하면서 어떤 변화가 있었는가?

화이트보드에 흐름을 그리다 보니, 내 머릿속에서만 얼추 알고 있던 단계들이 보다 명확한 순서와 구조를 갖추게 되었다. 덕분에 이 단계에서는 어떤 로직이 실행되어야 하는가?, 이 이벤트 이후에는 어떤 상태 변경이 필요하는가? 를 사전에 정리하고 나서 코드를 작성할 수 있었다. 이런 방식으로 미리 흐름을 설계하다 보니, 구현 후에 되돌아가서 수정해야 하는 부분이 확실히 줄어들었다.

수정이 줄어들었다는 것은 곧 전체 작업 속도가 빨라졌다는 의미이기도 했다. 무언가 애매한 상태에서 바로 코드를 치기보다는, 화이트보드 앞에서 고민한 뒤에 키보드를 잡는 방식이 잘 맞았다. 앞으로도 복잡한 기능을 구현하거나 생각 정리가 잘 되지 않을 때는, 먼저 화이트보드에 한 번은 그려보고 시작하는 습관을 더 굳혀가고자 한다.

이전 프로젝트의 내용을 바탕으로


어떤 과정을 진행 중이었는가?

화상 연결을 담당하는 mediasoup 로직을 통해 서버와 연결하는 과정을 진행하고 있었다. 이때 연결 통로인 transport 생성과 미디어를 송출하는 produce 과정이 여러 곳에서 반복적으로 사용되고 있었다.

특히 화면 공유 기능의 경우, 동일한 로직이 서로 다른 위치에서 중복되어 있었다. 상단의 화면 공유 중지 버튼, 하단의 화면 공유 토글 버튼, 브라우저 자체의 화면 공유 중지 버튼까지 총 세 군데에서 똑같은 공유 중지 로직이 사용되고 있었다. 이런 구조는 시간이 지날수록 유지보수와 변경에 불리한 형태라고 느껴졌다.

화면 공유 중지 기능이 사용되는 곳
화면 공유 중지 기능이 사용되는 곳
그렇다면 이를 하나의 훅으로 만들어서 담당하면 되는거 아닌가?

처음에는 이 중복된 로직을 하나의 훅으로 추상화해서, 필요한 위치에서 호출하는 방식을 떠올렸다. 공통 훅만 잘 만들어두면 화면 공유를 중지해야 하는 지점마다 그 훅을 호출하는 것으로 중복을 줄일 수 있을 것 같았기 때문이다.

하지만 곧 다른 기능들이 함께 떠올랐다. 카메라나 마이크를 켜고 끄는 기능도 결국 어떤 미디어를 송출할 것인가, 어떤 상태를 갱신할 것인가 만 다를 뿐, 서버에 미디어를 보내고 끊는 과정이라는 점에서는 동일한 로직을 공유하고 있었다.

이렇게 생각을 확장해보니, 화면 공유, 카메라, 마이크 등 각 기능마다 훅을 따로 만든다면 오히려 비효율적이라는 결론에 이르게 되었다. 겉으로 보기에는 훅으로 나눴으니 중복을 줄인 것 같지만, 실제로는 거의 같은 연결, 송출 로직이 여러 훅에 걸쳐 반복될 수 있기 때문이다. 결국 처음 생각했던 중복로직의 일관성 문제를 근본적으로 해결하지 못한다는 한계를 느꼈다.

변경 전, 컴포넌트와 훅의 관계
변경 전, 컴포넌트와 훅의 관계
연결하는 과정을 공통으로 만든다면 어떨까?

그래서 관점을 조금 더 근본적으로 바꾸어 보기로 했다. mediasoup에 직접적으로 관련된 로직들, 예를 들어 transport를 생성하고 producer나 consumer를 다루는 부분을 하나의 공통 계층으로 분리하는 방향이다. 그리고 이 계층을 기반으로 카메라, 마이크, 화면 공유 기능을 위에서 덧입히는 형태로 구조를 다시 설계해보았다.

이때 지난번에 OS를 직접 구현해보며 배웠던 개념이 떠올랐다. 운영체제의 커널에서 실행되는 로직들은 사용자 공간에서 직접 알 필요가 없고, 단지 고수준 API를 통해 기능을 요청하기만 하면 된다는 구조였다. 이를 mediasoup에 그대로 비유해보면, mediasoup 관련 로직을 커널에서 수행되는 영역처럼 두고, 컴포넌트에서는 이를 시스템 콜처럼 호출하는 고수준 API만 사용하는 구조를 만들 수 있겠다는 생각이 들었다.

그렇게 해서 mediasoup 로직을 담당하는 훅들을 가장 아래의 Low-Level 계층에 두고, 그 위에 이 훅들을 조합해 사용하는 인프라 계층을 두었다. 그 상단에는 비즈니스 흐름을 제어하는 중앙 제어 계층 역할을 두었고, 최상단에는 UI 컴포넌트가 이 컨텍스트를 통해 고수준 명령만 주고받도록 설계했다.

변경 후, 컴포넌트와 훅의 관계
변경 후, 컴포넌트와 훅의 관계
이렇게 했을 때의 장점은 무엇인가?

이 구조로 바꾼 뒤 가장 크게 느낀 점은 역할과 책임이 훨씬 명확해졌다는 것이다. mediasoup를 통한 연결이나 송출 과정에서 문제가 생긴다면 공통 계층의 로직을 먼저 확인하면 되고, 화면에 어떻게 보여줄지, 어떤 상태를 언제 변경할지는 이를 감싸는 상위 API와 컴포넌트에서 다루면 되었다. 어디를 먼저 살펴봐야 하는가?가 분명해지면서 디버깅과 원인 파악에 걸리는 시간이 줄어들었다.

또한 중앙 제어 계층을 둔 덕분에 컴포넌트는 mediasoup 내부 동작을 몰라도 되었고, mediasoup 관련 훅들 역시 개별 컴포넌트의 상태나 UI 세부사항을 신경 쓰지 않아도 되었다. 이렇게 계층을 분리하고 의존 방향을 정리하자 코드의 응집도와 재사용성이 올라갔다.

교훈이 있다면

이번 경험에서 가장 크게 느낀 점은, 특정 도메인 기능을 구현한다고 해서 그 도메인 안에서만 답을 찾아야 하는 것은 아니라는 것이다. 운영체제의 커널, 유저 공간처럼 전혀 다른 분야의 개념이라도, 문제를 분리하고 추상화하는 관점에서는 충분히 적용할 수 있는 아이디어가 많다는 점을 깨달았다. 다른 분야에서의 개념을 끌어와 구조를 재해석해 보는 과정이, 아직 많이 부족하다고 느끼는 나에게는 중요한 훈련이라고 느꼈다.

이런 연결고리를 찾아내고 실제 코드에 적용해본 점이 특히 좋았다. 물론 이런 설계 방식은 이미 소프트웨어 아키텍처나 디자인 패턴에서 특정 이름으로 정리되어 있을 것이다. 이번에 적용한 구조가 어떤 패턴에 가까운지, 어떤 이름으로 불리는지 더 찾아보고 공부하며, 앞으로는 이런 사고와 구조를 조금 더 다듬어 다른 프로젝트와 맥락에서도 자연스럽게 활용해보고 싶다.

사용자는 어떻게 느낄까?


어떤 부분을 맡고 있었는가?

원격 미디어 수신 설계를 마친 후, 사용자가 자신의 카메라와 마이크를 제어할 때 하드웨어 자원을 어떻게 효율적으로 다룰 것인지 고민했다. 단순히 장치를 켜고 끄는 기능을 넘어, 성능 효율과 사용자 편의성을 위해 여러 단계의 시행착오를 거쳤다.

첫 번째 시도: 전체 하드웨어 리소스 해제

카메라나 마이크를 끄면 즉시 track.stop()을 호출하여 하드웨어 권한을 반환하는 방식을 적용했다. 이렇게 하면 배터리 소모를 줄이고, 개인 프라이버시를 보호할 수 있다는 장점이 있다.

하지만 문제는 다시 켤 때였다. 장치를 초기화하는 지연이 발생해 화면이 일시적으로 검게 번쩍이거나 깜빡이는 현상이 나타났다. 이러한 시각적인 불안정은 분명 사용자 경험을 떨어뜨리는 요소였다.

두 번째 시도: 하드웨어 유지 및 스트림 송출만 중단

다음으로는 장치 권한을 유지하되, 서버로 미디어를 송출하는 부분만 제어하는 방식을 시도했다. 이 경우 장치를 다시 켤 때 초기화 과정이 생략되므로 반응 속도가 빨라지고, 화면 깜빡임도 사라져 자연스러운 전환이 가능했다.

그러나 예상치 못한 문제가 있었다. 사용자가 카메라를 껐음에도 불구하고 노트북 상단의 카메라 LED가 계속 켜져 있었다. 이는 사용자 입장에서 분명 카메라를 껐는데, 왜 여전히 켜져 있다 하지? 라는 불안감을 유발할 수 있으며, 서비스 신뢰도를 크게 떨어뜨릴 수 있는 문제였다.

마지막 시도: 미디어 특성별 분리

최종적으로 미디어의 특성과 사용자 피드백의 중요도 에 따라 제어 전략을 분리하였다.

사용자가 카메라를 끄는 경우, 하드웨어 리소스를 완전히 해제하도록 하였다. 약간의 재연결 지연보다는 LED가 꺼지는 물리적인 보안이 사용자 경험에서 더 중요한 요소라고 판단했기 때문이다.

오디오는 하드웨어 연결을 유지하고 소프트웨어 레벨에서 송출을 제어하였다. 음성 대화에서는 잠깐의 지연도 불필요한 재질문을 유도하며, 대화의 흐름이 끊길 수 있다. 원할한 커뮤니케이션 흐름에서는 즉각적인 반응성이 더 중요하다고 판단하였다.

Zoom에서 어떤 방식을 택하고 있는가?

실제 Zoom에서도 비슷한 동작 방식을 확인할 수 있었다. 회의 중 카메라가 켜지면 노트북의 LED와 브라우저 상단 아이콘이 표시된다. 반대로 카메라를 끄면 아이콘이 즉시 사라지고, LED도 꺼진다.

Zoom에서 카메라를 사용 중일 때
Zoom에서 카메라를 사용 중일 때

그러나 음소거 상태에서는 마이크 아이콘이 여전히 활성화된 상태로 남아 있다. 이는 하드웨어 연결은 유지하되 송출만 막고 있음을 의미한다. 또한, 음소거 상태에서 발언을 시도하면 마이크가 음소거되어 있습니다 라는 피드백을 제공한다. 즉, 마이크 권한은 유지된 채로 소프트웨어적으로 제어되고 있다는 것을 알 수 있었다.

결론

단순히 시스템 자원 효율성만을 생각하는 것은 바람직하지 않다는 점을 깨달았다. 만약 리소스 절감만을 고려했다면 카메라와 마이크 모두를 완전히 해제했겠지만, 그 결과 사용자는 불편함을 느끼고, 서비스를 이용하지 않게 될 것이다.

때로는 오디오처럼 더 많은 자원을 소모하더라도 즉각적인 대화 경험을 보장해야 하고, 때로는 비디오처럼 빠른 반응보다 안전하다는 감각이 더 중요할 수도 있다. 결국 기술적인 최적화와 사용자 경험 사이에서 균형을 맞추는 것이 중요하다고 느꼈다.

사용 테스트의 중요성


어떤 점에서 중요한가?

항상 로컬 환경에서만 테스트를 진행하던 중, 어느 정도 기능 구현이 되어가자, 이제는 전체 흐름을 점검하는 테스트가 필요하다고 느꼈다. 그래서 모든 팀원이 개발 서버에 접속해 실제 서비스 흐름대로 하나씩 테스트를 진행하기로 했다.

이거… 큰일났다…

하지만 막상 테스트를 시작하자마자 큰 문제가 터졌다. 로컬에서는 아무 문제 없이 돌아가던 다른 사람들의 화면이 서버 환경에서는 전혀 보이지 않았다. 해당 기능을 담당하던 입장에서 머리가 하얘질 수밖에 없었다. 로컬에서 수차례 확인했기에 스스로는 문제가 없다고 생각하고 있었고, 그래서 어디서부터 무엇을 의심해야 할지 막막한 상태였다.

그래도 문제를 풀어가기 위해 원인이 될 수 있는 후보들을 하나씩 정리하며 지워 나가기 시작했다. 강의실 생성이나 입장 API는 정상적으로 동작하는 것으로 보아 일반적인 서버 API의 문제라기보다는, WebRTC 신호 교환이나 mediasoup와의 연결 단계에서 문제가 생겼다고 판단했다.

환경 변수, 포트, 방화벽 설정 등 눈에 보이는 요소들을 차분히 확인했지만 뚜렷한 원인은 보이지 않았다. 이때 다른 팀원이 Docker 설정에서 문제 지점을 발견했고, 그 부분을 수정하면서 상황이 풀리기 시작했다.

어떤 점이 문제였는가?

문제의 핵심은 WebRTC 특성과 Docker 네트워크 방식의 차이에서 비롯되었다. WebRTC는 미디어 전송을 위해 매번 동적으로 여러 포트를 사용하는 반면, Docker의 기본 bridge 모드는 미리 정해진 포트만 열어주는 방식이라 충돌과 제약이 생기기 쉬운 구조였다.

WebRTC 특성상 필요한 포트를 모두 미리 열어두자니 비현실적이고, 그렇다고 그대로 둘 수도 없는 상황이었다. 결국 Docker를 host 모드로 전환해 네트워크 계층을 우회함으로써 포트를 별도로 열지 않아도 되도록 설정했고, 이후에는 서버가 정상적으로 동작하면서 비디오도 제대로 송출되었다.

로컬 테스트는 모든 것을 보장하지 않는다.

로컬 테스트만으로는 실제 상황에서의 모든 문제를 발견할 수 없다는 점을 뼈저리게 느꼈다. 만약 개발 초기에라도 실제 배포 환경과 비슷한 조건에서 테스트를 자주 해보았다면, 이번 이슈도 훨씬 이른 시점에, 더 적은 노력으로 해결할 수 있었을 것이다.

또 한 가지 크게 느낀 점은, 운이 좋아 뛰어난 팀원을 만났다는 점이다. 나 혼자였다면 포트 설정 정도에서 생각이 멈춰버렸을 것이다. 아마 그 이후에는 뚜렷한 가설 없이 전체 코드를 다시 훑어보는 비효율적인 방식으로 시간을 보냈을 것이다.

반면, 함께 문제를 해결한 팀원은 본인이 구현한 부분에 대해 확신을 가지고, 과감히 제외하면서 원인의 범위를 좁혀 나가는 접근을 보았다. 그 확신이 있기에 논리적으로 탐색 범위를 줄여갈 수 있었고, 그 과정이 인상 깊었다.

앞으로는 어떻게 할 것인가?

내가 작성한 코드에 대한 확신과 이해를 바탕으로 원인을 빠르게 추론하고 검증해 나가는 역량이 필요하다는 생각이 들었다. 지금의 나는 “확실히 아니다”라고 말하기보다, 이미 여러 번 확인한 부분조차 다시 들여다보며 시간을 흘려보내는 쪽에 더 가까웠다.

다음 주에는 오류가 발생했을 때, 우선 머릿속에서 가능한 원인을 몇 가지 가설로 세워보고, 실제 로그나 설정들을 하나씩 대조하며 내가 세운 가설이 맞는지 검증하는 연습을 해보고자 한다. 만약 생각과 다른 결과가 나왔다면, 단순히 “틀렸구나”에서 끝내지 않고 왜 달랐는지, 어떤 전제를 잘못 두었는지, 어떤 조건을 놓쳤는지까지 함께 돌아보고자 한다.

회고를 마무리하며


2주라는 시간

이제 정말 2주밖에 남지 않았다. 우리가 처음 생각했던 수준까지 기능을 구현하려면 아직 갈 길이 멀고, AI를 활용해야 하는 구간도 있어서 시간적으로 여유롭다고 보기는 어렵다. 그래서 남은 기간 동안 어떤 흐름으로 개발을 진행할지, 어디까지를 목표로 삼을지에 대한 계획을 다시 세워보려 한다.

이 프로젝트에서 우리가 진짜로 달성하고 싶은 가치가 무엇인지, 그리고 데드라인까지 현실적으로 가져갈 수 있는 범위가 어디까지인지를 기준으로 우선순위를 정하는 일이 중요하다고 느꼈다. 개발자라면 서비스 완성 에 집중해야 할 시점이라 생각한다.

코드 한 줄 한 줄의 완벽함에 집착하기보다는, 우선 사용자에게 동작하는 서비스를 내놓는 것이 우선이라고 받아들이고 있다. 남은 2주 동안에는 작동하는 기능을 빠르게 완성하는 데 집중하고, 그 이후에 리팩토링과 구조 개선을 진행하는 흐름으로 가져가고자 한다.

더 나아가서

시간이 지날수록 나 또한 그렇고 팀원들 모두 조금씩 지쳐가는 것 같다. 어떻게 해야 조금 더 즐겁게, 그리고 효율적으로 이 막바지를 버텨낼 수 있을지 고민하게 된다.

기획 단계에서는 오프라인으로 모여 의견을 주고받는 시간이 정말 도움이 되었다고 느꼈다. 그때는 의사결정 속도도 빨랐고, 한정된 시간 안에 많은 내용을 정리하고 합의할 수 있었다.

하지만 지금처럼 본격적인 개발 단계에 들어온 상황에서, 오프라인이 항상 최선의 해답인지는 잘 모르겠다. 이동하는 시간과 에너지, 듀얼 모니터 같은 개발 환경이 제한되는 점을 생각하면, 오프라인이 오히려 능률을 떨어뜨릴 가능성도 있다고 느꼈다.

대안을 찾던 중, 슬랙 허들이나 줌처럼 필요할 때 바로 음성으로 이야기할 수 있는 환경을 적극적으로 활용해보려 한다. 이슈가 생기면 바로 오디오를 켜고, 짧게 논의하는 식이라면 오프라인에서 느꼈던 속도를 어느 정도는 가져갈 수 있을 것 같다.

다음 주부터는 사실상 막바지라고 생각하고, 반드시 해내야 하는 구간에 들어간다. 더 끌어올려야 하고, 더 집중해야 하는 시기다. 좋은 팀원들을 만나 도전적인 프로젝트를 함께하고 있는 만큼, 이 프로젝트를 끝까지 밀어붙여 완주했다는 결과를 내고 싶다. 나 스스로 먼저 노력하고, 남은 기간 동안 조금 더 열심히, 조금 더 깊게 몰입해보려 한다.