[React] ํฌ๋กฌ ํ์ฅ ํ๋ก๊ทธ๋จ ๊ฐ๋ฐ ๋ฐฐ๊ฒฝ ์ง์ ๋ฐ ํํ ๋ฆฌ์ผ
ํฌ๋กฌ ํ์ฅ๊ธฐ๋ฅ ์ฃผ์ ๊ธฐ๋ฅ
- ์ฌ์ฉ์ ์ธํฐํ์ด์ค ์ฌ์ฉ์ํ : ๋ธ๋ผ์ฐ์ ํด๋ฐ์ ๋ฒํผ ์ถ๊ฐ, ์ฌ์ฉ์ ์ ์ ํ์ /์ค๋ฒ๋ ์ด ์์ฑ ๋ฑ
- ์ฝํ ์ธ ์คํฌ๋ฆฝํธ : ์นํ์ด์ง์ ์ง์ ์คํฌ๋ฆฝํธ๋ฅผ ์ฝ์ ํ์ฌ DOM ์กฐ์ ๊ฐ๋ฅ
- ๋ฐฑ๊ทธ๋ผ์ด๋ ์คํฌ๋ฆฝํธ : ๋ธ๋ผ์ฐ์ ๋ฅผ ์ฌ์ฉํ๋ ๋์ ์ง์์ ์ผ๋ก ์คํ๋ผ์ ํ์ํ ๊ธฐ๋ฅ ์ ๊ณต
- API ์ ๊ทผ : ๋ถ๋งํฌ/ํญ/์๋์ฐ ๊ด๋ฆฌ, ํ์คํ ๋ฆฌ ์กฐ์ ๋ฑ ๋ธ๋ผ์ฐ์ ์ ๋ค์ํ ์์ญ ์ ๊ทผ ๊ฐ๋ฅ
๐ ํญ ์ด๋, capture, zoom ๋ฑ ๋ค์ํ ๋ฐฉ์์ผ๋ก ์ ์ด ๊ฐ๋ฅ - ๋ฉ์์ง ์์คํ : ํ์ฅ ๊ธฐ๋ฅ์ ๋ค๋ฅธ ์ปดํฌ๋ํธ ํน์ ์นํ์ด์ง์ ๋ฉ์์ง ๊ตํ ๊ฐ๋ฅ
- ์น ์์ฒญ ์กฐ์ : ๋คํธ์ํฌ ์์ฒญ ๊ฐ๋ก์ฑ๊ธฐ, ๊ด๊ณ ์ฐจ๋จ, ํ๋ผ์ด๋ฒ์ ๋ณดํธ, ์น ํธ๋ํฝ ๊ด๋ฆฌ ๋ฑ
๐chrome.webRequest
API๋ก http ์์ฒญ์ ๊ฐ๋ก์ฑ๊ฑฐ๋ request header ์์ ๊ฐ๋ฅ - ์ฌ์ฉ์ ๋ฐ์ดํฐ ์ ์ฅ : ๋ก์ปฌ ์ ์ฅ์ ํน์ Google ๊ณ์ ๊ณผ ๋๊ธฐํ๋ ์ ์ฅ์๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ ์ ์ฅ/๊ด๋ฆฌ
- ๋ค๊ตญ์ด ์ง์ : ๋ค์ํ ์ธ์ด๋ก ํ์ฅ ๊ธฐ๋ฅ ์ ๊ณต
ํฌ๋กฌ ํ์ฅ๊ธฐ๋ฅ ๊ตฌ์กฐ
๐ก ์น ํ์ด์ง์์ ๋ง์ฐ์ค ์ฐํด๋ฆญ ์ ํ์๋๋ ๋ฉ๋ด์ ํ์ฅ ํ๋ก๊ทธ๋จ ํญ๋ชฉ์ ์ถ๊ฐํ ์๋ ์๋ค
- ๋ฐฑ๊ทธ๋ผ์ด๋ ์คํฌ๋ฆฝํธ : ์ด๋ฒคํธ ๊ธฐ๋ฐ์ ์๋น์ค ์์ปค(Manifest V3 ๊ธฐ์ค)
- ํ์ฅ ํ๋ก๊ทธ๋จ์ ํต์ฌ ๋ก์ง์ ์คํํ๋ ๊ณณ(๋ผ์ดํ์ฌ์ดํด ๊ด๋ฆฌ/์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ฑ)
- ๋ธ๋ผ์ฐ์ ์์์ ํ์ฑํ๋๊ณ , ๋๊ธฐ ์ํ๋ก ์๋ค๊ฐ ์ด๋ฒคํธ ๋ฐ์์ ์์ ์ํ
- ๋๋ถ๋ถ์ Extension API ์ฌ์ฉ ๊ฐ๋ฅ
- ์นํ์ด์ง์ ๋ํ ์ ๊ทผ ๊ถํ ์์ (DOM ์ ๊ทผ ๋ถ๊ฐ)
- ์ฝํ
์ธ ์คํฌ๋ฆฝํธ : ์นํ์ด์ง ๋ก๋์ ์คํฌ๋ฆฝํธ ์ฝ์
/์คํ
- ์น ํ์ด์ง์ ์ปจํ ์คํธ์์ ์คํ (์นํ์ด์ง์ ์๋ฐ์คํฌ๋ฆฝํธ ์ค์ฝํ/๋ณ์ ๋ฑ์ ์ ๊ทผ ๋ถ๊ฐ)
- ์คํฌ๋ฆฝํธ๊ฐ ์ฃผ์ ๋ ์น์ฌ์ดํธ์ DOM์ ์ ๊ทผํ๊ฑฐ๋ ์์ ๊ฐ๋ฅ
- ์ผ๋ถ Extension API๋ง ์ฌ์ฉ ๊ฐ๋ฅ
window.postMessage
ํน์ ์ปค์คํ ์ด๋ฒคํธ๋ฅผ ํตํด ์น ํ์ด์ง์ ๋ค๋ฅธ ์คํฌ๋ฆฝํธ์ ํต์ ๊ฐ๋ฅ
- ์ต์ ํ์ด์ง : ํ์ฅ ํ๋ก๊ทธ๋จ์ ์ค์ ์ ๋ณ๊ฒฝํ ์ ์๋ HTML ํ์ด์ง
- ํ์ ํ์ด์ง : ํ์ฅ ํ๋ก๊ทธ๋จ ์์ด์ฝ์ ํด๋ฆญํ๋ฉด ๋ํ๋๋ UI
์น ์์ปค vs ์๋น์ค ์์ปค
๐ก Manifest V3 ๊ธฐ์ค ํฌ๋กฌ ๋ถ๊ฐ๊ธฐ๋ฅ์ ๋ฐฑ๊ทธ๋ผ์ด๋ ์คํฌ๋ฆฝํธ๋ ์๋น์ค ์์ปค๋ก ์๋ํ๋ค. ์๋น์ค ์์ปค๋ ์ด๋ฒคํธ ๊ธฐ๋ฐ์ผ๋ก ์๋ํ๋ฉฐ, ํ์ํ ๋๋ง ํ์ฑํ๋ผ์ ๋ฆฌ์์ค ์ฌ์ฉ์ ์ต์ํํ ์ ์๋ ์ฅ์ ์ด ์๋ค.
๊ณตํต์
- ๋ณ๋ ์ค๋ ๋์์ ์คํ๋๋ฏ๋ก ๋ฉ์ธ ์ค๋ ๋์ ์ํฅ์ ์ฃผ์ง ์๋๋ค
window
,document
๊ฐ์ฒด์ ์ ๊ทผํ ์ ์๋ค → DOM ์กฐ์ ๋ถ๊ฐ- ๋ธ๋ผ์ฐ์ ์์ ์ ๊ณตํ๋ API์ ์ ํ์ ์ผ๋ก๋ง ์ ๊ทผํ ์ ์๋ค
- ๋์ผ ์ถ์ฒ ์ ์ฑ ์ค์ (์์ปค๋ฅผ ์คํํ๋ ์ถ์ฒ์ ๋ค๋ฅธ ์ถ์ฒ์ ๋ฐ์ดํฐ ์ ๊ทผ ๋ถ๊ฐ)
์ฐจ์ด์
- ์๋ช
์ฃผ๊ธฐ
- ์น ์์ปค : ์์ปค๋ฅผ ์์ฑํ ์คํฌ๋ฆฝํธ๊ฐ ์กด์ฌํ ๋๊น์ง ํ์ฑํ(์์ปค๋ฅผ ์คํ์ค์ธ ํญ์ ๋ซ์ผ๋ฉด ์์ปค๋ ์ข ๋ฃ)
- ์๋น์ค ์์ปค : ํ์ฑ ํญ๊ณผ ๊ด๊ณ์์ด ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ ๋๋ง๋ค ํ์ฑํ๋ผ์ ์คํ
- ์ค์ฝํ
- ์น ์์ปค : ํ ํ์ด์ง์์ ์ฌ๋ฌ ์น ์์ปค ์์ฑ ๊ฐ๋ฅ
- ์๋น์ค ์์ปค : ๋จ์ผ ์๋น์ค ์์ปค๊ฐ ์ค์ฝํ๋ด ๋ชจ๋ ํ์ฑ ํญ ์ ์ด
- ์ปค๋ฎค๋์ผ์ด์
- ์น ์์ปค : ๋ฉ์ธ ์ค๋ ๋์ ๋ฉ์์ง ๊ธฐ๋ฐ์ผ๋ก ํต์ (
postMessage
,onmessage
๋ฉ์๋ ์ฌ์ฉ) - ์๋น์ค ์์ปค : ์ด๋ฒคํธ ๊ธฐ๋ฐ. ๋คํธ์ํฌ ์์ฒญ์ ๊ฐ๋ก์ฑ ๋๋ ํธ์ ๋ฉ์์ง๋ฅผ ๋ฐ๋ ์ด๋ฒคํธ ๋ฑ์ ๋ฐ์
- ์น ์์ปค : ๋ฉ์ธ ์ค๋ ๋์ ๋ฉ์์ง ๊ธฐ๋ฐ์ผ๋ก ํต์ (
- ์ค์น / ์
๋ฐ์ดํธ
- ์น ์์ปค : ํ์์ ๋ฐ๋ผ ์ฆ์ ์์ฑ/์ฌ์ฉ
- ์๋น์ค ์์ปค : ์ค์น/ํ์ฑํ ๊ณผ์ ํ์. ์ ๋ฒ์ ์ ์์ปค๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด ์ ๋ฐ์ดํธ ํ์
- ์ฌ์ฉ ๋ชฉ์
- ์น ์์ปค : ๋ฉ์ธ ์ค๋ ๋ ์ฑ๋ฅ ์ ํ๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ๋ฌด๊ฑฐ์ด ์ฐ์ฐ/๋ค์ค ์ค๋ ๋ฉ์ด ํ์ํ ์์ ์ ์ฌ์ฉ
- ์๋น์ค ์์ปค : ๋คํธ์ํฌ ํ๋ก์, ๋ฐฑ๊ทธ๋ผ์ด๋ ๋๊ธฐํ, ํธ์ ์๋ฆผ, ์บ์ฑ, ์คํ๋ผ์ธ ์ฒ๋ฆฌ ๋ฑ
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 ์ฐ๊ฒฐ์ ์ฌ์ฉํ ํด๋ผ์ด์ธํธ ํฌํธ ์ง์
},
},
});
โบ ๊ฐ๋ฐ์ฉ ํ์ฅ ํ๋ก๊ทธ๋จ ์ค์น
pnpm run dev
๋ช ๋ น์ด๋ก ๊ฐ๋ฐ ์๋ฒ ์คํ → ์๋์ผ๋ก ๋น๋ ํdist
ํด๋์ ์ ์ฅ๋จ- ํฌ๋กฌ ์ฃผ์์ฐฝ์
chrome://extensions
์ ๋ ฅ ํdist
ํด๋๋ฅผ ํ์ฅ ํ๋ก๊ทธ๋จ ๋ชฉ๋ก์ผ๋ก ๋๋๊ทธ
๐ก ์ฐ์ธก ์๋จ ๊ฐ๋ฐ์ ๋ชจ๋ ํ์ฑํ ํ์ - ๋ถ๊ฐ๊ธฐ๋ฅ ํด๋ฐ ์ฐํด๋ฆญ - ํ์ ๊ฒ์ฌ๋ฅผ ํด๋ฆญํ๋ฉด ๊ฐ๋ฐ์ ์ฝ์์ ํ์ธํ ์ ์๋ค
Content Script ์ฝ์ ํ๊ธฐ — Shadow DOM ํ์ฉ
์น ํ์ด์ง๋ฅผ ๋ก๋ํ๋ฉด ํฌ๋กฌ ํ์ฅ ๊ธฐ๋ฅ์ ํตํด ์ํ๋ ์คํฌ๋ฆฝํธ๋ฅผ ์ฝ์ ํ๊ณ ์คํํ ์ ์๋ค. ์ด๋ฅผ ์ด์ฉํด์ ์ง์ ํ ์นํ์ด์ง์ ์๋ก์ด ์์๋ฅผ ์ถ๊ฐํ๋ ๋ฑ์ DOM ์กฐ์์ด ๊ฐ๋ฅํด์ง๋ค.
์๋ก์ด ์์๋ฅผ ์ถ๊ฐํ ๋ ์ผ๋ฐ DOM ๋์ Shadow DOM์ ์ฌ์ฉํ๋ฉด ์น ์ปดํฌ๋ํธ ๋ด๋ถ์ HTML ๋งํฌ์ ๊ณผ CSS ์คํ์ผ์ ์บก์ํํ ์ ์๋ค. ๊ทธ๋ผ ๋งํฌ์ , ์คํ์ผ์ด ํ์ด์ง ๋ค๋ฅธ ๋ถ๋ถ(์ผ๋ฐ DOM)์ ์ํฅ์ ๋ฏธ์น์ง ์๊ฒ ๋ผ์ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ๊ณ ๋ชจ๋ํ๋ ์ปดํฌ๋ํธ๋ฅผ ๋ ์ฝ๊ฒ ๋ง๋ค ์ ์๋ค.
์ํ๋ ์นํ์ด์ง์ ์คํฌ๋ฆฝํธ๋ฅผ ์ฝ์
ํ๋ ค๋ฉด 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);
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);
}
shadow-host
ํ๊ทธ ์ด๋ฆ์ ๊ฐ์ง ํธ์คํธ ์์๋ฅผ ์์ฑํ๊ณ , Shadow DOM ์ฐ๊ฒฐ- ํ๋ฉด์ DOM์ ๊ทธ๋ฆฌ๊ธฐ ์ , prop์ผ๋ก ๋ฐ์ ๋ถ๋ชจ ์์๋ฅผ ๊ธฐ์ค์ผ๋ก ์ง์ ํ ์์น์ Shadow DOM ์ถ๊ฐ
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 ๋ ๋๋ฌ(๊ณตํต), ์ปค์คํ ์ฝํ ์ธ ๋ฅผ ๊ฐ๊ฐ ๋ ๋ฆฝ์ ์ธ ๋ชจ๋๋ก ๋ถ๋ฆฌํ๋ฉด, ์ปค์คํ ์ฝํ ์ธ ์ ์๋ก์ด ๊ธฐ๋ฅ์ ์ถ๊ฐํ๊ฑฐ๋ ๋ณ๊ฒฝ ์ฌํญ์ ์ ์ฉํ๊ธฐ ํจ์ฌ ์์ํด์ง๋ค. ๋ํ ๋ค๋ฅธ ํ์ด์ง์๋ ๋์ผํ ๋ ๋๋ฌ๋ฅผ ์ฌ์ฌ์ฉํ ์ ์์ด ๊ด๋ฆฌ ํจ์จ์ฑ๋ ์ข์์ง๋ค.
๋ ํผ๋ฐ์ค
- ํ์ฅ ํ๋ก๊ทธ๋จ / ๊ฐ๋ฐ | Chrome for Developers
- ํฌ๋กฌ ํ์ฅ ํ๋ฌ๊ทธ์ธ ํบ์๋ณด๊ธฐ | ์ฐ์ํํ์ ๋ค ๊ธฐ์ ๋ธ๋ก๊ทธ
- Creating a Chrome extension with React and TypeScript - LogRocket Blog
- Develop Chrome Extensions using React, Typescript, and Shadow DOM
๊ธ ์์ ์ฌํญ์ ๋ ธ์ ํ์ด์ง์ ๊ฐ์ฅ ๋น ๋ฅด๊ฒ ๋ฐ์๋ฉ๋๋ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด ์ฃผ์ธ์
'๐ช Programming' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
๋๊ธ
์ด ๊ธ ๊ณต์ ํ๊ธฐ
-
๊ตฌ๋
ํ๊ธฐ
๊ตฌ๋ ํ๊ธฐ
-
์นด์นด์คํก
์นด์นด์คํก
-
๋ผ์ธ
๋ผ์ธ
-
ํธ์ํฐ
ํธ์ํฐ
-
Facebook
Facebook
-
์นด์นด์ค์คํ ๋ฆฌ
์นด์นด์ค์คํ ๋ฆฌ
-
๋ฐด๋
๋ฐด๋
-
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
-
Pocket
Pocket
-
Evernote
Evernote
๋ค๋ฅธ ๊ธ
-
[Algorithm] ์์ด / ์กฐํฉ ๊ฐ๋ ๊ณผ ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ
[Algorithm] ์์ด / ์กฐํฉ ๊ฐ๋ ๊ณผ ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ
2024.05.28 -
[React] ๋ฆฌ์กํธ์์ setTimeout ๋ ํธํ๊ฒ ์ฐ๊ธฐ
[React] ๋ฆฌ์กํธ์์ setTimeout ๋ ํธํ๊ฒ ์ฐ๊ธฐ
2024.05.28 -
[Algorithm] ์๋ฐ์คํฌ๋ฆฝํธ Map์ผ๋ก ๊ตฌํํ๋ LRU Cache ์๊ณ ๋ฆฌ์ฆ
[Algorithm] ์๋ฐ์คํฌ๋ฆฝํธ Map์ผ๋ก ๊ตฌํํ๋ LRU Cache ์๊ณ ๋ฆฌ์ฆ
2024.05.28 -
[Algorithm] ์ต์ ํ(Heap)์ผ๋ก ์ฐ์ ์์ ํ ๊ตฌํํ๊ธฐ
[Algorithm] ์ต์ ํ(Heap)์ผ๋ก ์ฐ์ ์์ ํ ๊ตฌํํ๊ธฐ
2024.05.28