크롬 확장에서 chrome.storage.local을 전역 마커로 쓰면 안 되는 이유

·

블로그 자동 발행용 크롬 확장을 직접 만들어 쓰고 있다. 초안을 로컬 서버에 올려두고 확장이 네이버 블로그 글쓰기 화면에서 제목·본문·태그를 자동으로 채운 뒤, 사람이 발행 버튼을 누르면 확장이 발행된 글의 URL을 감지해 서버에 보고하는 구조다. 그런데 같은 세션에서 세 번이나, 새 글을 발행할 때마다 등록된 URL이 바로 직전에 발행한 글의 것으로 나왔다.

증상 — 항상 정확히 한 칸씩 밀린다

새 글 A를 발행하면 등록되는 URL이 그 전에 발행했던 글 B의 URL이었다. 그다음 새 글 C를 발행하면 이번엔 A의 URL이 등록됐다. 무작위로 틀린 게 아니라, 매번 정확히 “직전 글”로 밀렸다는 규칙성이 있었다.

발행 순서:  B → A → C
등록된 URL: B는 정상 → A인데 B의 URL 등록됨 → C인데 A의 URL 등록됨

발행된 글 목록을 블로그의 공개 API로 다시 조회해서 실제 URL과 대조해보니, 매번 이 밀림이 재현됐다. 세 번 다 우연이라고 보기엔 패턴이 너무 일정했다.

구조 — chrome.storage.local 마커로 발행 완료를 감지한다

발행 버튼은 사람이 누른다. 확장이 그 버튼 클릭을 직접 가로채는 게 아니라, 클릭 이후 페이지가 이동하면서 확장 스크립트 자체가 다시 로드되는 걸 이용해 “발행이 끝났다”를 감지한다.

// 자동 채우기가 끝나면 chrome.storage.local에 표식을 남긴다
await chrome.storage.local.set({
  awaitingPublish: { title: draftTitle, at: Date.now() },
});

// 페이지가 새로 로드될 때마다(발행 후 이동한 새 페이지 포함) 이 함수가 실행된다
async function reportPublishedIfNeeded() {
  const { awaitingPublish } = await chrome.storage.local.get("awaitingPublish");
  if (!awaitingPublish) return;

  const url = publishedUrlNow(); // 현재 페이지 URL에서 글 주소 패턴을 뽑는다
  if (!url) return; // 아직 글쓰기 화면 등 — 다음 로드에서 다시 본다

  const title = await publishedTitle(); // 현재 페이지의 실제 글 제목
  if (!title.includes(awaitingPublish.title)) return; // 제목이 다르면 무시

  await chrome.storage.local.remove("awaitingPublish");
  await fetch(`${SERVER}/published`, { method: "POST", body: url });
}

제목이 다르면 무시하는 가드가 이미 있다. 그런데도 밀림이 재현됐다는 게 이번 문제의 핵심이다.

의심 지점 — storage는 탭 전역이다

chrome.storage.local은 확장 전체에서 공유되는 저장소다. 특정 탭에 묶여 있지 않다. 그래서 이런 시나리오가 가능하다.

1. 글 B를 발행하려고 자동 채우기 → awaitingPublish 마커에 title: "B" 저장
2. 글 B 발행 버튼 클릭 → 페이지 이동 → 새로 로드된 스크립트가 마커를 읽고,
   제목이 "B"와 일치 확인 → 정상 등록 → 마커 제거
3. 다음 세션에서 글 A를 발행하려고 자동 채우기 → awaitingPublish 마커에 title: "A" 저장
4. 그런데 이전에 열어뒀던 다른 탭(예: 글 B의 발행 결과 페이지)이 뭔가의 계기로
   다시 활성화되거나 재평가되면서, 그 탭의 URL(B의 URL)로 마커를 소비할 가능성

4번 시나리오는 아직 코드로 직접 재현해서 확정하지는 못했다. 다만 매번 “정확히 한 칸 밀린다”는 규칙성이, 탭 전역 저장소를 여러 탭이 순서 없이 건드릴 수 있는 구조와 맞아떨어진다는 정황은 있다.

지금까지 확인한 것과 못 한 것

확인한 것: 같은 세션에서 세 번 재현됐고, 매번 규칙적으로 한 칸씩 밀렸다. 제목 대조 가드는 이미 있는데도 막히지 않았다.

못 한 것: 정확한 재현 조건(어떤 탭 상태에서 어떤 순서로 페이지가 로드돼야 마커가 잘못 소비되는지)을 통제된 환경에서 재현하지 못했다. 그래서 위 4번 시나리오는 아직 가설이다.

당장은 근본 수정 대신 이중 확인으로 대응하고 있다. 확장이 보고한 URL을 그대로 믿지 않고, 발행 직후 블로그의 공개 목록 API로 실제 글 제목과 URL을 다시 대조해서 틀렸으면 그 자리에서 고친다. 근본 원인 조사는 별도로 진행 중이다 — 마커에 어떤 글을 기다리는지 식별자를 함께 저장해서, 그 식별자가 일치하는 요청만 소비하도록 바꾸는 방향을 검토하고 있다.

정리

chrome.storage.local은 탭이 아니라 확장 전체가 공유하는 저장소다. “발행 완료”처럼 한 번만 일어나야 하는 이벤트를 이 저장소의 단순한 플래그(예: awaitingPublish 하나)로 감지하면, 여러 탭이 같은 플래그를 두고 경쟁할 수 있다는 걸 이번에 실제로 겪었다. 제목 대조 같은 사후 검증 가드는 도움이 되지만 완전한 방어는 아니었다 — 애초에 “이 이벤트가 내가 기다리던 그 이벤트가 맞는가”를 식별자로 확인하는 게, 사후에 내용을 대조하는 것보다 안전하다는 걸 다시 확인했다.

자주 묻는 질문

왜 URL이 '직전 글'로 밀려 등록되나요?

확실하지 않습니다. 확장이 발행 완료를 감지하는 방식이 chrome.storage.local에 표식을 남기고 다음 페이지 로드에서 그 표식을 읽는 구조인데, 이 storage가 탭 전역이라 다른 탭(예: 이전 글이 열려 있던 탭)이 먼저 그 표식을 소비할 가능성을 의심하고 있습니다. 다만 재현 조건을 완전히 통제해서 확인하지는 못했습니다.

제목 대조 가드가 있는데도 왜 못 막나요?

발행 완료를 보고하기 전에 '지금 보고 있는 글의 제목이 방금 발행하려던 글의 제목과 같은지' 확인하는 코드가 있습니다. 그런데도 매번 정확히 한 칸 밀린 URL이 등록됐다는 건, 가드 자체가 틀렸다기보다는 가드를 통과하는 다른 경로가 있다는 뜻이라 원인 조사를 별도로 분리해뒀습니다.

당장은 어떻게 대응하고 있나요?

발행 직후 확장의 자동 등록 결과를 그대로 믿지 않고, 블로그의 공개 목록 API로 실제 URL을 다시 대조한 뒤 틀렸으면 수동으로 고치는 방식으로 운영하고 있습니다. 근본 수정 전까지는 이 이중 확인이 유일한 방어선입니다.

chrome.storage.local 말고 다른 저장소를 쓰면 해결되나요?

가능성 있는 방향입니다. 마커에 어떤 글을 기다리는지 식별자를 같이 저장해서, 그 식별자가 일치할 때만 소비하도록 바꾸면 탭 전역이라는 문제 자체는 남아도 오작동은 막을 수 있을 것으로 보입니다. 다만 아직 코드로 확정하지 않았습니다.

#Chrome Extension#race condition#chrome.storage#자동화#버그