← back

LMS 마이페이지

LMS(Learning Management System) 마이페이지를 전면 개편한 통합 대시보드. 학생은 할 일과 학습현황을, 교수자는 채점 현황과 학습 부진자를 한눈에 파악할 수 있다.

2024.12 ~ 2025.02 · 풀스택 개발
PHP/LaravelReactTypeScriptPostgreSQLMongoDB

background

기존 LMS 마이페이지는 단순 과목 목록 나열이었다. 학생 대시보드(할 일, 타임라인, 학습현황, 공지)와 교수자 대시보드(채점, 학습 부진자, 평가, 게시물)를 하나의 통합 대시보드로 제공한다.


architecture

graph TD
    CANVAS[Canvas LMS] -->|Global LTI| LX_API
    subgraph "LearningX · PHP / Laravel"
        LX_API["Dashboard API · REST
할일 · 타임라인 · 학습현황 · 채점/평가 · 공지"] LX_FE["Dashboard Frontend · React
학생 뷰 6종 · 교수자 뷰 6종 · 반응형"] LX_WORKER["Queue Worker · 학습활동 이벤트 수집"] end LX_API --- LX_FE LX_API --> LX_WORKER subgraph "Storage" PG[(PostgreSQL)] MONGO[(MongoDB)] NAS[NAS] end LX_WORKER --> PG LX_WORKER --> MONGO LX_WORKER --> NAS

기존 LMS 위에 구축

Canvas LMS + LearningX(자사 확장 레이어) 위에 대시보드를 새로 구축. 기존 아키텍처와 컨벤션을 존중하면서 새로운 모듈을 통합하는 방식.

듀얼 롤 대시보드

한 사용자가 학생이면서 교수(TA)일 수 있으므로, 하나의 페이지에서 역할별로 독립된 뷰와 데이터를 제공. 상단 탭으로 역할 전환 시 각 탭이 독립된 API 호출과 필터 상태를 가짐.

이벤트 기반 데이터 수집

Canvas의 학습활동 변경(과제 제출, 성적 입력 등)을 Queue Worker가 감지하여 대시보드 전용 테이블에 반영. PM2로 워커 프로세스를 관리.


what i built

대시보드 API 설계 및 구현

학생용 할 일 목록 API — 미완료 학습활동을 마감일 기준 정렬. D-Day 계산, 과목별 필터링, 완료 시 자동 제거
타임라인 API — 4개월 범위의 학습활동을 날짜별로 그룹핑. 학생/교수 공통 사용, 과목 필터 지원
과목별 학습현황 API — 남은 할 일 개수, 과목 유형(온/오프/블렌디드) 집계
교수자 전용 API — 채점 필요 과제 수, 학습 부진자 목록(종합지수, 출석률 등), 미열람 게시물
사용자 설정 개인화 — 필터/보기 옵션, 영역별 접기/펴기 상태를 계정 단위로 서버에 저장. 기존 로컬스토리지 방식에서 전환하여 기기 간 설정 동기화. 설정 변경이 빈번하므로 throttle + debounce를 조합하여 불필요한 API 호출을 최소화

프론트엔드 뷰 구현

기존 레거시(jQuery/Blade 기반)에서 React + TypeScript 함수형 컴포넌트로 전면 전환하여 새로운 대시보드 프론트엔드를 구축
학생 뷰 6종 — 할 일(D-Day, 완료 시 자동 제거), 타임라인(월간 스크롤, today 이동), 과목카드(남은 할 일 배지), 공지(안읽음 필터)
교수자 뷰 6종 — 채점 현황(배지), 학습 부진자 리스트(종합지수/출석/과제/공지/게시판/성적 6개 컬럼), 평가(미제출), 게시물(미열람)
역할 전환 탭, 학기 선택, 과목 유형 필터, 한국어/영문 로컬라이징, 모바일 반응형(테이블→카드 변환)

challenges

다수 API 호출 관리와 역할별 데이터 로딩 최적화

상황 대시보드 하나의 화면에서 학기 정보, 과목 목록, 할 일, 타임라인, 공지, 게시물, 학습 부진자, 평가 현황 등 다수의 API를 호출해야 했다. 여기에 학생/교수 역할에 따라 필요한 데이터가 완전히 달랐고, 학기 선택이나 필터 변경 시 관련 데이터만 다시 가져와야 했다.

사고 역할별 메인 섹션(학생 뷰, 교수 뷰, 과목별 뷰)을 React.lazy로 코드 스플리팅하여 현재 선택된 역할의 코드만 로드. 각 역할 탭이 독립된 필터 상태와 API 호출을 가지도록 분리하고, 학기 변경 시 이전 필터가 남아있는 문제를 방지하기 위해 필터 초기화 로직을 추가. 사용자 설정(필터, 접기/펴기)은 서버에 계정 단위로 저장하여 기기 간 동기화.

결과 역할 전환 시 불필요한 데이터 로딩 없이 즉각적으로 뷰가 전환되고, 필요한 데이터만 효율적으로 호출하는 구조를 확보.

신입 개발자로서의 첫 대규모 프로젝트

상황 입사 직후 받은 첫 대규모 프로젝트였다. 대규모 기존 코드베이스를 파악하면서 동시에 12종의 뷰와 API를 3개월 안에 개발해야 했고, 기획자/QA와의 협업, Jira 기반 이슈 관리, 코드 리뷰 프로세스 등 실무 워크플로우도 처음 경험했다.

사고 처음에는 기존 코드를 이해하는 데 시간이 많이 걸렸지만, 기존 컨벤션과 패턴을 먼저 파악하고 그에 맞춰 개발하는 것이 결국 더 빠르다는 걸 깨달았다. QA에서 2주간 132건의 이슈가 쏟아졌을 때는 카테고리별로 분류하고 우선순위(데이터 정합성 → 기능 → UX)를 정해 체계적으로 처리하는 방법을 배웠다.

배운 점 프로젝트 종료 후 Post-Mortem을 작성하면서 "잘한 점 / 아쉬운 점 / 개선할 점"을 정리하는 습관을 들였다. 이 회고 습관이 이후 프로젝트에서의 의사결정 품질을 높이는 기반이 됐고, "개발자가 괜찮다고 생각하는 것"과 "사용자가 자연스럽다고 느끼는 것"의 간극을 체감한 경험이었다.

LTI iframe 환경에서의 UX 제약 극복

상황 마이페이지는 Canvas LMS의 Global LTI로 동작하기 때문에 iframe 안에서 렌더링된다. 이로 인해 일반적인 웹 페이지에서는 당연한 동작들 — 콘텐츠 높이에 맞는 프레임 리사이즈, 모달/팝업의 화면 중앙 배치, 스크롤 위치 동기화 등이 자동으로 처리되지 않았다.

사고 iframe과 부모 창(LMS) 사이의 통신이 필요했다. window.parent.postMessage를 활용하여 대시보드의 콘텐츠 높이가 변경될 때마다 부모 프레임에 알려 iframe 크기를 동적으로 조정하고, 모달이 열릴 때 부모 창의 스크롤 위치를 기반으로 팝업을 화면 중앙에 배치했다. 마이페이지가 LMS 내부에 직접 삽입되는 경우와 외부 수강 관리 시스템에서 별도로 임베딩하는 경우, 부모 창의 DOM 구조가 달라서 메시지를 수신하는 측의 처리 방식을 각각 대응해야 했다.

결과 iframe 안에서도 일반 페이지와 동일한 수준의 UX를 제공. 사용자가 iframe 안에 있다는 것을 느끼지 못하는 자연스러운 경험을 확보.


retrospective

모든 것이 처음이었다. 대규모 코드베이스, 실무 협업, QA 프로세스, 배포까지 — 모든 것을 동시에 흡수해야 했다. 힘들었지만 그만큼 짧은 시간에 밀도 있게 성장할 수 있었다.

사용자의 눈으로 보기. 132건의 QA 이슈를 처리하면서, 내가 "괜찮다"고 생각한 것과 실제 사용자가 "자연스럽다"고 느끼는 것 사이의 간극을 몸으로 배웠다. 이 감각은 이후 모든 프로젝트에서 기준이 됐다.

회고하는 습관. 프로젝트가 끝난 뒤 처음으로 Post-Mortem을 작성했다. 잘한 것과 못한 것을 솔직하게 정리하는 과정 자체가 다음 프로젝트를 더 잘하게 만드는 가장 확실한 방법이라는 걸 느꼈다.