Case Study · 12

교육 포털 연동 1주 — 딥링크 진입, 시험·성적표, 녹화 차단

외부 교육 포털에서 링크 하나로 로그인·로비를 건너뛰고 바로 강의실로 들어오는 진입 경로를 1주 만에 앱에 붙였습니다. 웹에만 있던 라이브 시험·모의고사·1:1 라이브·성적표를 네이티브로 옮겼고, 성적표 PDF는 웹 생성기와 바이트 단위로 같게 맞췄습니다. 기기 녹화 차단은 녹화 화면의 검은 픽셀 비율을 0%에서 99.7%로 만들면서도 앱 안 화면 공유는 그대로 살렸고, 이후 일반 회의에도 같은 방식으로 확장했습니다.

Headline Outcome
0%0%

기기 화면 녹화 결과의 검은 픽셀 비율 (차단 적용 전후)

Product
MeetMate (Mobile)
Role
Frontend Engineer
Period
2026.09.09 — 2026.09.15
Stack
React Native 0.77 Deep Linking jsPDF FLAG_SECURE
Implementation AI-paired (Claude agent)
  • 기기 녹화 화면 검은 픽셀 **0% → 99.7%** — Android는 `FLAG_SECURE`, iOS는 캡처를 감지하면 전체를 덮는 가림막으로 막았습니다. 내가 화면을 공유하는 동안에만 차단을 풀어 앱 안 화면 공유는 그대로 살렸습니다(공유 중 40.5%, 회의 화면이 정상으로 보이는 상태).
  • 성적표 PDF가 웹 생성기와 **바이트 단위로 동등** — 네이티브 표로 만들던 방식을 버리고 웹 jsPDF 레이아웃을 수정 없이 옮겼습니다. 같은 실데이터를 넣었을 때 생성 시각과 ID만 빼면 결과가 같다는 것을 테스트로 확인합니다.
  • 로그인·로비 **0단계 진입** — 포털 딥링크 하나로 바로 장치 확인 화면에 들어갑니다. 외부 세션은 메모리에만 두고 기존 일반 로그인은 건드리지 않으며, 끝나면 원래 로그인으로 복원합니다(파서 퍼징·세션 수명·401 정책 테스트 66건).
  • 웹에만 있던 **라이브 시험·모의고사·1:1 라이브**를 네이티브로 옮김 — 60초 임시 저장, 문항별 개별 타이머, 만료 시 자동 제출을 넣었고, 시트를 접어도 종료 감시가 멈추지 않도록 감시를 서비스 수명에 묶었습니다.
  • 화면 공유 화질 설정 **5fps 고정 → 5/15/30 실제 반영** — 프로세스가 분리된 iOS 브로드캐스트 확장에는 App Group으로 값을 넘겼습니다. 학생 화면의 강사 타일에는 보는 사람 이름이 무작위 위치에 잠깐 나타나는 유출 추적 워터마크를 넣었습니다.

배경

입시 강의를 운영하는 파트너 교육 포털이 강의실로 MeetMate를 쓰기로 했습니다. 웹에는 포털 전용 기능(라이브 시험, 모의고사, 성적표, 1:1 라이브 대기열)이 이미 있었지만, 모바일 앱에는 하나도 없었습니다. 학생이 포털에서 강의를 누르면 앱에서는 로그인 화면부터 다시 시작해야 했습니다.

요구는 두 가지가 더 있었습니다. 강의 자료가 유출되지 않도록 휴대폰 녹화를 막아야 했고, 그러면서도 강사가 앱에서 화면을 공유하는 기능은 남겨야 했습니다. 문제는 기기 녹화와 앱 화면 공유가 OS에서는 같은 캡처 경로를 쓴다는 점이었습니다.

접근

먼저 진입 경로를 만들었습니다. 포털 딥링크는 엄격하게 검증해서, 우리 링크가 아니면 무시하고 내용이 어긋나면 실패 화면을 보여 줍니다. 외부 세션은 영속 저장소가 아니라 메모리에만 주입했습니다. 기존 일반 로그인은 그대로 두고, 세션이 끝나면 스냅샷으로 복원합니다. 외부 세션에서 401이 나도 일반 로그인을 지우지 않습니다. 콜드 스타트에서는 초기 URL을 앱 준비 조건에 포함했고, 이미 회의 중일 때 들어온 링크는 무시합니다.

시험·모의고사는 웹 계약을 그대로 옮기되, 규칙은 순수 함수로 분리했습니다. 시트 병합, 택1 과목, 제출 페이로드, 정오표 같은 규칙은 UI 없이 테스트할 수 있게 만들었습니다. 60초 임시 저장, 문항이 보일 때 시작해 백그라운드에서 멈추고 돌아오면 이어지는 개별 타이머, 만료 시 자동 제출은 서비스 계층에 두었습니다. 처음에는 종료 감시를 시트 컴포넌트에 넣었는데, 시트를 접은 채로 시간이 끝나자 타이머가 00:00:00에서 멈췄습니다. 그래서 감시를 서비스 수명으로 옮겼습니다.

성적표는 처음에 네이티브 표로 그렸습니다. 하지만 “웹과 같은 파일이어야 한다”는 결정에 따라 웹의 jsPDF 레이아웃을 손대지 않고 옮겼습니다. Hermes에 없는 atob/btoa를 폴리필로 채우고, 폰트는 한 번 내려받아 캐시합니다. 같은 실데이터로 바이트 동등성을 검증한 뒤 네이티브 표는 지웠습니다.

화면 녹화를 막는 기능과 화면 공유 기능은 OS 입장에서는 같은 행위입니다. 그래서 둘을 따로 설계하지 않고, 차단 여부를 한 곳에서 결정하도록 했습니다.

녹화 차단은 플랫폼마다 제공하는 수단이 달랐습니다. Android는 창에 FLAG_SECURE를 거는데, 플래그가 창 단위라서 회전처럼 액티비티가 다시 만들어지면 다시 걸어야 합니다. 이걸 빠뜨리면 화면을 한 번 돌리는 것만으로 구멍이 생깁니다. iOS에는 녹화를 막는 API가 없어서, 캡처를 감지하면 앱 전체를 가림막으로 덮습니다. 최종 차단 여부는 “정책상 차단이고 내가 공유 중이 아닐 때” 한 곳에서 계산하고, 공유를 시작하기 직전에 풀었다가 공유가 끝나거나 실패하면 다시 겁니다. 에뮬레이터 screencap으로 검은 픽셀 비율을 단계별로 측정했습니다. 일반 0%, 차단 99.7%, 내 공유 중 40.5%, 공유 종료 99.7%, 세션 종료 0%였습니다.

나머지 차이도 정리했습니다. 저장만 되고 실제 송출은 5fps에 고정돼 있던 화면 공유 화질 설정은 실제 송출 상한으로 연결했습니다. 강사·조교의 셀프뷰는 칠판 글씨가 뒤집히지 않도록 미러링에서 뺐고, 학생 화면의 강사 타일에는 워터마크를 넣었습니다. 포털 분기는 컴포넌트마다 흩어 두지 않고 정책 필드와 순수 함수 하나에 모았습니다. 1주 뒤 일반 회의에도 녹화 차단이 필요해졌을 때 정책 값을 on/off에서 범위(off, 회의 중, 항상)로 바꾸는 것만으로 확장할 수 있었습니다. 일반 회의는 로비 0%, 장치 확인 100%, 회의실 99.7%, 퇴장 후 로비 0%로 측정됐습니다.

결과

  • 포털 딥링크로 로그인·로비 없이 장치 확인 화면에 바로 진입. 외부 세션과 일반 로그인을 분리(테스트 66건).
  • 라이브 시험·설문·파일 첨부·모의고사 전용 화면·1:1 라이브 대기열을 네이티브로 이식하고 학생·강사 전 흐름을 실측.
  • 성적표 PDF가 웹 생성기와 바이트 동등.
  • 기기 녹화 차단 0% → 99.7% 검은 픽셀, 앱 화면 공유는 유지. 일반 회의로 확장.
  • 화면 공유 화질 5/15/30fps 실제 반영, 셀프뷰 미러링 예외, 강사 타일 유출 추적 워터마크.
  • 테스트 1,883 → 2,030건, tsc 신규 오류 0.

배운 점

같은 결과를 내야 할 때는 다시 만들지 말고 그대로 옮겨라. 성적표를 네이티브 표로 다시 그리는 동안 웹과 다른 부분은 계속 생겼고, 웹 생성기를 그대로 옮기고 나서야 “웹과 같다”를 바이트 비교 테스트로 증명할 수 있게 됐습니다.

또, 파트너 전용 요구를 컴포넌트 분기로 흩어 두지 않고 정책 필드 하나로 모아 둔 것이 일주일 뒤에 효과를 냈습니다. 일반 회의에도 녹화 차단이 필요해졌을 때, 기능을 새로 만들지 않고 정책 값의 범위를 넓히는 것만으로 끝났습니다.