본문 바로가기
C.W.K.
Stream
Lesson 01 of 07 · published

DOM Tools 개관 — Real-world toolkit

~10 min · dom, overview, tooling, concepts

Level 0Extension 입덕
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"앞의 여섯 track은 순순히 협조하는 페이지를 상대로 뼈대를 세운 거야. Track 7은 반격하는 페이지를 상대해 — 무거운 framework, 시도 때도 없이 다시 그려지는 화면, handler를 붙이기도 전에 내용을 갈아치우는 SPA. 이 lesson에서 연장통 지도를 그리고, 다음 여섯 lesson에서 하나씩 갈아."

DOM 도구가 뭐냐면

Track 3에서 배운 기본을 넘어서, content script와 페이지의 진짜 구조를 이어 주는 것들이야. 크게 세 갈래로 갈려:

  • 뽑아내기 — 엉망인 markup에서 깔끔하게 정리된 데이터를 건져 내는 일. Readability, schema.org 파싱, OpenGraph, 직접 만든 CSS-selector 모음 같은 것들.
  • 담아두기 — user가 지금 보고 있는 걸 그대로 기록하는 일. 선택한 텍스트, screenshot, 스크롤 위치, viewport 크기.
  • 고치기 — user를 위해 페이지를 손보는 일. 입력창 채우기, 가짜 event 쏘기, overlay 끼워 넣기, 원하는 데로 스크롤하기.

ClipDeck v1은 뽑아내기 (긴 clip을 위한 본문 추출) 랑 담아두기 (선택 영역 + screenshot)를 써. 고치기는 v2에서 붙일 거야 — 저장해 둔 조각을 페이지에 다시 넣어 주는 기능이지. Track 7에서 셋을 다 다루는 건 결국 쓰는 근육이 같아서야.

'현실 페이지' 라서 붙는 조정

이 도구들이 교과서 DOM 예제랑 다른 건, 현실의 페이지가 테스트 코드는 절대 안 하는 방식으로 extension 한테 적대적이기 때문이야:

  • Framework가 앞을 가려. React, Vue, Svelte가 DOM의 주인이야. state가 바뀌면 component를 다시 그리면서, 아빠가 끼워 넣은 overlay를 그대로 밀어 버려.
  • 가짜 event는 안 통해. Framework의 입력창은 자기 setter로 state를 따라가. input.value에 직접 값을 넣어도 framework는 입력이 바뀐 줄 몰라.
  • 늦게 불러오는 것들. 기사 이미지, 댓글, 관련 글은 user가 스크롤해야 그때 붙어. DOMContentLoaded 시점에 읽는 코드는 그 뒤에 오는 걸 전부 놓쳐.
  • CSS collision. Manifest로 선언한 extension CSS가 page CSP 때문에 거절되는 건 아니지만, light DOM에 둔 UI는 page selector와 부딪힐 수 있어. 격리가 필요하면 Shadow DOM이나 충분히 좁은 class 이름을 써.
  • Shadow DOM. 열려 있는 shadow root는 뒤질 수 있는데 닫힌 건 못 해. 요즘 site 들이 component 마다 이걸 점점 더 쓰고, 그럼 querySelector가 아무것도 못 찾아.

'user 신뢰' 라서 붙는 조정

페이지는 user의 공간이야. 뭘 바꾸든 그걸 존중해야 해:

  • 확정 전에 보여 주기. 뭘 할 건지 먼저 보여 주고, user가 확인하거나 물러날 수 있게 해. Track 7 Lesson 6에서 제대로 다뤄.
  • 잠깐은 되돌릴 수 있게. 되돌리기 힘든 일 (clip 삭제, 입력창 채우기) 에는 5 초짜리 undo를 붙여. 그 뒤엔 확정하고.
  • 흔적을 남기기. 입력창을 건드렸으면 작은 표시 (테두리 색, 옆에 라벨)를 남겨서 ClipDeck이 한 일이라는 걸 user가 기억하게 해.
  • 놀래키지 않기. 자동으로 채우지 말고, 자동으로 저장하지 말고, user가 시작하지 않은 일은 아무것도 하지 마.

Bundle을 어떻게 할까

도구 중엔 남의 library에서 오는 것도 있어 (Readability 같은 거). 가져오는 방법이 셋이야:

  • 그냥 넣기 — source 파일을 extension에 떨궈 놓고 같이 commit 해. 단순하고, 눈으로 확인하기 쉽고, 평범한 JS library 라면 build 단계도 필요 없어.
  • npm + bundler — 요즘 방식이야. esbuild, Rollup, Vite가 content.js 하나로 묶어 줘. ES module import를 쓰는 library 라면 이 길밖에 없고.
  • Classic file order — 정적 content_scripts에는 type: 'module' field가 없어. Classic file을 manifest 순서대로 load하거나 import를 bundle해. Runtime의 import(chrome.runtime.getURL(...)) 경로는 web-accessible-resource/CORS 조건이 더 붙으니 manifest module 대용품처럼 쓰면 안 돼.

ClipDeck v1은 Readability.js 파일 하나만 넣고 bundler는 안 써. 한 파일로 감당이 안 될 만큼 자랐을 때 어떻게 bundler로 넘어가는지는 Track 8 (packaging)에서 다룰 거야.

이 track에서 붙는 것

lesson 여섯 개에 도구 여섯 개야:

  • Lesson 2 — 기사 본문을 뽑는 Readability.js.
  • Lesson 3 — Selection과 Range API를 깊게 (toString 너머로).
  • Lesson 4 — 선택 영역 screenshot을 위한 chrome.tabs.captureVisibleTab + canvas 자르기.
  • Lesson 5 — React를 속이지 않고 입력창 채우는 요령.
  • Lesson 6 — 보여 주고 확인받는 패턴.
  • Lesson 7 — inline edit과 undo를 갖춘 ClipDeck의 Update와 Delete.

다 끝나면 ClipDeck이 이런 현실적인 흐름을 감당해. user가 긴 기사를 읽다가 세 문단을 잡고, Ctrl+Shift+K를 누르고, 뭐가 저장될지 미리 보고, 확인하고, 그다음 side panel에서 clip 제목을 고치고 아까 중복 저장한 걸 지워. 그동안 읽던 자리를 한 번도 안 잃고.

현실의 페이지는 반격해. Framework가 DOM을 쥐고 있고, 내용은 늦게 붙고, 어떤 건 shadow root 안에 숨어 있어. Track 7의 연장통은 그 적대감 속에서 살아남는 동작 모음이야 — 깔끔하게 뽑고, 정직하게 담고, 티 나게 고치고, 얌전하게 되돌리기.
이 track에 일부러 안 넣은 것. 페이지 통째로 렌더링하기 (PDF, MHTML), accessibility tree 훑기, web-component slot 짜맞추기, 복잡한 form 제출 조율하기. 하나하나가 자기 track이 될 만한 주제인데 ClipDeck v1 한테는 필요가 없어. 기준은 이거야 — 어떤 기능이 돌아가는 ClipDeck을 한 발짝도 못 밀어 주면서 lesson 30 분을 잡아먹는다면, 그건 미뤄.

Code

content.js — Track 7 도구들 위한 dispatcher 패턴·javascript
// content.js — 나중 lesson 의 도구 wire 하는 작은 dispatcher
const CD_TOOLS = {
  readability: null, // Lesson 2 가 할당
  selection: null,    // Lesson 3
  screenshot: null,   // Lesson 4 (SW 에 살아, 메시지 통해 호출)
  fillInput: null,    // Lesson 5
};

// 각 도구가 module load 시 자체 등록.
// content.js 가 얇은 coordinator 유지; 실제 logic 이 bundler (Track 8) 통해
// import 된 per-tool file 이나 옆 vendor 된 곳에 살아.

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message?.type === "tool") {
    const tool = CD_TOOLS[message.name];
    if (!tool) {
      sendResponse({ ok: false, reason: "unknown-tool" });
      return;
    }
    (async () => {
      try {
        const result = await tool(message.args);
        sendResponse({ ok: true, result });
      } catch (err) {
        sendResponse({ ok: false, reason: err.message });
      }
    })();
    return true;
  }
});

// 도구는 이렇게 자체 등록:
//   CD_TOOLS.selection = async (args) => { ... };
// Dispatcher 가 이름으로 route 하고 uniform 응답 shape 반환.

External links

Exercise

Code block의 dispatcher 뼈대를 clipdeck/content.js에 넣고 extension을 reload 해. Wikipedia 기사를 하나 열고 DevTools를 연 다음 console을 ClipDeck context로 바꿔. 거기서 chrome.runtime.sendMessage({type:'tool', name:'selection'}) 을 던져 봐 — 아직 도구를 하나도 안 붙였으니 {ok: false, reason: 'unknown-tool'} 이 돌아와야 정상이야. 이 track의 Lesson 2부터 6까지 가면서 각 lesson이 CD_TOOLS.<name> 에 자기를 등록하게 될 거야. 이 dispatcher를 두는 이유는 content.js를 얇게 유지하면서, 이미 있는 메시지 라우팅을 안 건드리고 새 도구를 계속 받아들이기 위해서고.
Hint
메시지가 Receiving end does not exist 라는 SDK 에러를 달고 돌아오면 이 페이지에 content script가 안 올라간 거야. content_scripts.matches가 이 URL을 포함하는지 확인해 봐 (범위를 좁혀 놨다면 chrome://extensions → ClipDeck → Site access도 같이). dispatcher는 async 도구가 sendResponse를 부를 수 있게 listener에서 true 를 돌려주고 있어. 나중에 async가 아닌 도구를 붙일 땐 그걸 기억해 둬. sync handler에 true 를 돌려주는 건 자리만 좀 낭비할 뿐 고장 나진 않아.

Progress

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

댓글 0

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

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