기능 시험이 모두 통과했는데 마지막 종료에서 오류가 났다. 빠르게 두 번 닫으면 첫 종료가 저장을 기다리는 동안 두 번째가 부모 창을 파괴할 수 있었다. 북극성의 종료 수정은 버튼을 잠깐 숨기는 수준보다 부모 창과 자식 탭, 저장과 커널의 수명을 맞추는 문제였다.

첫 종료는 창 파괴보다 먼저 시작된다

현재 close 이벤트는 preventDefault로 기본 종료를 보류한다. quitting이 이미 켜졌다면 후속 요청을 되돌리고 첫 절차가 계속하도록 둔다. 처음 요청에서 closing과quitting을 표시하고 타이머와 실행 중 제어기를 정리한다. 두 번째 요청이 별도 파괴 경로가 되지 않게 한 것이다.

기존 오류는 첫 요청이 초안과 탭 저장을 기다리는 구간에 나타났다. 이때 화면상 닫기를 눌렀다는 사건과 창이 실제로 사라져도 되는 시점은 다르다. 단순한 클릭 중복 제거와 달리 긴 비동기 정리를 하나의 종료 수명에 묶는 선택이었다. 사용자가 급해서 두 번 누르는 행동도 정상 입력 조건으로 다뤄야 했다.

호스트의 정리도 순서를 가진다

BrowserHost.dispose는 초안을 먼저 캡처하고 closed를 표시한 뒤 현재 브라우저 상태를 저장한다. 그 다음 driver를 분리하고 살아 있는 contents만 닫으며 로그인 보존기·다운로드 기록과 저장소 정리를 잇는다. 창의 모양만 사라진다고 내부 저장이 끝난 것은 아니다.

일반 close도 이미 종료한 호스트나 파괴된 부모 창이면 탭 제거를 수행하지 않는다. dispose에서는 파괴된 contents를 다시 닫지 않는다. 자식 destroyed 이벤트가 뒤늦게 부모 뷰에 접근하는 경로를 줄인 것이다. 탭 닫기와 앱 전체 종료가 같은 함수 이름 근처에 있어도 어떤 자료를 보존해야 하는지와 호출 시점은 다르다.

노트북 정리와 늦은 상태 전송도 고려한다

종료 경로는 각 프로젝트의 notebook.shutdown과flush를 호출한다. 실행 중이던 커널과 마지막 출력·소스의 수명이 창과 다르기 때문이다. 현재 코드가 어떤 비동기 작업을 묶고 언제 destroy를 호출하는지 실제로 읽어야 한다. 진행 문구만 보고 저장 성공을 추측하지 않는다.

특히 Promise.all의finally가 있다는 사실은 모든 실패 조건에서 저장 내구성을 보장한다는 뜻이 아니다. 한 작업이 거절하면 다른 정리의 완료나 실제 쓰기 성공은 별도 확인이 필요하다. 이 글에서 설명하는 성공 근거는 정상 종료 경쟁 시험의 범위다. 강제정전·디스크 오류·모든 커널 조합까지 안전하게 복구했다고 확대하지 않았다.

같은 회귀를 수정 전후에 남긴다

기존 close-lifecycle 시험은 독립 프로필에서 빠른 quit 두 번과 재시작을3회 반복한다. 매번 메인 프로세스 예외를 수집하고 탭 ID와 초안 내용을 정확히 비교한다. 마지막에는 일반 창 닫기도 확인해7개 결과를 남긴다. 단순히 프로세스가 종료됐다는 확인보다 보존까지 묻는 시험이다.

과거0.1.60은 같은 회귀에서 destroyed 오류로 실패했고0.1.61은 통과한 기록이 있다. 이 결과는 실제 패키지 실행과 지정한 초안·탭의 근거다. 외부 게시나 로그인 만료 시험으로 바꾸지 않는다. 수정 전 실패를 남겨야 무엇이 달라졌는지 설명할 수 있고 다른 기능 시험20개 통과와 종료 실패가 함께 있었던 사실도 사라지지 않는다.

닫힘과 저장됨은 사용자에게도 다르게 알려야 한다

종료 요청이 접수됐다는 응답은 다음 시작에서 모든 자료가 복원됐다는 결과가 아니다. 앱이 사라지는 순간보다 재시작 뒤 같은 탭과 원고를 확인하는 순간까지 근거를 이어야 한다. 사용자가 기다릴 이유도 내부 구현 용어보다 어떤 작업을 정리 중인지로 설명할 수 있다.

이번에는 원래 앱을 닫거나 시험을 재실행하지 않았다. 코드와 기록을 검토했고 합성 보존 모듈은 전후 투영만 비교한다. 내가 공유하려는 선택은 급한 두 번째 종료가 첫 저장을 끊지 않게 하고, 완료 주장을 실제 확인한 범위에 맞추는 것이다. 편한 종료 기능의 뒤에도 사용자의 작업을 지키는 순서가 필요했다.