[#4] [안전 교육 시스템] 영상 시청을 그냥 두지 않았다, 진행 상태를 이렇게 제어했다 | 연재 3편
Series 03 · Video Progress Control
영상 시청을 그냥 두지 않았다,
진행 상태를 이렇게 제어했다
온라인 안전교육에서 영상을 붙이는 것 자체는 어렵지 않다. 어려운 건
이 영상을 정말 이수했다고 볼 수 있는가를 시스템으로
판단하는 일이다. 이 프로젝트에서도 핵심은 재생기를 보여주는 게 아니라,
사용자가 영상을 어떤 순서로 보고, 어디까지 봤고, 언제 완료로 인정할지까지
제어하는 데 있었다.
이번 편에서는 영상 시청을 그냥 열어두지 않고, 왜 순차 시청을 넣었는지, 진행
상태를 왜 `localStorage`에 저장했는지, 왜 직무나 언어가 바뀌면 기록을
초기화했는지, 그리고 완료 판정은 어떤 기준으로 처리했는지를 구현 관점에서
정리한다.

실제 플레이어 화면과 진행률 UI 캡처가 들어가면 이 글의 주제가 가장
선명해진다.
영상 제어의 핵심은 UX보다 상태였다. 사용자가 무엇을 봤는지, 어느 순간
완료로 볼지를 끝까지 잡고 있어야 했다.
왜 순차 시청을 넣었나
안전교육 영상을 여러 개 붙여두고 자유롭게 보게 두면 사용성은 좋아 보인다.
하지만 실제 운영을 생각하면 가장 뒤에 있는 영상만 보거나, 필요한 부분만
골라서 보고 곧바로 이수 처리를 기대하는 흐름이 생긴다. 이 프로젝트에서는
그걸 허용하고 싶지 않았다.
그래서 `isVideoDisabled()`로 현재 영상보다 앞선 영상들이 모두 완료됐는지
확인하고, 완료되지 않았다면 다음 영상을 잠그는 방식으로 순차 시청을
걸었다. 첫 번째 영상만 항상 열고, 이후 영상은 이전 단계가 끝났을 때만
열리도록 했다.
진행 상태는 1초 단위로 누적했다
플레이어에서는 재생이 시작되면 `setInterval`로 1초마다 현재 재생 시간을
읽는다. 이 값을 `onProgress()`로 상위 컴포넌트에 보내고, 상위에서는
`updateVideoProgress()`로 `videoId`별 진행 상태를 갱신한다. 이 구조 덕분에
각 영상의 시청 시간과 마지막 위치, 완료 여부를 하나의 객체로 관리할 수
있었다.
핵심은 플레이어 상태와 화면 상태를 분리한 점이다. 재생 자체는 YouTube
IFrame API가 담당하지만, 진행률 계산과 완료 판정은 React 상태 쪽에서 계속
들고 간다. 그래야 다음 단계 잠금이나 전체 완료 여부도 같은 데이터에서
파생시킬 수 있다.

플레이어 시간 추적, 상태 저장, 완료 판정을 하나의 흐름으로 설명하는
다이어그램이 잘 어울린다.
완료 기준은 90%와 95%를 나눠서 다뤘다
이 프로젝트에서는 사용자가 영상의 90% 이상을 보면 다음으로 넘어갈 수
있도록 `canSkip`을 열고, 95% 이상을 보면 사실상 완료로 인정하는 식으로
기준을 나눴다. 이 분리는 꽤 중요했다.
왜냐하면 사용자는 끝부분 몇 초를 남기고도 이미 핵심 내용을 다 들었을 수
있고, 시스템은 너무 빡빡하면 불필요하게 사람을 붙잡는다. 반대로 너무
느슨하면 의미 없는 통과가 된다. 그래서 “다음으로 넘어갈 수 있는 기준”과
“완료로 볼 수 있는 기준”을 조금 다르게 뒀다.
그리고 영상이 실제로 끝나면 `handleVideoComplete()`가 실행돼 완료 상태를
확정하고, 상위 훅에서는 해당 영상의 시청 시간을 영상 전체 길이로 채우면서
최종 완료 상태를 저장한다.
진행 상태는 localStorage에 작업별로 저장했다
사용자가 교육 도중 페이지를 나가거나 다시 접속하는 상황은 충분히
현실적이다. 그래서 `useVideoWatching`에서는
`video_progress_${workDescription}` 형태의 키로 진행 상태를
`localStorage`에 저장하고, 진입 시 `restoreProgress()`로 다시 복구한다.
이 구조 덕분에 사용자는 중간부터 다시 이어볼 수 있다. 하지만 여기서도 그냥
저장만 하면 안 된다. 작업 종류가 바뀌면 이전 위험과 다른 교육을 받아야
하므로 기존 기록을 지워야 한다. 실제로 작업 종류 변경 시에는 이전
`workDescription` 기준의 진행 상태를 삭제하고, 새 작업에 맞는 상태로 다시
시작하도록 만들었다.
다국어 지원에서도 같은 원칙이 적용된다. 언어가 바뀌면 다른 영상과 다른
이해 맥락이 되므로 진행 상태를 초기화했다. 사용성보다 이수의 정확도를 더
우선한 선택이었다.
전체 완료 여부도 별도 상태로 계산했다
각 영상이 끝났다고 해서 교육 전체가 끝나는 건 아니다. 그래서 훅 안에서는
`videos.every(video => videoProgress[video.id]?.completed === true)`
방식으로 전체 완료 여부를 계산해 `allVideosCompleted` 상태를 유지한다.
이 값은 이후 단계로도 이어진다. 영상이 모두 끝나야 퀴즈가 열리고, 퀴즈를
통과해야 동의와 서명 단계로 넘어갈 수 있기 때문이다. 결국 영상 시청 제어는
하나의 독립 기능이 아니라, 전체 이수 흐름을 여는 첫 번째 관문 역할을 하고
있었다.

복구, 저장, 초기화 흐름을 캡처와 함께 보여주면 구현 포인트가 더
명확해진다.
구현 포인트를 짧게 정리하면
- 다음 영상을 열기 전에 이전 영상 완료 여부를 검사해 순차 시청을 강제했다.
- 플레이어 현재 시간을 1초 단위로 읽어 진행 상태를 계속 업데이트했다.
- 90%는 넘어가기 기준, 95%는 완료 기준처럼 판정 임계값을 분리했다.
- 진행 상태는 `localStorage`에 작업별로 저장하고, 진입 시 복구했다.
- 작업이나 언어가 바뀌면 이전 시청 기록은 초기화해 이수 정확도를 지켰다.
다음 글에서는
다음 편에서는 퀴즈, 안전서약서, 전자서명, 이수증 발급까지 이어지는 마지막
완료 흐름을 다뤄보려고 한다. 사용자의 이해도를 어떻게 확인했고, 마지막
동의와 제출은 어떻게 닫았는지, 그리고 이수증 다운로드까지 어떤 식으로
구현했는지를 정리할 생각이다.



