๋ฐ˜์‘ํ˜•

ํฌ๋กฌ ํ™•์žฅ๊ธฐ๋Šฅ ์ฃผ์š” ๊ธฐ๋Šฅ


  1. ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค ์‚ฌ์šฉ์žํ™” : ๋ธŒ๋ผ์šฐ์ € ํˆด๋ฐ”์— ๋ฒ„ํŠผ ์ถ”๊ฐ€, ์‚ฌ์šฉ์ž ์ •์˜ ํŒ์—…/์˜ค๋ฒ„๋ ˆ์ด ์ƒ์„ฑ ๋“ฑ
  2. ์ฝ˜ํ…์ธ  ์Šคํฌ๋ฆฝํŠธ : ์›นํŽ˜์ด์ง€์— ์ง์ ‘ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฝ์ž…ํ•˜์—ฌ DOM ์กฐ์ž‘ ๊ฐ€๋Šฅ
  3. ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šคํฌ๋ฆฝํŠธ : ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋™์•ˆ ์ง€์†์ ์œผ๋กœ ์‹คํ–‰๋ผ์„œ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ ์ œ๊ณต
  4. API ์ ‘๊ทผ : ๋ถ๋งˆํฌ/ํƒญ/์œˆ๋„์šฐ ๊ด€๋ฆฌ, ํžˆ์Šคํ† ๋ฆฌ ์กฐ์ž‘ ๋“ฑ ๋ธŒ๋ผ์šฐ์ €์˜ ๋‹ค์–‘ํ•œ ์˜์—ญ ์ ‘๊ทผ ๊ฐ€๋Šฅ
    ๐Ÿ” ํƒญ ์ด๋™, capture, zoom ๋“ฑ ๋‹ค์–‘ํ•œ ๋ฐฉ์‹์œผ๋กœ ์ œ์–ด ๊ฐ€๋Šฅ
  5. ๋ฉ”์‹œ์ง• ์‹œ์Šคํ…œ : ํ™•์žฅ ๊ธฐ๋Šฅ์˜ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ ํ˜น์€ ์›นํŽ˜์ด์ง€์™€ ๋ฉ”์‹œ์ง€ ๊ตํ™˜ ๊ฐ€๋Šฅ
  6. ์›น ์š”์ฒญ ์กฐ์ž‘ : ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๊ฐ€๋กœ์ฑ„๊ธฐ, ๊ด‘๊ณ  ์ฐจ๋‹จ, ํ”„๋ผ์ด๋ฒ„์‹œ ๋ณดํ˜ธ, ์›น ํŠธ๋ž˜ํ”ฝ ๊ด€๋ฆฌ ๋“ฑ
    ๐Ÿ” chrome.webRequest API๋กœ http ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑ„๊ฑฐ๋‚˜ request header ์ˆ˜์ • ๊ฐ€๋Šฅ
  7. ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ์ €์žฅ : ๋กœ์ปฌ ์ €์žฅ์†Œ ํ˜น์€ Google ๊ณ„์ •๊ณผ ๋™๊ธฐํ™”๋œ ์ €์žฅ์†Œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ ์ €์žฅ/๊ด€๋ฆฌ
  8. ๋‹ค๊ตญ์–ด ์ง€์› : ๋‹ค์–‘ํ•œ ์–ธ์–ด๋กœ ํ™•์žฅ ๊ธฐ๋Šฅ ์ œ๊ณต

 

 

ํฌ๋กฌ ํ™•์žฅ๊ธฐ๋Šฅ ๊ตฌ์กฐ


๐Ÿ’ก ์›น ํŽ˜์ด์ง€์—์„œ ๋งˆ์šฐ์Šค ์šฐํด๋ฆญ ์‹œ ํ‘œ์‹œ๋˜๋Š” ๋ฉ”๋‰ด์— ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜๋„ ์žˆ๋‹ค

 

ํฌ๋กฌ ํ™•์žฅ๊ธฐ๋Šฅ ์•„ํ‚คํ…์ฒ˜ โ˜ ์ด๋ฏธ์ง€ ์ถœ์ฒ˜ - sunnyzhou

  1. ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šคํฌ๋ฆฝํŠธ : ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜์˜ ์„œ๋น„์Šค ์›Œ์ปค(Manifest V3 ๊ธฐ์ค€)
    • ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ์˜ ํ•ต์‹ฌ ๋กœ์ง์„ ์‹คํ–‰ํ•˜๋Š” ๊ณณ(๋ผ์ดํ”„์‚ฌ์ดํด ๊ด€๋ฆฌ/์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋“ฑ)
    • ๋ธŒ๋ผ์šฐ์ € ์‹œ์ž‘์‹œ ํ™œ์„ฑํ™”๋˜๊ณ , ๋Œ€๊ธฐ ์ƒํƒœ๋กœ ์žˆ๋‹ค๊ฐ€ ์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œ ์ž‘์—… ์ˆ˜ํ–‰
    • ๋Œ€๋ถ€๋ถ„์˜ Extension API ์‚ฌ์šฉ ๊ฐ€๋Šฅ
    • ์›นํŽ˜์ด์ง€์— ๋Œ€ํ•œ ์ ‘๊ทผ ๊ถŒํ•œ ์—†์Œ (DOM ์ ‘๊ทผ ๋ถˆ๊ฐ€)
  2. ์ฝ˜ํ…์ธ  ์Šคํฌ๋ฆฝํŠธ : ์›นํŽ˜์ด์ง€ ๋กœ๋“œ์‹œ ์Šคํฌ๋ฆฝํŠธ ์‚ฝ์ž…/์‹คํ–‰
    • ์›น ํŽ˜์ด์ง€์˜ ์ปจํ…์ŠคํŠธ์—์„œ ์‹คํ–‰ (์›นํŽ˜์ด์ง€์˜ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์Šค์ฝ”ํ”„/๋ณ€์ˆ˜ ๋“ฑ์€ ์ ‘๊ทผ ๋ถˆ๊ฐ€)
    • ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์ฃผ์ž…๋œ ์›น์‚ฌ์ดํŠธ์˜ DOM์— ์ ‘๊ทผํ•˜๊ฑฐ๋‚˜ ์ˆ˜์ • ๊ฐ€๋Šฅ
    • ์ผ๋ถ€ Extension API๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
    • window.postMessage ํ˜น์€ ์ปค์Šคํ…€ ์ด๋ฒคํŠธ๋ฅผ ํ†ตํ•ด ์›น ํŽ˜์ด์ง€์˜ ๋‹ค๋ฅธ ์Šคํฌ๋ฆฝํŠธ์™€ ํ†ต์‹  ๊ฐ€๋Šฅ
  3. ์˜ต์…˜ ํŽ˜์ด์ง€ : ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ์˜ ์„ค์ •์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋Š” HTML ํŽ˜์ด์ง€
  4. ํŒ์—… ํŽ˜์ด์ง€ : ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ ์•„์ด์ฝ˜์„ ํด๋ฆญํ•˜๋ฉด ๋‚˜ํƒ€๋‚˜๋Š” UI

 

 

์›น ์›Œ์ปค vs ์„œ๋น„์Šค ์›Œ์ปค


๐Ÿ’ก Manifest V3 ๊ธฐ์ค€ ํฌ๋กฌ ๋ถ€๊ฐ€๊ธฐ๋Šฅ์˜ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šคํฌ๋ฆฝํŠธ๋Š” ์„œ๋น„์Šค ์›Œ์ปค๋กœ ์ž‘๋™ํ•œ๋‹ค. ์„œ๋น„์Šค ์›Œ์ปค๋Š” ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž‘๋™ํ•˜๋ฉฐ, ํ•„์š”ํ•  ๋•Œ๋งŒ ํ™œ์„ฑํ™”๋ผ์„œ ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ์„ ์ตœ์†Œํ™”ํ•  ์ˆ˜ ์žˆ๋Š” ์žฅ์ ์ด ์žˆ๋‹ค.

 

๊ณตํ†ต์ 

  • ๋ณ„๋„ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰๋˜๋ฏ€๋กœ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š”๋‹ค
  • window, document ๊ฐ์ฒด์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†๋‹ค → DOM ์กฐ์ž‘ ๋ถˆ๊ฐ€
  • ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ œ๊ณตํ•˜๋Š” API์— ์ œํ•œ์ ์œผ๋กœ๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค
  • ๋™์ผ ์ถœ์ฒ˜ ์ •์ฑ… ์ค€์ˆ˜ (์›Œ์ปค๋ฅผ ์‹คํ–‰ํ•˜๋Š” ์ถœ์ฒ˜์™€ ๋‹ค๋ฅธ ์ถœ์ฒ˜์˜ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๋ถˆ๊ฐ€)

 

์ฐจ์ด์ 

  1. ์ƒ๋ช… ์ฃผ๊ธฐ
    • ์›น ์›Œ์ปค : ์›Œ์ปค๋ฅผ ์ƒ์„ฑํ•œ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์กด์žฌํ•  ๋•Œ๊นŒ์ง€ ํ™œ์„ฑํ™”(์›Œ์ปค๋ฅผ ์‹คํ–‰์ค‘์ธ ํƒญ์„ ๋‹ซ์œผ๋ฉด ์›Œ์ปค๋„ ์ข…๋ฃŒ)
    • ์„œ๋น„์Šค ์›Œ์ปค : ํ™œ์„ฑ ํƒญ๊ณผ ๊ด€๊ณ„์—†์ด ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•  ๋•Œ๋งˆ๋‹ค ํ™œ์„ฑํ™”๋ผ์„œ ์‹คํ–‰
  2. ์Šค์ฝ”ํ”„
    • ์›น ์›Œ์ปค : ํ•œ ํŽ˜์ด์ง€์—์„œ ์—ฌ๋Ÿฌ ์›น ์›Œ์ปค ์ƒ์„ฑ ๊ฐ€๋Šฅ
    • ์„œ๋น„์Šค ์›Œ์ปค : ๋‹จ์ผ ์„œ๋น„์Šค ์›Œ์ปค๊ฐ€ ์Šค์ฝ”ํ”„๋‚ด ๋ชจ๋“  ํ™œ์„ฑ ํƒญ ์ œ์–ด
  3. ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜
    • ์›น ์›Œ์ปค : ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์™€ ๋ฉ”์‹œ์ง€ ๊ธฐ๋ฐ˜์œผ๋กœ ํ†ต์‹  (postMessage, onmessage ๋ฉ”์„œ๋“œ ์‚ฌ์šฉ)
    • ์„œ๋น„์Šค ์›Œ์ปค : ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜. ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑŒ ๋•Œ๋‚˜ ํ‘ธ์‹œ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ›๋Š” ์ด๋ฒคํŠธ ๋“ฑ์— ๋ฐ˜์‘
  4. ์„ค์น˜ / ์—…๋ฐ์ดํŠธ
    • ์›น ์›Œ์ปค : ํ•„์š”์— ๋”ฐ๋ผ ์ฆ‰์‹œ ์ƒ์„ฑ/์‚ฌ์šฉ
    • ์„œ๋น„์Šค ์›Œ์ปค : ์„ค์น˜/ํ™œ์„ฑํ™” ๊ณผ์ • ํ•„์š”. ์ƒˆ ๋ฒ„์ „์˜ ์›Œ์ปค๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์—…๋ฐ์ดํŠธ ํ•„์š”
  5. ์‚ฌ์šฉ ๋ชฉ์ 
    • ์›น ์›Œ์ปค : ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ์„ฑ๋Šฅ ์ €ํ•˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋ฌด๊ฑฐ์šด ์—ฐ์‚ฐ/๋‹ค์ค‘ ์Šค๋ ˆ๋”ฉ์ด ํ•„์š”ํ•œ ์ž‘์—…์— ์‚ฌ์šฉ
    • ์„œ๋น„์Šค ์›Œ์ปค : ๋„คํŠธ์›Œํฌ ํ”„๋ก์‹œ, ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™”, ํ‘ธ์‹œ ์•Œ๋ฆผ, ์บ์‹ฑ, ์˜คํ”„๋ผ์ธ ์ฒ˜๋ฆฌ ๋“ฑ
      e.g. ์‚ฌ์šฉ์ž๊ฐ€ ์š”์ฒญํ–ˆ๋˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•ด๋‘๊ณ , ์ดํ›„ ์š”์ฒญ์„ ๋‹ค์‹œ ๋ฐ›์•˜์„ ๋•Œ ์ €์žฅํ•ด๋‘” ๋ฐ์ดํ„ฐ ์ œ๊ณต

 

 

chrome.runtime API (๋ฉ”์‹œ์ง• API)


๐Ÿ’ก chrome.runtime.onMessage ๋ฆฌ์Šค๋„ˆ๋Š” ๋ฐฑ๊ทธ๋ผ์šด๋“œ/์ฝ˜ํ…์ธ  ์Šคํฌ๋ฆฝํŠธ ์–‘์ชฝ์—์„œ ๋ชจ๋‘ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

 

[Sender] ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ ๋ฉ”์‹œ์ง€ ์ „๋‹ฌ (์‚ฌ์šฉ ์ปจํ…์ŠคํŠธ: ํด๋ผ์ด์–ธํŠธ, ๋ฐฑ๊ทธ๋ผ์šด๋“œ/์ฝ˜ํ…์ธ  ์Šคํฌ๋ฆฝํŠธ, ํŒ์—…, ์˜ต์…˜)

chrome.runtime.sendMessage(extensionId?, message, options?, callback?);

 

[Listener] ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋™์ผํ•œ ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ์˜ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ณด๋‚ธ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ 

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { ... });

 

[Listener] ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋‹ค๋ฅธ ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ์ด๋‚˜ ์›น ํŽ˜์ด์ง€์—์„œ ๋ณด๋‚ธ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ 

chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => { ... });

 

[Sender] ์ฝ˜ํ…์ธ  ์Šคํฌ๋ฆฝํŠธ๋กœ ๋ฉ”์‹œ์ง€ ์ „๋‹ฌ (์‚ฌ์šฉ ์ปจํ…์ŠคํŠธ: ๋ฐฑ๊ทธ๋ผ์šด๋“œ, ํŒ์—…, ์˜ต์…˜)

chrome.tabs.sendMessage(tabId, message, options?, callback?);

 

[Listener] ์ฝ˜ํ…์ธ  ์Šคํฌ๋ฆฝํŠธ์—์„œ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ 

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { ... });

 

 

๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ค์ • (React + TypeScript + Vite)


โถ create-vite ๋ฆฌ์•กํŠธ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ํ…œํ”Œ๋ฆฟ ์„ค์น˜

# ์„ค์น˜ ๋ช…๋ น์–ด
pnpm create vite my-vue-app --template react-ts

 

โท @crxjs/vite-plugin ํ”Œ๋Ÿฌ๊ทธ์ธ ๋ฐ ํฌ๋กฌ ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ ํƒ€์ž… ์ •์˜ ์„ค์น˜

# Vite 5.x ๋ฒ„์ „ ์ด์ƒ์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด bet ๋ฒ„์ „์œผ๋กœ ์„ค์น˜ํ•ด์•ผ ํ•œ๋‹ค
# ์ฐธ๊ณ ๋กœ CRXJS ํ”Œ๋Ÿฌ๊ทธ์ธ์€ @vite/plugin-react-swc๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๋Š”๋‹ค
pnpm add -D @crxjs/vite-plugin@beta
pnpm add -D @types/chrome

 

CRXJS ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ƒˆ๋กœ๊ณ ์นจ ํ•˜์ง€ ์•Š์•„๋„ ์ˆ˜์ •๋œ ๋ถ€๋ถ„์„ ๋ฐ˜์˜ํ•ด ์ฃผ๋Š” HMR(Hot Module Replacement) ๊ธฐ๋Šฅ์„ ์ง€์›ํ•œ๋‹ค. ํฌ๋กฌ ํ™•์žฅ๊ธฐ๋Šฅ์„ ๊ฐœ๋ฐœํ•  ๋•Œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ๋ฐœ์ƒํ•˜๋ฉด ๋งค๋ฒˆ ์ƒˆ๋กœ ๋นŒ๋“œํ•˜๊ณ  ๋ถ€๊ฐ€๊ธฐ๋Šฅ์„ ๋ฆฌ๋กœ๋“œ ํ•ด์•ผ ํ•œ๋‹ค. CRXJS ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์‚ฌ์šฉํ•˜๋ฉด ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜๊ณ  ์ €์žฅํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ๋ฐ˜์˜๋ผ์„œ ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ์„ ํฌ๊ฒŒ ํ–ฅ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

 

โธ ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— manifest.json ์ƒ์„ฑ (๊ณต์‹ ๋ฌธ์„œ)  

// manifest.json
{
  "manifest_version": 3,
  "name": "...",
  "description": "...",
  "version": "1.0",
  "action": {
    "default_popup": "index.html"
  },
  "background": {
    "service_worker": "src/background/background.ts"
  },
  "content_scripts": [
    {
      "matches": ["https://*.google.com/*"],
      "js": ["src/content/content.google.tsx"]
    }
  ],
  "permissions": ["tabs"]
}

 

  • content_scripts : ํŽ˜์ด์ง€๋งˆ๋‹ค ๋™์ž‘ํ•  ์Šคํฌ๋ฆฝํŠธ ์ •์˜ | ์ฐธ๊ณ 
    • matches : ์ฝ˜ํ…์ธ  ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์‚ฝ์ž…๋  ํŽ˜์ด์ง€ ์ง€์ • | Match Pattern ์ฐธ๊ณ 
      "<all_urls>" ๋กœ ์ง€์ •ํ•˜๋ฉด ๋ชจ๋“  ์›นํŽ˜์ด์ง€์— ์‚ฝ์ž…๋œ๋‹ค
    • js : ๋งค์นญ๋œ ํŽ˜์ด์ง€๊ฐ€ ์‚ฝ์ž…๋  ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ ๊ฒฝ๋กœ
    • run_at : ์Šคํฌ๋ฆฝํŠธ ์‚ฝ์ž… ์‹œ์  | ์ฐธ๊ณ 
      • document_idle : ํŽ˜์ด์ง€์™€ ๋ฆฌ์†Œ์Šค๊ฐ€ ์™„์ „ํžˆ ๋กœ๋“œ๋œ ํ›„(๊ธฐ๋ณธ๊ฐ’)
      • document_start : DOM ๋กœ๋“œ๋ฅผ ์‹œ์ž‘ํ•  ๋•Œ
      • document_end : DOM ๋กœ๋“œ๋ฅผ ์™„๋ฃŒํ•œ ํ›„. ๋ฆฌ์†Œ์Šค๋Š” ์—ฌ์ „ํžˆ ๋กœ๋“œ์ค‘์ผ ์ˆ˜ ์žˆ์Œ
  • action : ํฌ๋กฌ ํˆด๋ฐ”์— ํ‘œ์‹œ๋  ์•„์ด์ฝ˜๊ณผ ํด๋ฆญ์‹œ ๋‚˜ํƒ€๋‚˜๋Š” ํŒ์—…์ฐฝ ์ •๋ณด | ์ฐธ๊ณ 
    • default_icon : ์•„์ด์ฝ˜ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ (์‚ฌ์ด์ฆˆ๋ณ„๋กœ ์ง€์ • ๊ฐ€๋Šฅ)
    • default_popup : ํŒ์—…์ฐฝ HTML ๊ฒฝ๋กœ
  • background : ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šคํฌ๋ฆฝํŠธ ์ •์˜ | ์ฐธ๊ณ 
    • service_worker : ์„œ๋น„์Šค ์›Œ์ปค๋กœ ์ง€์ •ํ•  ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ ๊ฒฝ๋กœ. Manifest V3 ๋ถ€ํ„ด 1๊ฐœ์˜ ์„œ๋น„์Šค ์›Œ์ปค๋งŒ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๊ณ , persistent ์†์„ฑ๋„ ์ œ๊ฑฐ๋๋‹ค.
    • type : ์ง€์ •ํ•œ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ES ๋ชจ๋“ˆ๋กœ ๋กœ๋“œํ• ์ง€ ์—ฌ๋ถ€
      • classic : ES ๋ชจ๋“ˆ ํฌํ•จ ์•ˆํ•จ
      • module : ES ๋ชจ๋“ˆ ํฌํ•จ (์Šคํฌ๋ฆฝํŠธ์—์„œ import ๊ตฌ๋ฌธ ์‚ฌ์šฉ)
  • permissions : tabs, storage, webRequest ๋“ฑ ํ”Œ๋Ÿฌ๊ทธ์ธ์—์„œ ์‚ฌ์šฉํ•  ๊ถŒํ•œ ์ •์˜ | ์ฐธ๊ณ   
  • options_ui : ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ ์˜ต์…˜์— ํ‘œ์‹œ๋  ํŽ˜์ด์ง€ ์ •์˜ | ์ฐธ๊ณ 
  • content_security_policy : ์ฝ˜ํ…์ธ  ๋ณด์•ˆ ์ •์ฑ…(CSP) ์„ค์ • | ์ฐธ๊ณ   

 

โน vite.config.ts ํŒŒ์ผ ์„ค์ •

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { crx } from '@crxjs/vite-plugin';
import manifest from './manifest.json';

type Bundle = Record<string, { fileName: string }>;

/**
 * Error: vite manifest is missing ์˜ค๋ฅ˜ ์ž„์‹œ ํ•ด๊ฒฐ
 * @see https://github.com/crxjs/chrome-extension-tools/issues/846#issuecomment-1861880919
 * */
const viteManifestHackIssue846: {
  name: string;
  renderCrxManifest(_manifest: typeof manifest, bundle: Bundle): void;
} = {
  name: 'manifestHackIssue846',
  renderCrxManifest(_manifest, bundle) {
    bundle['manifest.json'] = bundle['.vite/manifest.json'];
    bundle['manifest.json'].fileName = 'manifest.json';
    delete bundle['.vite/manifest.json'];
  },
};

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), crx({ manifest }), viteManifestHackIssue846],
  server: {
    /**
     * SyntaxError: Failed to construct 'WebSocket': The URL 'ws://localhost:undefined/' is invalid ์˜ค๋ฅ˜ ํ•ด๊ฒฐ
     * @see https://github.com/crxjs/chrome-extension-tools/issues/696#issuecomment-1526138970
     * */
    strictPort: true, // ๋‹ค๋ฅธ ํฌํŠธ๋ฅผ ์ฐพ์ง€ ์•Š๊ณ  ํ•ญ์ƒ ์ง€์ •ํ•œ ํฌํŠธ์—์„œ๋งŒ ์‹คํ–‰
    port: 5173, // vite ๊ฐœ๋ฐœ์„œ๋ฒ„๊ฐ€ ์‚ฌ์šฉํ•  ํฌํŠธ ์ง€์ •
    hmr: {
      clientPort: 5173, // HMR ์—ฐ๊ฒฐ์— ์‚ฌ์šฉํ•  ํด๋ผ์ด์–ธํŠธ ํฌํŠธ ์ง€์ •
    },
  },
});

 

โบ ๊ฐœ๋ฐœ์šฉ ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ ์„ค์น˜

  1. pnpm run dev ๋ช…๋ น์–ด๋กœ ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰ → ์ž๋™์œผ๋กœ ๋นŒ๋“œ ํ›„ dist ํด๋”์— ์ €์žฅ๋จ
  2. ํฌ๋กฌ ์ฃผ์†Œ์ฐฝ์— chrome://extensions ์ž…๋ ฅ ํ›„ dist ํด๋”๋ฅผ ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก์œผ๋กœ ๋“œ๋ž˜๊ทธ
    ๐Ÿ’ก ์šฐ์ธก ์ƒ๋‹จ ๊ฐœ๋ฐœ์ž ๋ชจ๋“œ ํ™œ์„ฑํ™” ํ•„์š”
  3. ๋ถ€๊ฐ€๊ธฐ๋Šฅ ํˆด๋ฐ” ์šฐํด๋ฆญ - ํŒ์—… ๊ฒ€์‚ฌ๋ฅผ ํด๋ฆญํ•˜๋ฉด ๊ฐœ๋ฐœ์ž ์ฝ˜์†”์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค

 

  1.  

Content Script ์‚ฝ์ž…ํ•˜๊ธฐ — Shadow DOM ํ™œ์šฉ


์›น ํŽ˜์ด์ง€๋ฅผ ๋กœ๋“œํ•˜๋ฉด ํฌ๋กฌ ํ™•์žฅ ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ์›ํ•˜๋Š” ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฝ์ž…ํ•˜๊ณ  ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฅผ ์ด์šฉํ•ด์„œ ์ง€์ •ํ•œ ์›นํŽ˜์ด์ง€์— ์ƒˆ๋กœ์šด ์š”์†Œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๋“ฑ์˜ DOM ์กฐ์ž‘์ด ๊ฐ€๋Šฅํ•ด์ง„๋‹ค.

 

์ƒˆ๋กœ์šด ์š”์†Œ๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ ์ผ๋ฐ˜ DOM ๋Œ€์‹  Shadow DOM์„ ์‚ฌ์šฉํ•˜๋ฉด ์›น ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์˜ HTML ๋งˆํฌ์—…๊ณผ CSS ์Šคํƒ€์ผ์„ ์บก์Šํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋Ÿผ ๋งˆํฌ์—…, ์Šคํƒ€์ผ์ด ํŽ˜์ด์ง€ ๋‹ค๋ฅธ ๋ถ€๋ถ„(์ผ๋ฐ˜ DOM)์— ์˜ํ–ฅ์„ ๋ฏธ์น˜์ง€ ์•Š๊ฒŒ ๋ผ์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ณ  ๋ชจ๋“ˆํ™”๋œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋” ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

 

DOM Tree์— Shadow Tree๋ฅผ ์—ฐ๊ฒฐํ•œ ๋ชจ์Šต via MDN

์›ํ•˜๋Š” ์›นํŽ˜์ด์ง€์— ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฝ์ž…ํ•˜๋ ค๋ฉด manifest.json ํŒŒ์ผ content_scripts ์†์„ฑ์˜ maches ๋ฐฐ์—ด์— ํ™•์žฅ ๊ธฐ๋Šฅ์ด ์ ์šฉ๋  URL ํŒจํ„ด์„ ์ถ”๊ฐ€ํ•˜๊ณ , js ๋ฐฐ์—ด์— ์‚ฝ์ž…ํ•˜๋ ค๋Š” ์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค.

// manifest.json
{
  // ...
  "content_scripts": [
    {
      "matches": ["https://*.google.com/*"], // ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ฃผ์ž…ํ•  URL (ํŒจํ„ด)
      "js": ["src/content/content.google.tsx"] // ์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ ๊ฒฝ๋กœ
    }
  ]
}

 

์‚ฝ์ž…๋œ ์Šคํฌ๋ฆฝํŠธ๋Š” ํ•ด๋‹น ํŽ˜์ด์ง€์˜ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ - ์†Œ์Šค - ์ฝ˜ํ…์ธ  ์Šคํฌ๋ฆฝํŠธ์—์„œ ์ฐพ์„ ์ˆ˜ ์žˆ๋‹ค (์œ„ ์†Œ์Šค์ฝ”๋“œ๋Š” ๋‚œ๋…ํ™”๋œ ์ƒํƒœ)

 

Shadow DOM ์˜ˆ์‹œ

Shadow DOM์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋จผ์ € ํ˜ธ์ŠคํŠธ ์š”์†Œ๊ฐ€ ํ•„์š”ํ•˜๋‹ค. ๊ธฐ์กด ์š”์†Œ๋ฅผ ํ˜ธ์ŠคํŠธ ์š”์†Œ๋กœ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์ƒˆ๋กœ์šด ์š”์†Œ๋ฅผ ์ƒ์„ฑํ•ด๋„ ๋œ๋‹ค. ๊ทธ๋Ÿฐ ๋‹ค์Œ ํ˜ธ์ŠคํŠธ ์š”์†Œ์— Shadow DOM์„ ์—ฐ๊ฒฐ(attach)ํ•ด์•ผ ํ•˜๋Š”๋ฐ open, closed ๋‘ ๊ฐ€์ง€ ๋ชจ๋“œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. open ๋ชจ๋“œ์—์„  ํ˜ธ์ŠคํŠธ ์š”์†Œ ๋ฐ–์—์„œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ด์šฉํ•ด Shadow DOM์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐ˜๋ฉด, closed ๋ชจ๋“œ์—์„  ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์—†๋‹ค.

// Shadow DOM์„ ํฌํ•จํ•  ํ˜ธ์ŠคํŠธ ์š”์†Œ ์ƒ์„ฑ
const shadowHost = document.createElement("shadow-host");

// ํ˜ธ์ŠคํŠธ ์š”์†Œ์— 'closed' ๋ชจ๋“œ๋กœ Shadow DOM ์—ฐ๊ฒฐ
const shadowRoot = shadowHost.attachShadow({ mode: "closed" });

shadowHost.shadowRoot; // null (์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ShadowDOM ์ ‘๊ทผ ๋ถˆ๊ฐ€)

 

element.attachShadow ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด Shadow Root๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š”๋ฐ ์ด๋ฆ„ ๊ทธ๋Œ€๋กœ Shadow DOM ํŠธ๋ฆฌ์˜ ๋ฃจํŠธ ๋…ธ๋“œ๋ฅผ ๊ฐ€๋ฆฌํ‚จ๋‹ค. ์ผ๋ฐ˜ DOM ์ฒ˜๋Ÿผ Shadow Root์˜ ์ž์‹์œผ๋กœ ์›ํ•˜๋Š” ์š”์†Œ๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๐Ÿ’ก ๋ฌธ์ž์—ด์„ ์ „๋‹ฌํ•˜๋ฉด HTML๋กœ ํŒŒ์‹ฑํ•ด์„œ DOM์— ์‚ฝ์ž…ํ•˜๋Š” insertAdjacentHTML ๋ฉ”์„œ๋“œ๋„ ์žˆ๋‹ค.

 

๋งŒ์•ฝ ํ˜ธ์ŠคํŠธ ์š”์†Œ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์ƒˆ๋กœ์šด ์š”์†Œ๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค๋ฉด ์ด ํ˜ธ์ŠคํŠธ ์š”์†Œ๋ฅผ DOM ํŠธ๋ฆฌ์— ์‚ฝ์ž…ํ•ด์•ผ ํ™”๋ฉด์— ํ‘œ์‹œ๋œ๋‹ค. parentElement.insertAdjacentElement(position, element) ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•˜๋ฉด ์‚ฝ์ž… ์œ„์น˜๋ฅผ ๋”์šฑ ์œ ์—ฐํ•˜๊ฒŒ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

position ํŒŒ๋ผ๋ฏธํ„ฐ ์‚ฝ์ž… ์œ„์น˜
beforebegin parentElement ์ด์ „์— ์‚ฝ์ž…
afterbegin parentElement ์ฒซ๋ฒˆ์งธ ์ž์‹์œผ๋กœ ์‚ฝ์ž…
beforeend parentElement ๋งˆ์ง€๋ง‰ ์ž์‹์œผ๋กœ ์‚ฝ์ž…
afterend parentElement ๋’ค์— ์‚ฝ์ž…

 

google.com ์ฒซ ํ™”๋ฉด์—์„œ ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด .k1zIA ํด๋ž˜์Šค๋ฅผ ๊ฐ€์ง„ ์š”์†Œ ์ด์ „ ์œ„์น˜(beforebegin)์— Shadow DOM์ด ์ถ”๊ฐ€๋˜๊ณ , ๊ตฌ๊ธ€ ๋กœ๊ณ  ์œ„์ชฝ์— Hello World ๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๊ฐ€ ํ‘œ์‹œ๋œ๋‹ค. ์ฐธ๊ณ ๋กœ ๊ตฌ๊ธ€ ์ฒซ ํ™”๋ฉด์˜ ๋กœ๊ณ ๋ฅผ ๊ฐ์‹ธ๋Š” ์š”์†Œ๋Š” ํ•ญ์ƒ .k1zIA ํด๋ž˜์Šค๋ฅผ ๊ฐ€์ง„๋‹ค.

// Shadow DOM์„ ํฌํ•จํ•  ํ˜ธ์ŠคํŠธ ์š”์†Œ ์ƒ์„ฑ
const shadowHost = document.createElement('shadow-host');

// ํ˜ธ์ŠคํŠธ ์š”์†Œ์— Shadow DOM ์—ฐ๊ฒฐ ํ›„ Shadow Root ๋ฐ˜ํ™˜
const shadowRoot = shadowHost.attachShadow({ mode: 'closed' });

// ํ™”๋ฉด์— ํ‘œ์‹œํ•  ์š”์†Œ๋ฅผ Shadow Root ์ž์‹์œผ๋กœ ์ถ”๊ฐ€
const h1 = document.createElement('h1');
h1.textContent = 'Hello World ๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€';
shadowRoot.appendChild(h1);

// logo ์ด์ „ ์œ„์น˜์— Shadow DOM ํ˜ธ์ŠคํŠธ ์ถ”๊ฐ€
const logo = document.querySelector('.k1zIA');
logo.insertAdjacentElement('beforebegin', shadowHost);

 

google.com ๋ฉ”์ธ ํ™”๋ฉด์—์„œ ์œ„ ์ฝ”๋“œ ์‹คํ–‰ ๊ฒฐ๊ณผ
.k1zIA ํด๋ž˜์Šค๋ฅผ ๊ฐ€์ง„ ์š”์†Œ ์ด์ „ ์œ„์น˜์— Shadow DOM์ด ์ถ”๊ฐ€๋œ ๋ชจ์Šต

 

React์—์„œ Shadow DOM ์‚ฌ์šฉํ•˜๊ธฐ

๐Ÿ“ฆ 
โ”œโ”€ public/
โ”œโ”€ src/
โ”œโ”€ components/
โ”‚  โ”œโ”€ index.ts
โ”‚  โ”œโ”€ google.tsx [๊ตฌ๊ธ€ ์ปค์Šคํ…€ ์ฝ˜ํ…์ธ ]
โ”‚  โ””โ”€ shadow-dom.ts [Shadow DOM ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ]
โ”œโ”€ content/
โ”‚  โ”œโ”€ content.google.tsx [๊ตฌ๊ธ€ ์ปค์Šคํ…€ ์ฝ˜ํ…์ธ  ๋ Œ๋”๋Ÿฌ -> ์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ ๊ฒฝ๋กœ]
โ”‚  โ””โ”€ content.render.ts [Shadow DOM ๋ฒ”์šฉ ๋ Œ๋”๋Ÿฌ]
โ””โ”€ manifest.json

 

Shadow DOM ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ

๋ถ€๋ชจ ์š”์†Œ๋ฅผ prop์œผ๋กœ ๋ฐ›์•„ ์ง€์ •ํ•œ ์œ„์น˜์— Shadow DOM์„ ์ƒ์„ฑ/์—ฐ๊ฒฐํ•˜๊ณ  createPortal API๋ฅผ ์ด์šฉํ•˜์—ฌ Shadow Root ๋‚ด๋ถ€์— children์„ ๋ Œ๋”๋งํ•˜๋Š” ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ.

 

์ผ๋ฐ˜์ ์œผ๋กœ React ์ปดํฌ๋„ŒํŠธ๋Š” ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์˜ DOM ๊ณ„์ธต ์•ˆ์—์„œ ๋ Œ๋”๋ง๋˜๋Š”๋ฐ ์ด๋Ÿฐ ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ๋ฒ—์–ด๋‚˜ ๋‹ค๋ฅธ ์œ„์น˜์— ๋ Œ๋”๋งํ•˜๊ณ  ์‹ถ์„ ๋•Œ createPortal์„ ์‚ฌ์šฉํ•œ๋‹ค. ํŠนํžˆ ๋ชจ๋‹ฌ, ํŒ์—… ๋“ฑ์„ ๊ตฌํ˜„ํ•  ๋•Œ ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” API.

// components/shadow-dom.ts
interface ShadowDomProps {
  parentElement: Element;
  position?: InsertPosition;
}

export default function ShadowDom({
  parentElement,
  position = 'beforebegin',
  children,
}: PropsWithChildren<ShadowDomProps>) {
  // ํ˜ธ์ŠคํŠธ ์š”์†Œ ์ƒ์„ฑ
  const [shadowHost] = useState(() => document.createElement('shadow-host'));
  // ํ˜ธ์ŠคํŠธ ์š”์†Œ์— closed ๋ชจ๋“œ๋กœ Shadow DOM ์—ฐ๊ฒฐ
  const [shadowRoot] = useState(() =>
    shadowHost.attachShadow({ mode: 'closed' }),
  );

  // DOM์ด ํ™”๋ฉด์— ๊ทธ๋ ค์ง€๊ธฐ ์ „์— ํ˜ธ์ถœ
  useLayoutEffect(() => {
    if (parentElement) {
      /** position ์œ„์น˜์— shadowHost DOM ์ถ”๊ฐ€ */
      parentElement.insertAdjacentElement(position, shadowHost);
    }
    return () => {
      /** ์–ธ๋งˆ์šดํŠธ์‹œ shadowHost DOM ์ œ๊ฑฐ */
      shadowHost.remove();
    };
  }, [parentElement, position, shadowHost]);

  return createPortal(children, shadowRoot);
}

 

  1. shadow-host ํƒœ๊ทธ ์ด๋ฆ„์„ ๊ฐ€์ง„ ํ˜ธ์ŠคํŠธ ์š”์†Œ๋ฅผ ์ƒ์„ฑํ•˜๊ณ , Shadow DOM ์—ฐ๊ฒฐ
  2. ํ™”๋ฉด์— DOM์„ ๊ทธ๋ฆฌ๊ธฐ ์ „, prop์œผ๋กœ ๋ฐ›์€ ๋ถ€๋ชจ ์š”์†Œ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ง€์ •ํ•œ ์œ„์น˜์— Shadow DOM ์ถ”๊ฐ€
  3. createPortal์„ ์ด์šฉํ•ด Shadow Root ๋‚ด๋ถ€์— children ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง

 

๊ตฌ๊ธ€ ์ปค์Šคํ…€ ์ฝ˜ํ…์ธ 

.k1zIA ํด๋ž˜์Šค ์ด๋ฆ„์„ ๊ฐ€์ง„ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ๋ถ€๋ชจ ์š”์†Œ๋กœ ์ง€์ •ํ•˜๊ณ , ์ด ๋ถ€๋ชจ ์š”์†Œ ์ด์ „ ์œ„์น˜์— Shadow DOM์„ ์ถ”๊ฐ€ํ•œ๋‹ค. ShadowDom ์ปดํฌ๋„ŒํŠธ์˜ children์€ createPortal์„ ํ†ตํ•ด Shadow Root ๋‚ด๋ถ€์— ๋ Œ๋”๋ง๋œ๋‹ค.

// components/google.tsx
import ShadowDom from './shadow-dom';

export default function Google() {
  const [parentElement] = useState(() => document.querySelector('.k1zIA'));
  if (!parentElement) return null;

  return (
    <ShadowDom parentElement={parentElement} position="beforebegin">
      <h1>Hello World ๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€</h1>
    </ShadowDom>
  );
}

 

Shadow DOM ๋ฒ”์šฉ ๋ Œ๋”๋Ÿฌ

์ฝ˜ํ…์ธ  ์Šคํฌ๋ฆฝํŠธ์˜ UI๋ฅผ ๋ Œ๋”๋งํ•˜๊ธฐ ์œ„ํ•œ ๋ Œ๋”๋Ÿฌ. createRoot() ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•ด React์˜ ๋ Œ๋”๋ง ์‹œ์ž‘์ (๋ฃจํŠธ)์„ DocumentFragment๋กœ ์ง€์ •ํ•˜๊ณ , prop์œผ๋กœ ๋ฐ›์€ ReactElement๋ฅผ ๋ฃจํŠธ์— ๋ Œ๋”๋งํ•œ๋‹ค.

// content/content.render.ts
export default function contentRender(content: ReactElement) {
  const container = document.createDocumentFragment();
  const root = createRoot(container);
  root.render(content);
}

 

DocumentFragment๋Š” ์ผ๋ฐ˜์ ์ธ DOM ๋…ธ๋“œ์ฒ˜๋Ÿผ ์ž‘๋™ํ•˜์ง€๋งŒ ๋ฉ”์ธ DOM ํŠธ๋ฆฌ์— ์†ํ•˜์ง€ ์•Š๋Š” ์ž„์‹œ ์ปจํ…Œ์ด๋„ˆ๋‹ค. DOM ๋…ธ๋“œ๋ฅผ ์ž„์‹œ ์ €์žฅํ•˜๊ฑฐ๋‚˜, ์—ฌ๋Ÿฌ ๋…ธ๋“œ์— CRUD ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•œ ํ›„ ์ผ๊ด„์ ์œผ๋กœ DOM ํŠธ๋ฆฌ์— ์ถ”๊ฐ€ํ•  ๋•Œ ์œ ์šฉํ•˜๊ฒŒ ์“ฐ์ธ๋‹ค. ์‹ค์ œ DOM ํŠธ๋ฆฌ์— ์†ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ reflow/repaint๋ฅผ ์œ ๋ฐœํ•˜์ง€ ์•Š๋Š” ์žฅ์ ์ด ์žˆ๋‹ค. ์ฐธ๊ณ ๋กœ DOM ํŠธ๋ฆฌ์— ์ถ”๊ฐ€ํ•  ๋• DocumentFragment์˜ ์ž์‹ ๋…ธ๋“œ๋“ค๋งŒ ์ถ”๊ฐ€๋œ๋‹ค.

 

์ตœ์ข… ๋ Œ๋”๋ง

์œ„์—์„œ ์ž‘์„ฑํ•œ Shadow DOM ๋ Œ๋”๋Ÿฌ๋ฅผ ํ™œ์šฉํ•ด ๊ตฌ๊ธ€ ์ฒซ ํŽ˜์ด์ง€์— ์‚ฝ์ž…ํ•  ์ปค์Šคํ…€ ์ฝ˜ํ…์ธ ๋ฅผ ๋กœ๋“œํ•˜๊ณ  ๋ Œ๋”๋ง ํ•œ๋‹ค. manifest.json์—์„œ ์•„๋ž˜ ํŒŒ์ผ์„ ์Šคํฌ๋ฆฝํŠธ ๊ฒฝ๋กœ๋กœ ์„ค์ •ํ•˜๋ฉด google.com ์ ‘์†์‹œ ์ž๋™์œผ๋กœ ์‹คํ–‰๋˜๊ณ  ์ฃผ์ž…๋œ๋‹ค.

// content/content.google.tsx
import contentRender from './content.render.ts';
import { Google } from '@/components';

contentRender(<Google />);

 

์ด๋Ÿฐ์‹์œผ๋กœ Shadow DOM ์ƒ์„ฑ/์—ฐ๊ฒฐ(๊ณตํ†ต), Shadow DOM ๋ Œ๋”๋Ÿฌ(๊ณตํ†ต), ์ปค์Šคํ…€ ์ฝ˜ํ…์ธ ๋ฅผ ๊ฐ๊ฐ ๋…๋ฆฝ์ ์ธ ๋ชจ๋“ˆ๋กœ ๋ถ„๋ฆฌํ•˜๋ฉด, ์ปค์Šคํ…€ ์ฝ˜ํ…์ธ ์— ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ ์šฉํ•˜๊ธฐ ํ›จ์”ฌ ์ˆ˜์›”ํ•ด์ง„๋‹ค. ๋˜ํ•œ ๋‹ค๋ฅธ ํŽ˜์ด์ง€์—๋„ ๋™์ผํ•œ ๋ Œ๋”๋Ÿฌ๋ฅผ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด ๊ด€๋ฆฌ ํšจ์œจ์„ฑ๋„ ์ข‹์•„์ง„๋‹ค.

 

 

๋ ˆํผ๋Ÿฐ์Šค


 


๊ธ€ ์ˆ˜์ •์‚ฌํ•ญ์€ ๋…ธ์…˜ ํŽ˜์ด์ง€์— ๊ฐ€์žฅ ๋น ๋ฅด๊ฒŒ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”
๋ฐ˜์‘ํ˜•