화면을 읽기 시작한 뒤 사용자가 다른 페이지로 이동하면 예전 관찰이 늦게 도착할 수 있다. 마지막에 도착했다는 이유로 그것을 최신 자료로 저장하면 현재 화면 설명이 과거로 돌아간다. 나는 읽기 시작 시점의 정체성을 결과를 넣기 전까지 붙잡았다.

도착 시간이 최신성의 근거는 아니었다

A 페이지의 관찰이 느리고 B 페이지의 관찰이 빨리 끝날 수 있다. 두 응답을 공통 변수에 차례로 넣으면 나중에 온 A가 B를 덮는다. 자료가 정확히 수집됐어도 지금 화면과는 다른 내용이다. 비동기 처리에서 내용 정확성과 현재성은 별도의 질문이었다.

그래서 prepare 시작 시 documentEpoch와 URL, 해당 탭의 generation을 저장한다. current 함수는 페이지가 살아 있고 휴면이 아니며 그 값들이 아직 같은지 확인한다. 화면 변경 중에 수집한 결과를 받아들일 자격이 있는지 보는 검사다. 단순한 성공 응답만으로 캐시에 넣지 않는다.

문서 세대와 준비 세대는 다른 변경을 잡는다

문서 세대는 탐색이나 문서 교체를 구별한다. generation은 invalidate로 늘어나는 준비 계층의 세대다. 같은 주소로 돌아왔어도 예전 준비 요청이 현재 결과를 덮지 못하게 별도의 번호를 갖는다. URL 하나만 맞추는 검사보다 좁게 대상을 묶는다.

현재 내용의 revision도 함께 대조한다. URL이 같아도 페이지 안에서 본문과 버튼 상태가 달라질 수 있어서다. 주소·문서·준비 번호·내용 변화는 같은 값을 네 번 적은 것이 아니다. 서로 다른 방식의 변경을 구별하려는 단서들이며 어느 한 가지로 모두 대체할 수 없다.

비동기 경계를 지난 뒤 다시 본다

공개 주소 확인을 기다린 뒤 current를 보고, observe를 기다린 뒤에도 다시 본다. 관찰 응답의 URL과 contentRevision이 필요한 조건을 만족해야 한다. 마지막 revision 조회 뒤에도 내용과 현재성을 확인한 다음 캐시에 저장한다.

검사를 함수 시작에 한 번만 두면 기다리는 동안 발생한 변경이 빠진다. 나는 await 앞에서 확인한 상태가 await 뒤에도 같다고 가정하지 않았다. 다만 이 검사가 페이지를 원자적으로 잠그는 것은 아니다. 결과를 읽은 이후 실제 행동까지 모든 변경을 막는 기능은 별도 실행 계층에서 다뤄야 한다.

실패 안내도 과거 화면에서 오면 버린다

prepare의 catch도 current를 먼저 확인한다. 이미 다른 화면으로 바뀐 뒤 온 오류로 현재 준비 상태를 error로 덮지 않는다. 오래된 성공뿐 아니라 오래된 실패도 사용자의 지금 화면을 잘못 설명할 수 있다.

같은 탭의 pending Promise는 별도로 관리하고 끝날 때 자기 operation인지 확인해 지운다. 늦은 작업이 다른 준비의 대기 표식을 잘못 정리하지 않게 소유자를 대조하는 방식이다. 데이터 저장뿐 아니라 진행 중 표시와 정리에도 비동기 소유권을 생각해야 했다.

기존 시험의 범위와 새로 읽은 구현을 나눈다

현재 단위 시험은 revision이 바뀐 캐시를 peek이 돌려주지 않고 새 observe 뒤 내용을 바꾸는지 확인한다. documentEpoch가 늘어난 경우와 다른 탭, 허용되지 않는 페이지의 제거도 본다. 이 근거는 현재성을 확인하는 일부 경계를 시험한다.

이 글에서는 그 시험과 prepare의 비동기 경계별 검사를 읽었다. 모든 가능한 이벤트 순서를 실제 브라우저에서 새로 발생시킨 전수 경합 시험은 아니다. 특히 같은 주소 안의 모든 시각 변화가 revision에 완전히 잡힌다고 주장하지 않는다. 관찰 추적에 포함되는 변화와 실제 행동 직전 검사를 함께 봐야 한다.

빠른 화면에서도 늦은 결과의 주인을 남긴다

회사 현황 화면의 검색이나 거래처 전환에서도 비슷한 문제가 생긴다. 늦게 온 응답을 무조건 보여 주면 사용자가 방금 고른 대상과 다른 자료를 보게 된다. 나는 비동기 결과마다 어느 요청과 화면의 것인지 식별자를 남기는 것이 중요한 설계라고 본다.

북극성에서는 그 질문을 탭·문서·준비 세대에 적용했다. 오래된 결과를 버리는 것이 자료를 잃는 일이 아니라 현재 화면의 의미를 지키는 일인 경우가 있다. 취소할 수 없는 작업이라면 결과를 버리는 것만으로 끝나지 않지만, 읽기 준비에서는 현재 자료로 승격하지 않는 경계를 명확히 둘 수 있었다.