C.W.K.
Stream
Lesson 02 of 07 · published

Readability 추출 — Mozilla 의 Reader Mode 를 content script 에

~12 min · readability, article, extraction, mozilla

Level 0Extension 입덕
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"News article 가리키면 ClipDeck 이 headline, byline, body 빼냄 — 광고 없음, related post 없음, 쿠키 배너 없음. Lesson 2 가 Mozilla 의 Readability.js 를 content script 에 wire 해서 '전체 article 저장' 이 single 버튼 됨."

Readability.js, 솔직하게

github.com/mozilla/readability 가 Firefox 의 Reader Mode 동작시키는 JavaScript library. 같은 engine, MPL-licensed, 사용에 Firefox 필요 없음. Shape:

  • Input: cloned document. document 직접 전달 안 함 — Readability 가 받은 tree mutate, user 가 읽는 page mutate 원치 않음.
  • Output: { title, byline, dir, content, textContent, length, excerpt, siteName, lang }. content 가 sanitized HTML; textContent 가 plain text.
  • 비용: 약 50 KB minified. Content script 에 fit. Typical article parse 에 50–100 ms 정도 추가.

Vendor vs Bundle

ClipDeck 와 Readability ship 하는 두 합리적 방법:

  • Vendor: GitHub release 에서 Readability.js 다운, clipdeck/vendor/Readability.js 에 drop, content_scripts.js 배열에 content.js BEFORE 선언. Library 가 global Readability 노출. 단순. Auditable.
  • Bundle: npm install @mozilla/readability, TS/JS source 에서 import, esbuild/Rollup 로 single content.js 로 bundle. 장기적으로 더 깔끔. Tree-shaking 이나 TypeScript 원하면 필수.

ClipDeck v1 vendor. Track 8 에서 bundling 으로 이동.

Cloning trick

표준 주문:

const documentClone = document.cloneNode(true);
const reader = new Readability(documentClone);
const article = reader.parse();

document.cloneNode(true) 가 deep copy 생성. Readability 가 article 아니라고 판단하는 모든 것 (nav, sidebar, footer, comment, 광고) 을 그 node 들 clone 에서 물리적 제거. User 의 page 안 만짐.

Result read

흔한 field:

  • title — article title, 정리됨. 종종 document.title 이 가지는 site-name suffix 빠짐.
  • byline — author. Best-effort; 작은 블로그에선 종종 null.
  • content — sanitized HTML. Extension iframe 에 렌더 안전; user 의 page 에 inject 전엔 여전히 DOMPurify 같은 sanitizer 통과.
  • textContentcontent 의 plain text 버전. 깔끔한 text 저장 원하면 ClipDeck 에 이거; heading/paragraph 구조 보존 원하면 content.
  • lengthtextContent 의 글자 수. 추출 sanity-check 에 유용 (매우 짧음 = 아마 실패).
  • excerpt — 첫 문단 정도. Preview 에 좋음.
  • siteName, lang, dir — metadata.

reader.parse() 가 null 반환하면 Readability 가 page 가 article-shape 아니라고 결정 (너무 짧음, 너무 어수선, 명확한 main content 없음). Selection 있으면 그것으로 fallback, 아니면 page title + URL 로 context.

ClipDeck 'Save Full Article' action

Selection-save 와 별도 trigger 로 wire:

  • Popup 버튼: "Save full article from this page."
  • 키보드 단축키 option (free combo 있으면).
  • contexts: ['page'] 가진 context menu item: "Save this page to ClipDeck."

Handler 가 content script 에 {type: 'readArticle'} 로 메시지; content script 가 Readability 돌리고 article object 반환; SW 가 article 텍스트, flag isArticle: true, URL + title 가진 clip 만듬 — selection clip 과 같은 shape, body 만 더 많음.

Performance note

Readability 가 대부분 page 에서 십 밀리초 단위, 거대한 거 (긴 Wikipedia article, 보관된 forum thread) 엔 몇 백 ms 까지. Content script 의 main thread 안에서 돌리기; user 안 알아챔. Parse 동안 page responsive 유지 필요하면 Web Worker 에 offload 가능하지만, click 당 one-shot 엔 overkill.

Document clone. Clone 을 Readability 에 건넴. Structured article object 받음. User 의 page 가 parse 못 느끼고; ClipDeck 이 custom selector engineering 없이 깔끔한 텍스트 + title + byline 얻음.
Readability 가 맞는 도구 아닐 때. Forum thread, comment section, GitHub issue, multi-document SPA — 이것들이 의도된 구조 가지지만 Readability 가 평탄화 / 폐기. 이런 거엔 domain 별 custom selector (또는 user 가 한 번에 댓글 하나 선택 받아들이기) 가 더 잘 동작. Blog post / news article / long-form page 에 맞는 default.

Code

manifest.json — global 사용 가능하도록 content.js BEFORE Readability.js load·json
{
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "exclude_matches": [
        "https://accounts.google.com/*",
        "https://*.bank.com/*"
      ],
      "js": [
        "vendor/Readability.js",
        "content.js"
      ],
      "run_at": "document_idle"
    }
  ]
}
content.js — structured article 반환하는 readArticle handler·javascript
// content.js — readArticle 도구를 dispatcher 에 wire
CD_TOOLS.readability = async () => {
  if (typeof Readability !== "function") {
    throw new Error("Readability not loaded");
  }
  const documentClone = document.cloneNode(true);
  const reader = new Readability(documentClone);
  const article = reader.parse();
  if (!article) return { ok: false, reason: "not-article-shaped" };
  return {
    ok: true,
    article: {
      title: article.title,
      byline: article.byline,
      excerpt: article.excerpt,
      content: article.content,
      textContent: article.textContent,
      length: article.length,
      siteName: article.siteName,
      lang: article.lang,
    },
  };
};

// Dispatcher 의 `{type:'tool', name:'readability'}` envelope 안 통과 하는
// 직접 caller (popup, SW) 위한 top-level legacy handler.
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message?.type !== "readArticle") return;
  (async () => sendResponse(await CD_TOOLS.readability()))();
  return true;
});
background.js — context-menu save-article wiring·javascript
// background.js — popup 버튼이나 context menu trigger 하는 save-article flow
chrome.contextMenus.create({
  id: "clipdeck-save-article",
  title: "Save full article to ClipDeck",
  contexts: ["page"],
});

chrome.contextMenus.onClicked.addListener(async (info, tab) => {
  if (info.menuItemId !== "clipdeck-save-article" || !tab?.id) return;
  const response = await chrome.tabs.sendMessage(tab.id, { type: "readArticle" });
  if (!response?.ok) {
    console.warn("[ClipDeck SW] article extraction failed:", response?.reason);
    return;
  }
  const a = response.article;
  const clip = {
    id: crypto.randomUUID(),
    text: a.textContent,
    url: tab.url,
    title: a.title || tab.title || "",
    siteName: a.siteName,
    byline: a.byline,
    isArticle: true,
    savedAt: Date.now(),
  };
  const { clips = [] } = await chrome.storage.local.get("clips");
  await chrome.storage.local.set({ clips: [clip, ...clips] });
});

External links

Exercise

github.com/mozilla/readability 최신 release 에서 Readability.js 다운, clipdeck/vendor/Readability.js 에 배치. manifest.json 의 content_scripts.js 배열을 update 해서 vendor/Readability.js 를 content.js BEFORE load (첫 번째 code block). 두 번째 code block 의 dispatcher 등록을 content.js 에 추가. 세 번째 code block 의 context-menu wiring 을 background.js 에 추가. Reload. 실제 news article (대형 news site 나 Wikipedia article) 우클릭 → 'Save full article to ClipDeck'. Side panel 열기 — article 의 title 과 full body 텍스트 (article 에 따라 몇 KB 에서 십수 KB) 가진 새 clip 나타나야 함. Readability 가 잘 처리 안 하는 page (SPA dashboard) 에서 같은 시도 — 부드러운 fail-over 확인: 아무것도 안 저장되고 경고가 SW DevTools 로 로깅.
Hint
Content script console 에 Readability is not defined 나타나면 vendor file load 안 됨 — 경로 맞고 content.js BEFORE content_scripts.js 배열에 나타나는지 확인 (배열 순서 중요; Chrome 이 선언 순서로 inject). reader.parse() 가 항상 null 반환하면, page 가 iframe 안일 수도, OR page 가 Readability 의 heuristic (clean text 약 140 자 minimum length default 가짐) 에 너무 작음/짧음. 더 긴 page 에서 먼저 시도해 wiring 확인.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.