클래스업 학생앱 - 타이머 서비스


About Project
Objective
앱이 종료되거나 백그라운드로 전환되어도 학습 시간이 안정적으로 누적되도록, 서버 중심으로 타이머 상태를 관리한 학생앱 타이머 기능.
Tools & Technologies
Express.js, TypeScript, MySQL, Sequelize, MongoDB, DynamoDB, Joi, Swagger
Challenge
학생앱의 타이머 기능은 단순히 화면에서 시간을 재는 기능이 아니라, 실제 사용자가 앱을 종료하거나 백그라운드로 전환하더라도 학습 시간이 끊기지 않고 안정적으로 누적되어야 했습니다. 또한 타이머 기록이 누적될수록 공부 기록 조회가 느려졌고, 개발 서버와 운영 환경 간 시간대 차이로 인해 다른 날짜에 기록이 저장되는 문제도 발생했습니다.
이를 해결하기 위해 서버 중심의 타이머 상태 관리 구조를 설계하고, 저장소 마이그레이션, 인덱스 최적화, 페이지네이션 개선, 시간대 변환 자동화까지 함께 적용했습니다.
startedAt을 저장하고 활성 타이머 상태를 생성합니다.tb_user_timer_status를 기준으로 사용자별 활성 타이머를 관리합니다.현재 시각 - startedAt으로 경과 시간을 계산합니다.durationSec을 저장합니다.앱 환경에서는 사용자가 화면을 닫거나 앱을 종료할 수 있기 때문에, 클라이언트에서만 타이머를 관리하면 학습 시간의 신뢰성이 떨어질 수 있었습니다. 이를 해결하기 위해 타이머 시작 시점을 저장하고, 서버가 경과 시간을 계산하는 구조로 설계했습니다.
핵심 구조는 다음과 같습니다.
startedAt 저장tb_user_timer_status에서 관리현재 시각 - startedAt 계산durationSec 확정 저장이 구조를 통해 클라이언트 상태와 무관하게 서버 기준으로 학습 시간을 계산할 수 있었고, 실제 사용자 환경에서도 안정적인 타이머 기능을 제공할 수 있었습니다.
타이머 서비스에서는 같은 사용자가 동시에 여러 개의 타이머를 활성화하면 학습 시간이 중복 계산될 수 있었습니다. 이를 방지하기 위해 tb_user_timer_status.userId에 유니크 인덱스를 적용해 한 사용자당 하나의 활성 타이머만 가질 수 있도록 제약을 걸었습니다.
이를 통해:
가 가능해졌습니다.
초기에는 개발 서버의 MongoDB를 기반으로 기능을 구현했습니다. 다만 실제 서비스 단계에서는 AWS 생태계와의 연동, 기존 운영 설정과의 호환성, 고가용성을 함께 고려해야 했습니다. 이에 따라 운영 환경에서는 DynamoDB로 마이그레이션하는 방향으로 구조를 전환했습니다.
이 경험을 통해 단순히 기능 구현에 그치지 않고, 운영 환경에 적합한 저장소 선택과 마이그레이션까지 고려하는 백엔드 설계의 중요성을 배웠습니다.
MongoDB에 타이머 로그, 즉 사용자가 언제 어떤 과목을 얼마나 공부했는지를 지속적으로 저장하고 있었는데, 데이터가 많이 쌓이면서 공부 기록 조회 성능이 크게 저하되었습니다.
초기에는 userId 단일 인덱스만 사용하고 있었지만, 실제 조회 패턴은 사용자별 기록을 startedAt 기준으로 정렬해 가져오는 형태였습니다. 이를 반영해 userId + startedAt 복합 인덱스를 추가했고, explain 기준으로 주요 쿼리 실행 시간을 77% 단축했습니다.
이 경험을 통해 조회 조건과 정렬 기준까지 함께 반영한 인덱스 설계가 중요하다는 점을 확인할 수 있었습니다.
타이머를 사용하는 전체 사용자 목록을 조회하는 API는 무한 스크롤 방식으로 동작했는데, 기존에는 offset 기반 페이지네이션을 사용하고 있어 아래로 스크롤할수록 성능이 저하되는 문제가 있었습니다.
이를 해결하기 위해 startedAt 기준의 키셋 페이지네이션을 적용했습니다. 가장 오래 공부한 사용자가 상위에 노출되도록 정렬 기준을 맞추고, 다음 페이지 조회 시에도 offset 누적 없이 안정적으로 데이터를 가져올 수 있도록 개선했습니다. 그 결과 explain 기준으로 기존 대비 30% 성능 개선을 확인할 수 있었습니다.
개발 서버의 MongoDB는 KST 기준으로 동작했고, 운영 AWS 환경은 UTC 기준이어서 시간대 차이로 인해 공부 기록이 다른 날짜에 저장되는 문제가 실제로 발생했습니다.
이를 해결하기 위해 Mongoose Hook을 활용해 KST 기준 입력이 들어올 경우 저장/조회 전 UTC 변환이 일관되게 수행되도록 구성했고, 이를 MongoDB 문서 전반에 공통 적용했습니다. 이를 통해 날짜 단위 학습 기록의 정합성을 확보할 수 있었습니다.
기존 Express 구조에서는 try-catch 반복이 많았고, 누락된 비동기 에러가 전역 처리로 전달되지 않는 문제가 있었습니다. 또한 API마다 응답 형식이 달라 유지보수와 협업 비용이 발생했습니다.
이를 해결하기 위해:
async handler를 도입해 반복적인 try-catch를 제거하고 비동기 에러 전파를 일관화success / data / error 구조의 응답 미들웨어를 설계해 응답 형식을 통일했습니다. 이 구조는 제가 먼저 학생앱 서버에 직접 적용했고, 이후 문제 없음을 확인한 뒤 웹서버와 관리자 서버에도 동일한 방식이 확산되었습니다.
기존에는 요청/응답 검증 누락이 자주 발생해 프론트와 백엔드 간 소통 비용이 컸고, 연결 과정에서 타입 불일치 문제가 반복적으로 생겼습니다.
이를 해결하기 위해 Joi 기반으로 요청/응답 검증을 모두 적용하고, Joi to Swagger를 사용해 Joi 스키마 기반으로 Swagger 명세를 자동화했습니다. 이 체계를 적용한 이후에는 프론트-백 연결 과정에서 타입 검증 문제는 한 번도 발생하지 않았습니다.
tb_user_timer_status의 유니크 제약으로 사용자별 단일 활성 타이머 보장startedAt 기준 키셋 페이지네이션으로 무한 스크롤 API 최적화이 프로젝트를 통해 단순 타이머 기능 구현을 넘어, 실제 사용자 환경에서 동작하는 서버 상태 관리, 조회 성능 최적화, 운영 환경 대응까지 함께 설계하는 백엔드 개발의 중요성을 배웠습니다.
Related Projects