본문으로 건너뛰기

완성된 요리 말고 레시피만 보내자 — 학생 학습 기록 용량을 줄인 이야기

2026년 7월 28일
5 views
MathCanvas
성능 최적화
Vue
주니어 개발
회고

런 히스토리가 뭐냐면

제가 다니는 회사의 MathCanvas는 학생이 수학 교구를 화면에서 직접 만지며 공부하는 서비스입니다. 도형을 옮기고, 돌리고, 슬라이더를 움직이고 — 학생이 하는 이런 모든 조작을 하나하나 객체로 저장해서 서버로 보냅니다. 이 조작 기록을 저희는 "런 히스토리(run history)"라고 부릅니다.

왜 저장하냐면, 나중에 선생님이 학생이 어떻게 풀었는지 되감아 볼 수 있게 하기 위해서예요. 그래서 학습 중의 모든 순간이 기록으로 남습니다.

지금은 괜찮습니다. 그런데 앞으로가 문제였어요

솔직히 지금 당장은 아무 문제 없습니다. 고도화 버전이 아직 오픈 전이라, 실제 학급 단위 학생 데이터만큼의 부하를 받을 일이 없거든요. 문제는 "앞으로" 였습니다.

계산을 해봤습니다.

  • 학생 한 명이 얼마나 조작할지는 아무도 모릅니다. 의미 없이 이것저것 많이 만지면 그만큼 기록이 쌓이니까요.
  • 한 명당 1MB로만 잡아도 → 30명이면 30MB, 300명이면 300MB가 서버로 전송돼야 합니다.
  • 그런데 1MB는 아주 넉넉하게 잡은 값이고, 실제로는 보통 한 명당 3~5MB씩 나가고 있었습니다.
  • 여러 학급, 여러 학교가 겹치면 기가바이트 단위까지 서버에 부하를 줄 수 있다고 판단했습니다.

즉 지금 난 불이 아니라, 서비스가 커지면 반드시 날 불이었어요. 그래서 터지기 전에 미리 저장 방식을 바꾸기로 했습니다.

왜 이렇게 무거웠을까

원인은 저장 방식 자체였습니다. 원래는 조작이 일어날 때마다 관련 객체의 변경 전(before)과 후(after)를 다 들고 있었고, 변경이 생길 때마다 캔버스 전체 노드를 통째로 복사해서 저장했습니다.

조작 한 번에 화면 전체를 사진 찍어 저장하는 셈이라, 조작이 쌓일수록 용량이 눈덩이처럼 불어났습니다. 아래 그래프에서 주황색(개선 전)이 조작이 늘수록 가파르게 치솟는 게 그 모습입니다.

3D 교구가 많은 슬라이드
조작 20번 쌓였을 때 3,755KB → 9.3KB개선 전 (전체 복사)개선 후 (strip+델타+gzip)
0KB1.0MB2.0MB3.0MB4.0MB3579131820조작 기록 수 (누적)3.67 MB9 KB
일반 슬라이드
조작 33번 쌓였을 때 436KB → 16.4KB개선 전 (전체 복사)개선 후 (strip+델타+gzip)
0KB118KB235KB353KB471KB13681216202533조작 기록 수 (누적)436 KB16 KB

어떻게 고쳤나 ① — 델타: "바뀐 것만" 기록하기

첫 번째로 저장 방식을 "델타(delta)" 로 바꿨습니다. 이름은 어렵지만 원리는 단순합니다.

  • 처음 보는 노드(객체)는 한 번만 통째로 저장합니다.
  • 그다음부터는 직전과 비교해서 바뀐 노드만 기록합니다.

매 조작마다 전체를 복사하던 이전 방식과 비교하면 자원이 훨씬 절약됩니다. 예를 들어 교구를 한 번 옮겼을 뿐인데 기존엔 약 100KB를 저장하던 게, 델타로는 바뀐 값 약 40바이트만 저장하면 됩니다.

앱 안쪽(조작·되돌리기·재생 기능)은 한 줄도 안 바꿨습니다. 저장할 때만 줄여서 보내고, 불러올 때 원래대로 되돌리는 식이라, 선생님이 보는 재생 화면은 그대로예요.

어떻게 고쳤나 ② — 3D 교구: 완성된 요리 말고 레시피만

3D 교구가 특히 무거웠습니다. 3D 모형은 매번 "완성된 3D 데이터"까지 통째로 저장되고 있었는데, 이게 용량을 크게 잡아먹고 있었어요.

비유하자면 이렇습니다. 요리로 치면 레시피만 보내주면 되는데, 완성된 요리 전체를 같이 보내주고 있던 상황이었습니다.

  • 받는 쪽(재생 기능)은 어차피 레시피(위치·회전·슬라이더 값)만 읽어서 3D 모형을 처음부터 다시 만듭니다.
  • 저장돼 있던 "완성된 요리"는 한 번도 꺼내 쓰지 않았습니다. (코드를 전부 뒤져서 확인했어요.)

그래서 완성 데이터는 저장에서 제외했습니다. 어차피 안 쓰는 걸 창고에 넣고 있었던 거죠. 그리고 이렇게 줄이고 남은 데이터는 gzip으로 한 번 더 압축했습니다(zip 파일로 묶는 것과 같은 무손실 압축이라 내용은 그대로예요).

결과

상황개선 전개선 후줄어든 정도
3D 교구 많은 슬라이드 (조작 20번)3,755 KB9.3 KB405배
일반 슬라이드 (조작 33번)436 KB16.4 KB26.6배
  • 재생 품질은 그대로입니다. 저장할 때만 줄이고 불러올 때 원래대로 되돌리기 때문이에요.
  • 되돌린 결과가 원본과 정말 똑같은지 자동 테스트 27개로 확인했고, 선생님 재생 기능도 직접 눌러보며 검증했습니다. 전체 테스트 1,101개도 다 통과했습니다.
  • 옛날 방식으로 저장된 기록도 그대로 읽힙니다(하위호환).

하면서 배운 것

1. 지금 안 아프다고 나중에도 안 아픈 건 아니다. 당장은 멀쩡했지만, 사용자 수로 스케일을 계산해보니 손봐야 할 이유가 분명했습니다. "지금 문제없음"과 "나중에 문제없음"은 다른 이야기더라고요.

2. 가짜 데이터로 잰 효과는 믿으면 안 된다. 사실 처음엔 델타만 적용하고 테스트용 가짜 데이터로 효과를 쟀는데 "17배 줄어든다!" 나왔습니다. 그런데 실제 데이터로 재보니 겨우 1.1배였어요. 진짜 원인(3D 완성 데이터)은 실제 데이터를 뜯어보고 나서야 찾았습니다. 실제 데이터로 재보는 게 제일 중요했습니다.

3. "어떻게 더 줄이지?"보다 "이거 꼭 저장해야 하나?" 가장 크게 줄어든 건 압축 기술이 아니라, "완성된 요리는 애초에 안 써도 되는 데이터" 라는 걸 알아챘을 때였습니다. 줄이는 방법을 고민하기 전에, 그게 정말 필요한 데이터인지 먼저 묻는 게 더 큰 지렛대였어요.