배경
LMS 웹은 라이브 테스트를 화상 강의실(MeetMate) 팝업 창에서 진행합니다. 두 앱은 저장소도 배포 주기도 다르고, 서로 소통하는 방법은 브라우저 창 간 postMessage뿐입니다. 6월 말 “강의실에서 제출하자마자 성적표를 보여달라”는 요구가 들어와, 강의실 창이 LMS 창에 성적표 PDF를 요청하면 LMS가 PDF를 만들어 회신하는 구조를 먼저 붙였습니다.
문제는 검증 방식이었습니다. 처음엔 회신 대상이 와일드카드였고, 이후 보강한 검증도 허용 도메인 목록에 의존했습니다. 7월 6일 운영 환경 주소 하나가 목록에서 빠져 있었고, 그 결과 성적표가 에러도 없이 무한 로딩되는 장애가 났습니다.
접근
장애 당일엔 누락된 주소를 추가해 복구하고, 다음 날 검증을 도메인이 아니라 창의 신원으로 판정하도록 다시 설계했습니다. 판정은 세 겹입니다. ① LMS가 직접 연 강의실 창을 WeakSet에 기억해 두고 메시지의 source와 같은 창인지 비교합니다. 도메인과 무관하고, 중간에 리다이렉트가 일어나도 유지됩니다. ② LMS를 새로고침하면 ①의 기억이 사라지므로, 보낸 창의 opener가 지금 이 창인지를 봅니다. 브라우저가 보증하는 참조라 위장할 수 없습니다. ③ 기존 허용 목록은 마지막 안전망으로 남겼습니다.
검토 과정에서 ‘같은 상위 도메인이면 허용’ 방식도 따져봤지만, 공용 접미사(co.uk 같은) 판별이 불완전하면 엉뚱한 도메인이 통과할 수 있어서 쓰지 않았습니다. 기존에 통과하던 origin은 모두 ③에 남겨, 새 판정이 이전 판정의 순수 상위집합이 되게 했습니다. 바꿨다고 기존 환경이 막히는 일은 구조적으로 생기지 않습니다.
허용 목록은 ‘빠뜨리면 조용히 깨지는’ 구조였습니다. 신원 기반 판정으로 바꾼 뒤엔 환경이 늘어도 고칠 코드가 없습니다.
회신 쪽은 targetOrigin을 와일드카드에서 요청을 보낸 창의 origin으로 고정했습니다. 그동안 검증 없이 받던 ‘제출 완료’ 알림과 ‘성적표 보기로 이동’ 메시지 수신부도 같은 진입점을 거치게 묶었습니다. 메시지는 제출 완료, 성적표 열기, PDF 요청·데이터·에러 5종으로 정리했고, 제출 완료를 받으면 목록을 스로틀된 재조회로 갱신해 새로고침 없이 ‘응시완료’로 바뀝니다.
7월 말에는 서버 로그에서 같은 답안이 0.06초 간격으로 두 번 저장된 사고가 확인됐습니다. 답안 제출 함수에 테스트 단위 단일비행 가드를 넣어, 이미 진행 중인 제출이 있으면 새 요청을 보내지 않고 같은 Promise를 돌려줍니다. 끝나면 바로 풀려서 정상적인 재제출은 막지 않습니다. OMR·설문·첨부 세 종류 응시 화면의 제출 버튼에는 동기 ref 기반 재진입 가드를 따로 걸어 더블탭도 막았습니다. 강의실 앱 쪽과 같은 규칙으로 맞췄습니다.
마지막으로 창 결합 구조 자체의 한계를 문서로 남겼습니다. LMS 창이 닫혀 있으면 실패하고, 두 앱의 배포 버전이 맞아야 동작하며, 응급 조치로 PDF 생성기가 두 저장소에 중복돼 있다는 점입니다. 마감 후 통계 처리 시점에 서버가 학생별 PDF를 미리 만들어 두는 안을, 필요한 데이터·권한·실패 케이스까지 적어 서버팀 협의 문서로 만들었습니다.
결과
- 창 간 메시지 검증: 도메인 목록 1겹 → 신원 기반 3중 레이어. 새 배포 환경 추가 시 코드 수정 0, 기존 통과 origin 전부 보존.
- 성적표 PDF 회신 대상: 와일드카드 → 요청한 창의 origin. 검증 없던 수신부 2종도 같은 정책으로 통일.
- 답안 중복 제출(0.06초 간격) → 테스트 단위 단일비행 + 제출 화면 3종 재진입 가드.
- 강의실 제출 → LMS 목록 실시간 응시완료 반영, 강의실 팝업에서 바로 성적표 PDF 확인.
- 성적표 PDF 서버 사전 생성안을 근거 문서로 정리해 서버팀에 전달.
배운 점
postMessage 검증을 *어디서 왔는가(도메인)*로 하면 환경이 하나 늘 때마다 깨질 준비를 하는 셈이었습니다. *누가 보냈는가(내가 연 창인가)*로 바꾸니 판정이 더 엄격해지면서도 운영 부담은 오히려 줄었습니다. 그리고 새 판정을 기존 판정의 상위집합으로 설계해 두면, 보안을 강화하는 변경이 회귀를 만들지 않는다는 것을 확인했습니다.
또 하나, 응급 복구와 구조 개선은 따로 가야 했습니다. 장애 당일엔 한 줄로 막고, 다음 날 원인 유형 자체를 없애고, 남은 구조적 결합은 문서로 넘겼습니다. 세 단계를 섞지 않은 덕에 운영을 멈추지 않고 정리할 수 있었습니다.