"뉴스 기사를 가리키면 ClipDeck이 제목, 필자, 본문만 건져 내. 광고도, 관련 기사도, 쿠키 배너도 없이. 이 lesson에서 Mozilla의 Readability.js를 content script에 붙여서 '기사 통째로 저장' 을 버튼 하나로 만들 거야."
Readability.js가 뭐냐면
github.com/mozilla/readability는 Firefox의 Reader Mode를 돌리는 바로 그 JavaScript library 야. 엔진이 같고, Apache-2.0 라이선스고, 쓰는 데 Firefox가 필요하지도 않아. 생긴 건 이래:
- 넣는 것: 복사해 둔
document.document를 그냥 넘기면 안 돼 — Readability는 받은 tree를 헤집어 놓거든. user가 읽고 있는 페이지를 헤집을 순 없잖아. - 나오는 것:
{ title, byline, dir, content, textContent, length, excerpt, siteName, lang }.content는 뽑아낸 HTML이지 sanitizer가 보증한 결과가 아니고,textContent는 순수 텍스트야. - 값: 최소화해서 50 KB 쯤. content script에 넣기 충분해. 보통 기사 하나 파싱에 50–100 ms 쯤 더 들고.
파일로 넣을까, 묶을까
ClipDeck 이랑 Readability를 같이 내보내는 방법은 둘 다 말이 돼:
- 파일로 넣기: GitHub release에서
Readability.js를 받아clipdeck/vendor/Readability.js에 두고,content_scripts.js배열에서content.js보다 앞에 적어. 그러면 library가 전역Readability를 만들어 줘. 단순하고, 눈으로 확인하기도 쉽고. - 묶기:
npm install @mozilla/readability하고 source에서 import 한 다음 esbuild 나 Rollup으로 content.js 하나로 묶어. 길게 보면 더 깔끔해. tree-shaking 이나 TypeScript를 쓸 거면 이 길밖에 없고.
ClipDeck v1은 파일로 넣고 갈 거야. 묶는 쪽으로 옮기는 건 Track 8에서 다뤄.
복사해서 넘기는 요령
정해진 주문처럼 외워 두면 돼:
const documentClone = document.cloneNode(true);
const reader = new Readability(documentClone);
const article = reader.parse();
document.cloneNode(true)가 깊은 복사본을 만들어. Readability는 내비게이션, 사이드바, 푸터, 댓글, 광고처럼 기사 본문이 아니라고 판단한 부분을 복사본에서 물리적으로 뜯어내. user의 페이지는 손끝 하나 안 닿고.
결과 읽기
자주 쓰는 항목들이야:
title— 정리된 기사 제목.document.title뒤에 흔히 붙는 사이트 이름이 떨어져 나가 있는 경우가 많아.byline— 필자. 최선을 다해 찾는 정도라 작은 블로그에선 null 인 일이 잦아.content— 뽑아낸 HTML. Readability는 HTML sanitizer가 아니야. Extension page를 포함해 어디서든innerHTML로 그리기 전에 DOMPurify처럼 유지보수되는 sanitizer를 거쳐.textContent—content에서 HTML 태그를 걷어 낸 순수 텍스트야. 깔끔한 텍스트만 저장할 거면 ClipDeck은 이걸 쓰고, 제목과 문단 구조를 살리고 싶으면content를 써.length— 추출한 본문의 글자 수를 담아. 추출이 제대로 됐나 가늠할 때 쓸모 있어 (너무 짧으면 십중팔구 실패한 거야).excerpt— 첫 문단 정도. 미리보기에 좋아.siteName,lang,dir— 부가 정보.
reader.parse()가 null을 돌려주면 Readability가 이 페이지는 기사 모양이 아니라고 판단한 거야 (너무 짧거나, 너무 어수선하거나, 본문이라고 할 만한 게 없거나). 그럴 땐 선택 영역이 있으면 그걸로 대신하고, 없으면 페이지 제목이랑 URL만 챙겨.
ClipDeck의 '기사 통째로 저장'
선택 영역 저장이랑은 따로 불러:
- popup 버튼: "이 페이지의 기사 통째로 저장".
- 남는 조합이 있으면 단축키도 하나.
contexts: ['page']를 준 우클릭 메뉴: "이 페이지를 ClipDeck에 저장".
handler가 content script 한테 {type: 'readArticle'}를 보내면, content script가 Readability를 돌려서 기사 object를 돌려줘. 그럼 SW가 그 텍스트랑 isArticle: true 표시, URL과 제목을 담아 clip을 만들어. 선택 영역 clip 이랑 모양은 같고 본문만 훨씬 긴 거지.
속도 얘기
Readability는 대부분의 페이지에서 수십 밀리초면 끝나. 아주 큰 것들 (긴 Wikipedia 문서, 오래된 포럼 스레드)은 수백 ms까지 가고. content script의 main thread에서 그냥 돌려도 user는 못 느껴. 파싱하는 동안에도 페이지가 반응해야 한다면 Web Worker로 넘길 수도 있는데, 클릭 한 번에 한 번 도는 일에는 과해.
get → modify → set을 surface 둘이 동시에 돌리면 한쪽 변경이 사라질 수 있어. Service worker를 sole writer로 두고 mutation queue 하나로 직렬화하거나 record를 독립 key로 저장해. 한 번의 set은 그 안의 key를 함께 적용하지만, 앞뒤 read-modify-write 전체가 atomic인 건 아니야.