"User의 page가 user context. ClipDeck이 손 뻗어 form 채우거나, 영역 highlight 하거나, DOM mutate 하는 순간 user가 일어날 거 보고 깔끔히 back out 할 자격. Lesson 6가 extension action을 놀람 대신 작은 contract로 만드는 preview-and-confirm overlay 패턴."
왜 preview가 필요한가
가만히 읽기만 하는 동작 — 선택 영역 읽기, clip 저장, 화면 찍기 — 에는 미리보기가 필요 없어. user가 시작했고, 결과는 side panel에 뜨고, 페이지는 그대로거든. 반대로 페이지를 건드리는 동작 — 입력창 채우기, 버튼 누르기, 창 닫기, 특정 자리로 스크롤 — 에서는 얘기가 뒤집혀:
- user는 extension이 뭘 하려는지 늘 알 수는 없어. 어느 버튼을 누를지, 무슨 값을 붙일지, 어디로 옮겨 갈지. 미리보기가 그걸 말로 붙여 주는 거야.
- User가 page를 특정 방식으로 set up 했을 수도 (form 반쯤 채움, mid-scroll). Preview가 그 work 만져지기 전 confirm 이나 back out 순간 줘.
- 현실의 페이지에는 extension 코드가 미리 헤아리지 못한 구석이 늘 있어. 뭘 할 건지 먼저 보여 주면, user가 확정 전에 '어 저건 아닌데' 를 잡아낼 수 있고.
마찰이 작음 (confirm 위해 click 하나나 Enter tap 하나). 신뢰 이득 거대.
Overlay 패턴
Content script에서 host page에 작은 overlay inject. Overlay가 action 기술, 있으면 target highlight, Confirm / Cancel 제공:
- 위치: viewport 우상단 floating, 또는 action target 근처 anchor. Never modal — user가 더 context 필요하면 page scroll 가능해야.
- Content: action의 한 줄 요약 ("이 input에 'service worker eviction' paste"), target element의 bordered preview, 버튼 둘.
- Lifecycle: SW 나 popup이 action 시작할 때 나타남. Confirm (action 돔), Cancel (action abort), 10 초 timeout (action default abort)에 dismiss.
키보드 affordance
Enter가 confirm. Escape가 cancel. Mount 시 Confirm 버튼 focus, 키보드 경로가 obvious. Default behavior가 90% user가 원하는 거 매칭 — 보통 Confirm — 하지만 user가 안 본 focus 된 버튼에 Enter 쳐서 ambush 느낌 절대 없어야.
Target highlighting
Action이 특정 element 영향 줄 때, 임시 border로 outline 해서 user가 만져질 것 보게:
function highlightElement(el) {
const original = el.style.outline;
el.style.outline = '2px solid #1a6bd6';
el.style.outlineOffset = '2px';
return () => { el.style.outline = original; };
}
Confirm / cancel 둘 다 unhighlight 가능하게 cleanup function 반환. Outline이 시각적으로 시끄럽지만 layout shift 안 함, user의 위치 mental model 보존.
Z-index 전쟁
실제 페이지들은 z-index를 두고 서로 싸워. 그러니 최댓값을 그냥 박아 버려:
overlay.style.zIndex = '2147483647'; // 2^31 - 1, 최대 int
그리고 page scroll 상관없이 overlay가 viewport 위에 머무르도록 position: fixed 사용. Fixed header 가진 page가 가끔 여전히 occlude. 그러면 자체 top-layer에 있고 z-index 완전 우회하는 :popover element (Chrome 114+) 안에 overlay 렌더.
Shadow root wrap
Page CSS가 overlay에 bleed 하는 거에 추가 안전 위해, overlay container에 shadow root attach 하고 markup + style 그 안에 두기:
const host = document.createElement('div');
host.style.cssText = 'position:fixed;top:0;right:0;z-index:2147483647;';
document.body.appendChild(host);
const shadow = host.attachShadow({ mode: 'closed' });
shadow.innerHTML = `<style>...</style><div>...</div>`;
Closed shadow root라 page script는 host.shadowRoot로 안쪽을 훑을 수 없어. 그래도 UI는 accessibility tree에 남고 composed event는 경계를 건널 수 있어. Closed mode는 캡슐화지 투명 망토가 아니야.
Confirmation Promise
전체 flow를 caller가 await 하는 Promise로 wrap:
const confirmed = await previewAndConfirm({
summary: 'Paste "' + clip.text.slice(0, 60) + '" into this input',
target: focusedElement,
timeoutMs: 10000,
});
if (confirmed) await fillInput(focusedElement, clip.text);
빠져나가는 길은 셋이야. 확인하면 true, 취소하면 false, 시간이 지나도 false. 부르는 쪽은 참/거짓만 보고 갈라지면 돼. event listener를 이리저리 엮을 일도, callback을 겹겹이 쌓을 일도 없어. 화면에 띄운 건 Promise가 풀리기 전에 먼저 걷어 내고.