Chrome Extension 개발 이제 여기에 react를 끼얹은...
검색검색을 하다가 비교적 최신의, 귀여운 react로 만든 chrome extension 프로젝트를 찾았다.
이 문서는 아래 아티클에 대한 번역 + 따라가는 과정이 담겨있습니다.
https://react.christmas/2020/12
Chrome Extension의 기초
extension에는 세가지 메인 컴포넌트가 있습니다. popup, content script, background script입니다.
- Popup은 extension icon이 눌리면 뜨는 콘텐츠입니다.
- Content script는 Javascript나 CSS로 된 코드이고, 현재 웹 페이지의 context에 주입(inject)됩니다.
- Background script는 브라우저에서 분리된 인스턴스로 실행되는 javascript 코드입니다. 그리고 거의 이벤트를 listening하고, 브라우저를 다루기 위해서 사용됩니다.
(저) 아티클에서는 react와 typescript를 이용하여 chrome extension을 만들어 보도록 하겠습니다.
프로젝트 셋팅
create-react-app으로 리액트 프로젝트를 시작하면 쉽지만, chrome extension으로 개발하기에는 좀 까다로운 편이어서 저 아티클에서는 Webpack을 기반으로 리액트 프로젝트를 구상합니다.
미리 만든 보일러플레이트를 기반으로 우선 디렉토리를 구상합니다.
$ npx degit https://github.com/sivertschou/react-typescript-chrome-extension-boilerplate.git#christmas snow-extension
npx degit 명령어는 clone을 하고, 그 repository의 git 파일들을 삭제합니다. 따라서 위 git repo의 파일을 순수하게 (기존 git dependency 없이 사용할 수 있습니다)
이 프로젝트는, react의 build 결과 -> dist 폴더를 extension으로 업로드 하도록 구성되어 있습니다.
그래서 일단 manifest를 조작할 수 있는 public부터 보시죠
public
public 폴더에는 extension의 meta data가 되는 애들이 들어갑니다.
icon 이미지 파일들, popup으로 띄울 - react의 mounting point인 popup.html, extension의 config가 되는 manifest.json
manifest.json이 extension개발의 가장 중요한 파일이므로 이 파일을 먼저 봅시다.
우선, metadata가 되는 name, description, manifest_version, version을 설정합니다.
(여기서는 manifest_version을 2로 설정했네요)
그리고 아이콘은 적어도 16, 48, 128에 대해서는 제공해야만 합니다.
// public/manifest.json
{
"name": "Snow Extension",
"description": "Chrome Extension for making it snow in your browser!",
"manifest_version": 2,
"version": "1.0.0",
"icons": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
},
...
extension을 클릭 할 때 띄울 popup에 대한 설정은 browser_action이나 page_action으로 가능합니다.
page_action은 특정 페이지에서만 뜨도록 설정되는데, 그에 반해 browser_action으로 설정하면 어떤 페이지든 브라우저에서 popup을 띄울 수 있으므로, 여기서는 browser_action으로 popup에 대한 설정을 하겠습니다.
browser_action의 default_icon은 브라우저에서 클릭 가능한 아이콘의 이미지를 의미합니다. 우리가 클릭하는 바로 그 아이콘 말이죠.
그리고 defuault_popup 은 그 아이콘이 클릭될 때 뜰 popup에 대한 설정 정보입니다. 여기서는, React 코드를 실행할 그 popup.html이죠.
// public/manifest.json
...
"browser_action": {
"default_icon": {
"16": "icon16.png",
"48": "icon48.png"
},
"default_popup": "popup.html"
}
...
background script는 아래와 같이 설정합니다.
.ts가 아닌 .js로 쓴 것은 이 script는 그대로 browser에서 실행되는 것이어서, ts로는 실행이 불가합니다. 우리는 .ts로 파일을 만들더라도 이게 js로 빌드된 결과물이 들어가야하죠.
persistent: false 설정은, 이 background script를 항상 불러올지, idle일땐 업로드하지 않을지에 대한 설정입니다. 여기서는 false로 하면서 extension이 idle 상태일 때 background script가 로딩되지 않게 설정하였습니다.
// public/manifest.json
...
"background": {
"scripts": [
"background.js"
],
"persistent": false
},
...
content_scripts 에서는 "특정" 웹 페이지에서 실행될 script를 설정합니다. matches를 통해 해당 script가 주입되고, 실행 될 페이지 주소를 지정할 수 있고, 여기서는 모든 페이지로 설정했습니다. background script 설정과 같이 content_scripts로 지정하는 js 역시 .js 포맷이어야 합니다.
// public/manifest.json
...
"content_scripts": [
{
"matches": [
"*://*/*"
],
"js": [
"content.js"
]
}
]
}
src 둘러보기
우리가 필요할 파일은 popup.html에 들어갈 popup.js(보통의 react 프로젝트에서의 index.js), background.js, content.js 입니다.
물론 우리는 이 프로젝트를 ts로 개발 할 것이기 때문에, 빌드 결과물이 그렇게 나와야겠죠. - 이 부분은 webpack의 도움을 받읍시다.
popup.tsx로 index.js와 같은 구성을 하고 - document의 특정 id에 <App/>을 root로 두는 React component를 뿌립니다. - 그 이하 App의 구성 component들이 있겠죠. 여기서는 App.tsx로 충분했습니다. 따라서 다음과 같은 파일 구성이 되었습니다.
- App.css
- App.tsx
- background.ts
- content.ts
- custom.d.ts
- logo.svg
- popup.css
- popup.tsx
Content script 작성하기
현재 content script는 console.log를 찍는 한 줄 뿐입니다. 지금대로면 모든 페이지 진입시마다 content.ts가 실행되고, console log가 찍히는 것을 볼 수 있습니다.
console.log("Hello from content script!")
여기다가 눈이 내리게 하는 코드를 끼얹으면 끝! 이 이하는 코드 복붙으로 완성되어서 넘어갑니다. - 리액트 관련된건 넘어갔으니!
Webpack 설정 보기
여기서는 3개의 entry가 필요합니다. 각각 popup.tsx, content.ts, background.ts 이죠. 그리고 각각 그 이름으로 된 js 파일로 output 되어야 합니다.
const config = {
entry: {
popup: path.join(__dirname, "src/popup.tsx"),
content: path.join(__dirname, "src/content.ts"),
background: path.join(__dirname, "src/background.ts"),
},
output: { path: path.join(__dirname, "dist"), filename: "[name].js" },
module로는 js babel-loader, css loader, ts loader, svg loader, png loader가 들어갔습니다.
...,
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: "babel-loader",
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
exclude: /\.module\.css$/,
},
{
test: /\.ts(x)?$/,
loader: "ts-loader",
exclude: /node_modules/,
},
{
test: /\.css$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
importLoaders: 1,
modules: true,
},
},
],
include: /\.module\.css$/,
},
{
test: /\.svg$/,
use: "file-loader",
},
{
test: /\.png$/,
use: [
{
loader: "url-loader",
options: {
mimetype: "image/png",
},
},
],
},
],
},
아래 설정은 참고 할만 한 것 같습니다
resolve: {
extensions: [".js", ".jsx", ".tsx", ".ts"],
alias: {
"react-dom": "@hot-loader/react-dom",
},
},
devServer: {
contentBase: "./dist",
},
plugins: [
new CopyPlugin({
patterns: [{ from: "public", to: "." }],
}),
],
여기까지 코드를 바탕으로 react, typescript로 chrome extension 개발하는 틀을 잘 잡을 수 있을 것 같군요 :b