Case Study · 19

LMS 테스트·성적표 정합성 — 고객사 수정 요청 20건 triage + 숫자가 틀리던 화면 바로잡기

미응시 필터가 97명 중 7명만 보여주던 화면을 전원 노출로 바꾸고, 영역별 배점 합 168점 → 100점, 선택과목 응시 분포 5명 → 3명으로 실제 인원과 일치시켰습니다. 고객사 수정 요청 27슬라이드를 20건으로 분류해 위험도별로 처리하면서, 테스트 카드 상태·성적표 노출 판정을 목록과 상세가 같은 기준으로 보도록 정리한 3개월 작업입니다.

Headline Outcome
7명7명

제출현황 미응시 필터에 보이는 인원 (전체 미응시 97명 기준)

Product
LMS (Web)
Role
Frontend Engineer
Period
2026.06 — 2026.08
Stack
React 18 TypeScript Redux React Query v3
Implementation AI-paired (Claude agent)
  • 미응시 필터 **7명 → 97명** — 클라이언트가 이미 받은 첫 페이지만 거르던 상태 필터를 서버 측 필터+페이지네이션으로 전환하고 총 인원을 (대상 − 제출)로 보정.
  • 성적표 영역별 배점 합 **168점 → 100점** — 선택과목 조합마다 중복 포함되던 공통 문항을 문항번호 기준으로 한 번만 집계. 같은 이유로 응시 분포는 서버 집계값(3명 응시인데 5명) 대신 과목별 실제 제출 수로 다시 계산.
  • 목록 무한 재조회 **3~5초 주기 → 같은 응답당 1회** — 제출 시점에 멈춘 잔여 시간이 계속 양수로 내려와 타이머가 목록을 계속 다시 불러오던 문제를 절대 만료 시각 기준으로 바꾸고, 성적 처리 대기는 마감+10분 상한으로 묶음.
  • 강의 등록 중 취소 시 남던 **고아 테스트 → 0건** — 강의 등록 API 성공 뒤에만 테스트를 만드는 지연 생성으로 바꾸고, 부분 실패 시 만든 테스트와 신규 강의를 함께 롤백.
  • 고객사 수정 요청 27슬라이드 → **20건 분류**(UI 6 / 로직 14, 화상 앱 쪽 5건 분리) — 저위험 UI부터 일괄 처리하고 서버 의존 항목은 근거와 함께 서버팀에 넘겨, 3개월 동안 이 영역에서만 154커밋.

배경

학원용 LMS 웹입니다. 강사가 테스트를 등록하고, 학생은 라이브 강의실(MeetMate)이나 웹에서 응시하고, 마감 뒤 학생별 성적표 PDF를 받는 흐름이 핵심입니다. 6월 초 고객사에서 27슬라이드짜리 수정 요청이 들어왔는데, 실제로 열어보니 UI 요청과 숫자가 틀리는 버그가 섞여 있었습니다.

예를 들어 제출현황의 미응시 필터는 97명 중 7명만 보여줬고, 선택과목 응시 분포 그래프는 3명이 응시했는데 5명으로 그렸고, 국어 영역별 점수의 배점 합이 168점으로 나왔습니다. 학생 테스트 카드의 상태(응시하기 / 응시완료 / 응시종료 / 성적처리중 / 성적표 보기)도 목록과 상세가 서로 다른 기준으로 판정하고 있어, 목록에선 ‘성적표 보기’인데 들어가면 정오표가 나오는 일도 있었습니다.

접근

먼저 요청 27슬라이드를 20건으로 분류했습니다. 저위험 UI·CSS 6건, 로직·서버 결정이 따라오는 14건, 그리고 이 레포에서 손댈 수 없는 화상 앱 쪽 5건. UI 배치는 한 번에 끝내고, 로직 14건은 항목마다 코드 위치 → 서버 의존 여부 → 구현 → 실데이터 검증 순서로 돌렸습니다. 서버가 먼저 바뀌어야 하는 항목은 왜 클라이언트만으로는 정확할 수 없는지 근거를 붙여 서버팀에 넘겼습니다.

숫자 버그는 실제 응답을 보고 원인을 좁혔습니다. 미응시 필터는 클라이언트가 로드된 페이지만 걸러서 생긴 문제라 서버 필터로 옮겼습니다. 응시 분포는 서버 통계값이 +1씩 많이 내려오고 있어서, 과목별 실제 제출 수를 따로 조회해 다시 집계하고 서버 버그로 보고했습니다. 배점 합 168점은 선택과목 조합마다 공통 문항이 중복으로 들어간 탓이라, 문항별 분석 표와 같은 ‘문항번호 기준 중복 제거’를 영역별 점수에도 적용해 100점으로 맞췄습니다.

화면에 보이는 숫자가 틀리면 원인은 대개 같은 데이터를 두 군데서 다른 기준으로 세고 있는 것이었습니다. 고칠 때마다 ‘이 숫자를 누가 세는가’를 하나로 모았습니다.

카드 상태와 성적표 노출 판정은 목록과 상세가 같은 함수를 보도록 묶었습니다. 강사가 강의실을 강제로 종료한 경우와 응시 기간이 실제로 끝난 경우를 구분하는 판정을 따로 만들어, 기간이 끝나기 전 성적표가 먼저 보이던 문제를 막았습니다. 개별 타이머가 끝나면 새로고침 없이 ‘응시종료’로 바뀌도록 만료 시각을 상태 갱신 시점에 넣었는데, 이게 제출 완료 카드에서 3~5초마다 목록을 다시 부르는 루프를 만들었습니다. 그래서 응답을 받은 시각 기준의 절대 만료 시각으로 바꿔 같은 응답으로는 한 번만 갱신되게 했고, 성적 처리 대기는 마감 + 1분에 다시 조회하되 30초 간격 재시도, 마감 + 10분 상한을 걸었습니다.

강의 등록 화면에서는 테스트를 언제 만드는지를 바꿨습니다. 기존엔 테스트 설정 단계에서 바로 서버에 생성해서, 강의 등록을 취소하면 어디에도 연결되지 않은 테스트가 남았습니다. 강의 등록이 성공한 뒤에만 테스트 → 상세유형 → 문항 순서로 만들고, 중간에 실패하면 만든 것을 모두 롤백합니다. 이 과정에서 매 렌더마다 새 배열이 넘어가 생긴 무한 렌더 루프는 useMemo로 참조를 고정해 끊었습니다.

성적표 PDF 쪽은 jsPDF로 직접 그리는 생성기에 영어 절대평가 레이아웃(원점수·등급·최고점, 등급 비율)을 추가하고, 성적 데이터 관리 화면은 다시 들어왔을 때 예상 등급컷과 문항별 분석 표가 자동 복원되게 했습니다. 처음엔 저장해 둔 엑셀을 내려받아 파싱했는데, 서버가 문항별 분석 조회 API를 새로 만들어준 뒤 그쪽으로 옮기고 엑셀 파싱 경로는 지웠습니다. 두 소스는 Promise.allSettled로 따로 불러 한쪽이 실패해도 다른 쪽은 복원됩니다.

결과

  • 미응시 필터 노출 7명 → 97명 (전체 미응시 인원 전원).
  • 성적표 영역별 배점 합 168점 → 100점, 선택과목 응시 분포 5명 → 3명(실제 제출 인원과 일치), 응시현황 원그래프 100% 오표기 해소.
  • 학생 목록 무한 재조회 3~5초 주기 → 같은 응답당 1회, 성적 처리 대기 재조회는 마감+10분 상한.
  • 강의 등록 취소 시 남던 고아 테스트 0건 (지연 생성 + 부분 실패 롤백).
  • 테스트 카드 상태·성적표 노출 판정을 목록·상세가 같은 기준으로 판정 — ‘목록은 성적표, 상세는 정오표’ 불일치 해소.
  • 고객사 수정 요청 27슬라이드 → 20건 분류, 3개월간 이 영역 154커밋. 서버 측 집계 버그 2건은 근거와 함께 서버팀에 보고.

배운 점

숫자가 틀린 화면 대부분은 계산 실수가 아니라 세는 위치가 여러 군데인 것이었습니다. 클라이언트가 페이지 일부만 세거나, 서버 통계와 실제 제출 목록이 따로 놀거나, 목록과 상세가 다른 함수로 상태를 판정하는 식이었습니다. 고칠 때마다 실제 응답을 먼저 확인하고 ‘이 숫자의 출처를 하나로’를 기준으로 삼았더니, 같은 종류의 버그가 다른 화면에서 다시 나오지 않았습니다.

또 하나는 실시간 반영과 요청 폭주가 한 끗 차이라는 점입니다. 새로고침 없이 상태를 바꾸려고 넣은 만료 시각이 그대로 무한 재조회 루프가 됐습니다. 그 뒤로는 자동 갱신을 넣을 때마다 ‘같은 입력으로 몇 번 발화하는가’와 ‘언제 멈추는가’를 같이 정했습니다.