hswork.win

"영상 다 봤어요"를 어떻게 믿을 것인가

2026-09-13 · 학습엔진

이 서비스의 존재 이유가 이 한 줄입니다. 강사가 영상을 올리고 학생이 "다 봤다"고 하면, 그 말을 시스템이 대신 확인해 주는 것. 나머지 기능은 전부 여기에 딸린 것들입니다.

그래서 이 부분만은 대충 넘어갈 수 없었습니다. 실제로 개발 기간의 상당 부분을 여기에 썼습니다. 구체적인 판정 기준과 수치는 서비스의 핵심이라 적지 않겠지만, 부딪힌 문제와 그때 세운 원칙은 남길 만하다고 생각합니다.

브라우저가 하는 말은 증거가 아니다

처음에 떠올리는 방식은 단순합니다. 영상이 끝나면 브라우저가 서버에 "다 봤다"고 알려 주고, 서버가 기록합니다.

이건 전혀 안 됩니다. 브라우저에서 도는 코드는 사용자가 볼 수 있고 고칠 수도 있습니다. 개발자 도구를 열어 그 요청을 손으로 한 번 보내면 이수가 찍힙니다. 코드를 읽을 줄 몰라도 재생 속도를 올리거나 진행 막대를 끝으로 끌면 몇 초 만에 같은 결과가 납니다.

그래서 원칙을 하나 세우고 시작했습니다. 브라우저에서 하는 일은 전부 편의이고 판정 권한은 100퍼센트 서버에 있다. 앞으로 감기를 막고 배속을 제한하는 건 학생이 실수로 건너뛰지 않게 돕는 장치입니다. 그걸 뚫고 들어온 요청은 서버에서 걸러야 합니다.

브라우저가 보낸 구간을 서버가 검증하는 흐름
브라우저가 보낸 구간을 서버가 검증하는 흐름

시간을 더하지 않고 구간을 모은다

두 번째로 바꾼 생각은 진도를 숫자 하나로 세지 않는 것이었습니다.

재생된 시간을 그냥 더하면, 같은 30초를 스무 번 반복 재생한 학생이 10분을 본 것으로 계산됩니다. 그래서 누적 시간 대신 실제로 재생된 구간을 모으기로 했습니다. 영상의 몇 분 몇 초부터 몇 분 몇 초까지 재생되었는지를 조각으로 쌓고 겹치는 조각은 하나로 합칩니다. 같은 데를 열 번 봐도 그 구간은 한 번으로 셉니다.

이렇게 하면 부수 효과가 여럿 생깁니다. 뒤로 감아 복습하는 건 아무 제약 없이 허용해도 됩니다. 어차피 이미 본 구간이라 진도가 더 오르지 않습니다. 중간에 창을 닫았다 다시 들어와도 앞서 본 구간이 그대로 남아 있습니다.

시간이 흐른 만큼만 인정한다

구간을 모으는 방식에도 구멍이 있습니다. 위조한 요청으로 "0초부터 끝까지 봤다"는 조각을 한 번에 보내면 그만입니다.

그래서 서버가 요청을 받을 때 자기 시계를 봅니다. 직전 보고를 받은 시각부터 지금까지 실제로 흐른 시간보다 더 긴 구간이 들어오면, 그 조각은 버립니다. 사람이 앉아서 보는 이상 재생된 길이가 흐른 시간을 넘어설 수 없습니다. 클라이언트가 함께 보내는 경과 시간은 참고만 하고 믿지 않습니다. 클라이언트가 보낸 값으로 클라이언트를 검증하면 검증이 아닙니다.

같은 이유로 한 사람이 동시에 두 기기에서 재생하는 것도 막았습니다. 노트북과 휴대폰에서 같은 영상을 동시에 틀어 두면 두 배 속도로 진도가 찹니다.

시간을 더하는 방식과 구간을 합치는 방식의 차이
시간을 더하는 방식과 구간을 합치는 방식의 차이

진짜 함정은 정직한 학생 쪽에 있었다

부정 시청을 막는 건 생각보다 수월했습니다. 정작 오래 잡아먹은 건 평범하게 보는 학생에게서 나오는 오작동이었습니다.

하나는 탭을 잠시 다른 데로 옮겼을 때입니다. 브라우저는 보이지 않는 탭의 타이머를 늦춥니다. 그러면 짧은 간격으로 들어오던 보고가 갑자기 뚝 끊겼다가 한참 뒤에 들어옵니다. 그 사이를 "그동안 봤다"로 이어 버리면 화면을 보지도 않은 시간이 통째로 진도가 됩니다. 조각 사이가 부자연스럽게 벌어지면 잇지 않고 끊도록 고쳤습니다.

되돌리기에서도 비슷한 문제가 있었습니다. 뒤로 감은 직후의 조각을 되감기 전 위치와 그대로 이어 붙이면, 건너뛴 구간이 통째로 본 것으로 기록됩니다. 되돌리는 순간 조각을 끊고 그 표본은 버리게 했습니다. 되돌린 기록이 진도 기준선을 밀어 올리지 않는지도 따로 확인했습니다. 여기가 뚫리면 "한 번 튕겨도 두 번째엔 통과"가 됩니다.

세 번째는 영상 끝부분입니다. 영상 플레이어는 마지막 1~2초를 정확히 보고해 주지 않는 경우가 잦습니다. 끝까지 본 학생이 완료 처리가 안 되는 민원은 반드시 생기므로, 끝자락에 약간의 여유를 두고 완료로 봅니다.

그런데 이 보정이 예상 못 한 곳에서 사고를 냈습니다. 영상 한 편에 맞춰 만든 여유를 회차나 강좌처럼 여러 영상을 묶은 단위에도 그대로 적용해 버린 겁니다. 그러자 긴 영상 하나를 다 보고 짧은 영상을 절반만 본 학생의 회차 진도가 완료로 표시됐습니다. 전체 길이 대비 비율로는 끝자락에 걸린 겁니다. 화면에는 "완료"인데 목록에는 미완료 영상이 남아 있었습니다. 강사가 보면 바로 이상한 상태입니다.

고친 방향은 단순합니다. 묶음 진도는 영상별 완료 여부를 먼저 보고 계산하고 미완료가 하나라도 있으면 완료로 표시되지 않게 했습니다. 하나의 규칙을 다른 층위에 그대로 가져다 쓸 때는 그 규칙이 왜 생겼는지부터 다시 봐야 한다는 걸 여기서 배웠습니다.

화면에서 만난 것들

로직과 별개로 화면에서도 몇 번 걸렸습니다.

플레이어가 첫 진입에서 아예 붙지 않는 버그가 있었습니다. 플레이어가 들어갈 자리가 로딩이 끝난 뒤에야 화면에 생기는데, 로딩이 끝나기 전에 플레이어를 붙이라고 부르고 있었습니다. 붙일 자리가 없으니 조용히 아무 일도 일어나지 않고 끝났습니다. 화면은 멀쩡해 보여서 눈으로는 잡히지 않았습니다. 브라우저에서 스크립트가 실제로 삽입됐는지를 확인하고 나서야 원인을 알았습니다.

또 하나는 학교나 학원의 네트워크였습니다. 이런 곳에서는 영상 서비스를 막아 두는 경우가 있는데, 거부 응답을 주지 않고 그냥 침묵하는 방식이 많습니다. 그러면 브라우저는 실패 신호를 받지 못하고 학생 화면에는 안내도 없이 빈 자리만 남습니다. 일정 시간이 지나도 준비가 안 되면 "네트워크나 차단 프로그램을 확인해 주세요"를 띄우도록 바꿨습니다.

주소는 숨길 수 없었다

영상 주소를 숨겨 달라는 요청을 받고 조사한 적이 있습니다. 결론부터 적으면 대부분 불가능했습니다.

브라우저가 영상을 받아와야 재생이 되니 주소는 원리상 드러납니다. 개발자 도구를 막는 방법도 널리 알려진 것들이 있지만 우회가 전부 한 줄이고, 무엇보다 정상적으로 쓰는 사람을 오작동으로 막아 버립니다. 브라우저 확대를 개발자 도구로 착각해 학습을 중단시키면 그게 더 큰 사고입니다.

그래서 목표를 차단에서 억제와 추적으로 바꿨습니다. 마음먹고 가져가려는 사람은 못 막지만, 우클릭 한 번으로 주소가 손에 들어오는 상황은 막을 수 있습니다. 그리고 재생 화면에 수강생을 알아볼 수 있는 옅은 표식을 띄웁니다. 화면 녹화로 유출되면 누구 화면에서 나갔는지가 남습니다. 표식은 한자리에 두지 않고 주기적으로 위치를 옮깁니다. 고정해 두면 그 부분만 잘라내면 그만입니다.

강사 본인이 자기 영상을 확인하는 미리보기 화면에는 이 중 아무것도 적용하지 않습니다. 자기 영상 주소를 자기가 못 복사하게 막으면 그건 보호가 아니라 방해입니다.

두 방향을 같이 보기

이 기능을 만들면서 제일 많이 한 일은 "이걸 어떻게 뚫을까"를 먼저 떠올려 보는 것이었습니다. 코드를 쓰는 시간보다 그쪽이 길었습니다. 방어를 하나 붙일 때마다 학생 입장에서 우회 방법을 떠올려 보고, 떠오르면 그게 다음 할 일이 됐습니다.

동시에 반대쪽도 계속 봐야 했습니다. 부정을 막으려고 조인 규칙이 정직하게 보는 학생을 걸고 넘어지면, 그때부터 그건 민원입니다. 이 두 가지를 같이 보는 게 이 엔진에서 제일 어려운 부분이었습니다. 지금도 계속 다듬고 있습니다.

← 개발 기록 목록