발단
회사 점심시간에 회사 분과 이야기를 하는 도중에
"Github Pull Request를 자동으로 승인해 주면 있으면 어떨까?" 하다가
간단하게 만들어보기로 하고 코드가 완성 되었습니다.
물론 이걸 가지고 승인을 하지는 않았습니다
저는 좋은 코드 리뷰 문화를 지향합니다 🤣
도전하실 분들은 팀원들의 원망을 각오하셔야 할지도...
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
(async () => {
const anchorElem = document.querySelector(".markdown-title");
const hrefValue = anchorElem.getAttribute("href");
const regex = /\/pull\/(\d+)/;
const match = hrefValue.match(regex);
const issueNumber = match ? match[1] : null;
document.querySelector(`#issue_${issueNumber} > div > a`).click();
await sleep(2000);
document
.querySelector(
"#repo-content-turbo-frame > div > div.flex-items-center.flash.flash-warn.width-full.mb-3.p-2.d-flex > a"
)
.click();
await sleep(2000);
document.querySelector("#review-changes-modal > summary").click();
await sleep(2000);
document.querySelector("#pull_request_review_body").textContent = "LGTM!!!";
await sleep(1000);
document
.querySelector(
"#review-changes-modal > div > div > div > form > div:nth-child(4) > div:nth-child(3) > label > input[type=radio]"
)
.click();
await sleep(1000);
document.querySelector(
"#review-changes-modal > div > div > div > form > div.form-actions.p-2.m-0.color-bg-subtle.border-top > button"
);
})();
대략 아래의 영상과 같은 동작을 하는 코드입니다
Extension 도입
여기서 puppeteer나 selenium을 이용하여 자동화를 할 수 있는 프로그램을 만들 수도 있겠지만
요번에는 Chrome extension을 이용해서 개발을 해보기로 결정했습니다.
평소에도 한 번 정도는 개발해보고 싶기도 했고 재밌어 보여서 시작하게 되었습니다.
1. Extension 개발 시작
익스텐션의 개발의 경우 아래의 크롬 공식문서에서 내용을 참고할 수 있습니다.
간단한 예제들과 사용법들을 확인하실 수 있습니다.
https://developer.chrome.com/docs/extensions/
그럼 크롬 익스텐션을 구성하는 요소들에 대해서 잠깐 확인해 보도록 하겠습니다.
1. mainfest.json
확장 프로그램들의 메타데이터를 담고 있는 파일입니다.
확장 프로그램의 이름, 설명, 버전, 권한 등등을 설정할 수 있죠.
node의 package.json과 약간 비슷하다고 바라보시면 이해하시기 편합니다.
react native를 할 때 native 안드로이드에서 봤던 기억이 있던 거 같은데 긴가민가 하네요.
2. popup
저희가 흔히 익스텐션을 사용하면 나오는 작은 화면입니다.
HTML, CSS, JS를 이용해서 아래와 같은 페이지를 개발할 수 있죠.
사용자에게 필요한 정보를 보여줄 수 있는 창이라고 할 수 있습니다.
3. content script
웹 페이지에 DOM에 접근할 수 있는 스크립트입니다.
웹 페이지의 콘텐츠를 읽거나, 수정할 수 있고, 사용자 인터페이스 변경하거나, 이벤트를 처리하는 데 사용됩니다.
주의점은
1) 지정된 URL 패턴과 일치하는 웹 페이지에서 실행되고
2) 웹 페이지와 독립된 별도의 JS 실행 콘텍스트에서 작동함
4. background
확장 프로그램의 코어 로직을 처리하는 데 사용되는 스크립트입니다.
background script는 확장 프로그램이 활성화되는 동안 지속적으로 실행되며
여러 페이지 간의 지속적인 정보를 저장하거나 전달하는 데 사용될 수 있습니다.
익스텐션의 모든 요소와 메시지를 주고받을 수 있다는 점이 가장 중요한데
content script의 경우 웹 페이지와 별도의 실행 콘텍스트를 가지기 때문에
background에서 중개자 역할을 해주어야 합니다.
이외에도 redux devtools 같은 패널을 개발할 수 있는 devtools도 있고
new tab을 구성할 수 있는 것도 있지만 요번 프로젝트의 메인 내용은 아니니
추후 다른 익스텐션을 개발하게 되면 다뤄보도록 하겠습니다.
익스텐션 개발의 경우 순수 HTML, CSS, JS를 통하여도 개발이 가능하지만
webpack, vite 같은 번들러의 도움을 받는다면 React로도 개발이 가능합니다.
Github에 보일러플레이트 코드가 몇 개 있는데
그중 Vite로 동작했던 아래 Repository를 사용하였습니다.
알고 보니 한국 개발자 분이시네요(대단...)
https://github.com/Jonghakseo/chrome-extension-boilerplate-react-vite
2. content script 작성
2-1. approvePR 코드 작성
요번 프로젝트의 코어 로직 부분입니다.
아까 content script 내용에서 확인하였듯이 웹페이지의 DOM에 접근할 수 있기 때문에
Github 페이지에 존재하는 모든 태그들을 가져올 수 있게 되는 거죠. 😃
// approvePR.ts
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const waitForElement = async (selector) => {
while (!document.querySelector(selector)) {
await sleep(100);
}
return document.querySelector(selector);
};
const approvePR = async () => {
const anchorElem = await waitForElement(".markdown-title");
const hrefValue = anchorElem.getAttribute("href");
const regex = /\/pull\/(\d+)/;
const match = hrefValue.match(regex);
const issueNumber = match ? match[1] : null;
// 1. 첫 번째 이슈 클릭
(await waitForElement(`#issue_${issueNumber} > div > a`)).click();
// 2. Files changed 클릭
(
await waitForElement(
"#repo-content-turbo-frame > div > div.clearfix.js-issues-results > div.px-3.px-md-0.ml-n3.mr-n3.mx-md-0.tabnav > nav > a:nth-child(4)"
)
).click();
// 3. Review changes 클릭
(await waitForElement("#review-changes-modal > summary")).click();
// 4. 텍스트 입력
(await waitForElement("#pull_request_review_body")).textContent = "LGTM!!!";
// 5. Approve 라디오 버튼 변경
(
await waitForElement(
"#review-changes-modal > div > div > div > form > div:nth-child(4) > div:nth-child(3) > label > input[type=radio]"
)
).click();
// 클릭은 제외 (테스트)
await waitForElement(
"#review-changes-modal > div > div > div > form > div.form-actions.p-2.m-0.color-bg-subtle.border-top > button"
);
};
export default approvePR;
주석에 달린 것처럼 순차적으로 로직을 수행하게 됩니다.
뭔가 Github 측에서 API를 제공해 줄 것 같지만 손수 태그를 태그를 따오는 재미?를 느껴볼 수 있습니다.
혹여나 저 선택자들을 어떻게 가져오는지 궁금한 분들을 위해 말씀을 드리자면
위와 같은 방법으로 selector를 복사하여 찾을 수 있습니다.
2-2 메세징 코드 작성
content script는 별도의 실행 콘텍스트를 가지기 때문에
아래와 같은 onMessage를 통해 사용자에게 보일 popup과 통신을 할 수 있습니다.
chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
if (request.action === "approvePR") {
await approvePR();
}
});
3. Popup 코드 작성
이제 사용자와 소통할 수 있는 Popup 코드를 작성해 볼 차례입니다.
아까 2-2에서 메시지를 받아 approvePR을 실행시키는 로직을 작성하였기 때문에
여기서는 메시지를 보내는 로직을 작성해야겠죠?
import "@pages/popup/Popup.css";
const Popup = () => {
const onClickApprove = () => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
chrome.tabs.sendMessage(tabs[0].id, { action: "approvePR" });
});
};
return (
<div className="App">
<header className="App-header">
<button onClick={onClickApprove}>Approve</button>
</header>
</div>
);
};
export default Popup;
근데 왜 script에서는 chrome.runtime.sendMessage를 통해서 메시지를 처리하고
popup에서는 chrome.tabs.query를 쓸까요?
이건 script와 크롬 탭이 가지는 특성 때문에 그렇습니다.
- chrome.runtime.sendMessage : 익스텐션 내의 구성 요소끼리 메시지를 주고받을 때 사용
- chorme.tabs.query : 특정 탭을 찾아 그 탭과 관련된 작업을 수행
아래와 같이 저희는 여러 탭들을 두고 인터넷을 사용할 수 있죠.
아까 script는 각각의 실행 콘텍스트를 가지기 때문에 여기서
저희가 익스텐션이 메시지를 보내도록 바라는 탭을 특정 지어 통신을 해야 하기 때문에 그렇습니다.
결과물
뭔가 못생기긴 했지만 다음과 같은 popup이 만들어졌습니다.
이제 이 버튼을 누르면
1. popup -> script (approvePR 메시지 전송)
2. script 메시지 수신
3. approvePR 실행
4. 로직 수행
으로 마무리가 되게 됩니다.
평소에 많이 사용하면서 한 번쯤은 만들어보고 싶던 익스텐션을 다뤄보니
재밌다는 생각이 많이 들었던 거 같습니다.
앞으로 뭔가 재미난 게 생각난다면 애용하게 될 것 같네요
메시지를 주고받는 방식이 서버 통신과 달라 조금은 생소하긴 했지만
사용자 편의성을 높이기 위한 키를 하나 얻은 느낌입니다.
끝으로 개발 중인 LGTM 레포 주소를 남기고 다음 크롬 익스텐션 탐방기로 찾아뵙겠습니다 😃
https://github.com/ww8007/LGTM
'Chrome Extension' 카테고리의 다른 글
swagger chrome extension을 만들어보며 (1) | 2023.07.01 |
---|