Overview
프로젝트 소개 · 데이터베이스 · 문제 해결 내용을 담고 있습니다.
※ 이 PDF에서는 다이어그램이 페이지 폭에 맞춰 작게 보일 수 있습니다. 원본 크기로 보거나 개별 인쇄하려면, 온라인 문서(각 장 상단의 "웹 문서" 링크)에서 다이어그램을 클릭해 주세요.
소개
-
기획부터 프론트엔드와 백엔드 개발까지 직접 진행한 개인 프로젝트입니다.
-
Pomodoro technique을 기반으로 하는 공부 & 작업 시간 측정 App입니다.
-
한 사이클의 작업 시간을 여러 개의 작업 세션들과 이어지는 짧은 휴식 그리고 마지막 긴 휴식으로 나누어 높은 집중력을 유지하는 데 도움을 줍니다. (작업 시간을 pomodoro 또는 pomo라고 부릅니다).
One cycle == (pomo + short break) * number of pomos + long break
-
작업 시간에 대한 통계를 제공하여 사용자의 생산성을 파악하는데 도움을 주고 다양한 Feedback기능을 통해 동기부여가 될 수 있도록 도와줍니다.
만들게 된 계기
- 원래 사용하던 앱이 있었는데, 일시 정지를 한 번 했을 때 얼마 동안 했는지 기록이 안 되었습니다.
- 그것을 계기로 제가 원하는 기능들을 추가하면서 배워보자는 생각으로 만들기 시작했습니다.
문제 해결 요약
Socket.IO로 시그널링 채널 설계하기
브라우저끼리는 코덱과 네트워크 정보를 직접 주고받을 수 없어서, 이를 중계하는 채널을 NestJS Gateway 위에 Socket.IO로 구현했습니다. mediasoup은 연결 시점에 앱이 콜백을 호출해 주기를 기다리는데, 그 결과는 서버를 거쳐야 알 수 있고 콜백이 호출되지 않으면 아무 에러 없이 연결이 멈춰 버립니다. 그래서 소켓 ack 응답을 이 콜백에 연결해 성공과 실패가 모두 라이브러리까지 전달되도록 했습니다. 또한 재연결할 때마다 소켓 ID가 새로 발급되는 문제는, 사용자 신원을 기준으로 세션을 승계하고 뒤늦게 도착한 이전 소켓의 종료 이벤트는 무시하는 방식으로 해결했습니다.
WebRTC 연결 끊김 대응하기
화면 공유 기능은 시그널링에 TCP를, 미디어 전송에 UDP를 사용하는데, 두 연결이 따로 또는 동시에 끊기는 경우가 있었습니다. 재연결 시 Firebase UID로 기존 세션을 복구하고, ICE Restart로 미디어 연결까지 되살리도록 구현했습니다. 또한 이런 상황을 재현해 검증할 수 있도 록 로컬 환경에서 TCP/UDP 연결을 직접 끊어 보는 테스트 방법도 마련했습니다.
세션 진행 중에도 다른 페이지를 자유롭게 이동하기
타이머 UI를 그리는 컴포넌트는 다른 페이지로 이동하면 unmount되어 상태를 잃어버립니다. 그래서 상태를 IndexedDB에 저장해 두었다가 돌아올 때 복원하도록 했고, 페이지 밖에서도 카운트다운이 이어지도록 처음에는 Service Worker를 사용했습니다. 하지만 Service Worker는 특정 이벤트에만 반응하고 곧 종료된다는 사실을 뒤늦게 알게 되어, 카운트다운은 메인 스레드로 다시 옮기고 Pub/Sub 패턴으로 페이지 간 상태를 동기화했습니다.
인터넷 연결이 끊겨도 핵심 기능은 계속 쓸 수 있게 하기
시험 기간에 학교 도서관에서 작업하면 와이파이가 간헐적으로 끊겼고, 그동안 진행한 기록이 서버에 저장되지 않는 문제가 있었습니다. 요청을 임시로 보관해 두었다가 연결이 복구되면 순서대로 다시 보내도록 했고, 같은 리소스를 여러 번 수정한 요청은 하나로 합쳐 보내도록 했습니다.
Horizontal Timeline 직접 만들기
앱의 사용 방식에 맞는 타임라인 라이브러리를 찾기 어려워, CSS 연습을 겸해 직접 구현했습니다. 반응형으로 만드는 과정에서 media query 방식이 기존 이벤트 핸들러와 충돌해 제대로 동작하지 않았고, 대신 matchMedia 같은 Web API로 화면 크기 변화를 감지해 픽셀 값을 다시 계산하는 방식으로 바꿨습니다.
기능 요약
- 포모도로와 휴식으로 구성된 한 사이클에 대한 설정 및 각 세션의 측정
- 타임라인을 통해 시각적 피드백 제공
- 일시 정지 및 정지한 시간 측정
- 한 세션에서 한 일시 정지들을 타임라인에 그려서 피드백 제공
- 자동 시작
- Statistics 페이지에서 일간 및 주간 통계 제공
- 한 사이클을 얼마나 잘 이행했는지에 대한 feedback 제공
- 세션을 구분하기 위한 카테고리 생성
- 한 세션에서 여러 카테고리의 작업 가능
- 카테고리별 통계
- 세션 진행 도중
/timer,/statistics, 그리고/settings페이지 간 자유롭게 이동 가능 - 목표설정 및 달성에 관한 통계
- 인터넷 연결이 불안정한 경우에도 주요 기능을 사용할 수 있도록 하기
- Todoist Integration Option 제공
- 화면 공유 스터디룸
기술 스택
프론트엔드
HTML, CSS, JavaScript and WEB APIs
- CSS
- WEB APIs
TypeScript
SPA(Single Page Application) Related
- React
useMemo,useCallback,useContext, etc.
- React Router DOM
- Nested Routing
HTTP Requests & Authentication
Styling
- Styled Components, React Inline Style
Real-time Communication
- WebRTC (Media Transport via mediasoup-client)
- Socket.io (Signaling)
Etc
- Recharts
- idb
- for a promise-based interface for interacting with Indexed DB
- date-fns
- JavaScript Design Patterns
- Pub/Sub (Publisher-Subscriber Pattern)
백엔드 & 인프라
- Framework: NestJS
- Database & ORM: PostgreSQL, Drizzle
- Real-time & Media: WebRTC, mediasoup (SFU Media Server), socket.io (Signaling)
- Infrastructure & Hosting:
- GCP Compute Engine (Ubuntu virtual instance)
- Vercel (Frontend React app)
- Web Server & Process Management: Nginx (Reverse Proxy), PM2
아키텍처 (Architecture)
이 프로젝트의 전체 시스템 아키텍처는 C4 모델을 사용하여 표현했습니다. C4 모델은 소프트웨어 시스템을 위성 사진을 보듯 가장 넓은 관점(C1)에서 시작하여, 점차 코드가 있는 상세 레벨(C4)까지 확대하며 설명하는 방식입니다.
System Context Diagram (C1)
이 다이어그램은 우리 '뽀모도로 앱'이 외부 세계와 어떻게 상호작용하는지 보여주는 가장 높은 수준의 지도입니다.
Container Diagram (C2)
위 시스템을 확대한 다이어그램입니다. '뽀모도로 앱'이 어떤 기술적인 컨테이너들(웹 애플리케이션, API 서버, 데이터베이스 등)로 구성되어 있는지 보여줍니다.
Component Diagram (C3)
미디어 전송 구조 (SFU)
위 다이어그램들이 "무엇으로 구성되어 있는지"를 보여준다면, 이 그림은 런타임에 미디어가 실제로 흐르는 모양을 보여줍니다. 참가자가 3명인 방을 기준으로 그렸습니다.
왼쪽과 오른쪽은 같은 참가자 3명이며, 한 사람이 동시에 송신자이자 수신자라는 점을 화살표가 겹치지 않게 나눠 그린 것입니다.
- 업스트림 3개, 다운 스트림 6개라는 비대칭이 SFU의 핵심입니다. P2P라면 각 참가자가 나머지 모두에게 직접 보내야 해서 1인당 N-1개를 업로드해야 하지만, SFU에서는 한 번만 올리고 복제는 서버가 합니다. 업로드 대역폭이 참가자 수에 비례해 늘어나지 않습니다.
- 대신 서버는 재인코딩을 하지 않습니다. 그래서 수신자별 화질 조절은 서버가 새로 만들어내는 것이 아니라, 송신자가 미리 올려둔 simulcast 레이어 중 하나를 고르는 방식으로 이뤄집니다. 요청한 화질과 실제 전달되는 화질이 어긋날 수 있는 이유이기도 합니다(관련 문제 해결).
- 미디어는 UDP로, 시그널링(Socket.IO)은 TCP로 흐르며 서로 독립적으로 끊길 수 있습니다. 이 점을 다룬 내용은 WebRTC Disconnection Handling에 있습니다.
- 현재 화면 공유는 영상만 전송합니다(
getUserMedia({ video: true, audio: false })). 또한 mediasoup worker와 router를 각각 하나만 두고 모든 방이 공유하므로, 방이 늘어나면 방별 router 분리가 필요합니다.
Page Screenshots
- 아래는 각 페이지의 간단한 스크린샷입니다. 구체적인 기능 설명은 'Features' 사이드바 항목에 서 다루겠습니다.
/timer

세션 진행 도중에 카테고리 바꾼 경우.

타임라인

/statistics
Total Graph and Category graph
두번째 그래프에 Uncategorized만 그려짐.

두번째 그래프에 Documentation 와 Development.

두번째 그래프에 Uncategorized 와 Job Search and Application.

Two graphs show different week data
첫번째 - 11.4 ~ 11.10, 두번째 - 10.28 ~ 11.3.

첫번째 - 10.28 ~ 11.3, 두번째 - 11.4 ~ 11.10.

Mouse hover over each graph


Weekly Trend

Goals
Daily goals

Weekly goals
/group-study
로비

방

한 화면에

/settings


