작성 배경
우아한형제들에 이직한지 어느 덧 7개월이 넘었다. 큰 프로젝트들이 성공적으로 배포되고 드디어 여유가 생겨서, 특정 서비스를 개발-테스트 하는 과정에서 개발자 뿐만이 아니라, 기획/QA 쪽에서도 생산성 향상을 기대할 수 있는 부분들을 추려두었다가 간단한 업무용 구글 확장프로그램으로 구현하게 되었다. 이를 기억보단 기록하고자 글을 작성한다.
크롬 익스텐션에 대해 기본적인 지식이 있다는 가정하에 쓰여진 글 입니다. (아주 기본적인 것을 자세하게 설명하지는 않음)
개발 관련된 것만 살펴보고 싶으시면, 스크롤을 내리셔서 개발과정 정리 또는 블로커만 참조해주세요.
먼저 어떤 서비스를 개발하고 있나요
큰 틀에서 현재 2가지 서비스를 개발하고 있다. (반응형으로 데스크탑-모바일 멀티지원)
셀프서비스 대시보드
나는 우아한형제들 B2B서비스실 셀프서비스팀에 소속되어있고, 주요 업무는 사장님들이 배달의민족(이하 배민) APP 가게에 서빙하는 모든 것들을 스스로 손쉽게 설정/관리하고, 주문내역, 광고효과, 통계 등 분석 데이터들을 파악하기 쉽게 제공하여 조회 할 수 있게 도와드리는 서비스이다.
온라인 전자계약서(=OLS)
사장님들이 원한다고 즉시 배민 APP에 소유한 가게를 서빙할 수 있는 것이 아니다. 일련의 과정(굉장히 복잡하고, 받아야할 정보들도 많고, 인증 과정 등 호흡이 길다)을 통해야만 배민 APP을 통해 서빙 될 수 있다.
이때, 기존 외부 영업사원과 내부 담당직원이 직접 의사소통을 통해 어드민 툴에서 진행되던 것을, 사장님이 직접 배민사장님광장을 통해 필요한 데이터를 작성해 입점(배민 APP 노출될 가게 등록) 할수 있도록 도와주는 서비스이다. 이때 폼이 적지 않아서, 스텝(입력해야하는 폼들을 관심사에 따라 UI를 각 스텝으로 분리)이 굉장히 긴데 이 때 중간에 끊었다가 다시 이어서 할 수 있도록 임시저장 기능을 지원하고 있다.
당장 개발한 구글 확장프로그램의 지원범위는 온라인 전자계약서만 지원하며, 추후 니즈가 생기면 셀프서비스 도메인쪽으로도 기능을 확장하려고 한다.
[개발환경 only] OLS를 위한 익스텐션 만들게 된 이유
위에서 살짝 언급했듯, 온라인 전자계약서의 데이터 구조는 굉장히 복잡하다. 이는 우아한형제들의 긴 업력 속에서(초기부터 전자계약, 기존 레거시 코드들의 호환성을 유지하면서 새로운 요구사항을 일정에 맞춰 충족시키는 수 많은 과정이 반복적으로 있었을 것) 필연적으로 수반되는 일이 였을 것이다.
이로인해 다음 스텝으로 넘어가거나, 스텝을 아예 벗어날때 데이터를 저장하는 POST API 가 호출되고, 실제 API 통신전에 데이터를 한번 말아주는 과정(refine or clean-up)을 거치게 된다(Mapper writer layer 같은 느낌). 이는 임시저장을 위해서도 있지만, 사장님이 일정 스텝에서 멈춘후 진행을 안 하실때 상담원이 전화해서 여쭤보고 어드민 툴에서 이어서 마무리할 수 있도록 도와주시는데, 어드민에서도 이어서 작성할 수 있도록 데이터를 처리해주어야한다. (어드민이 셀프서비스의 슈퍼셋) 사전 설명이 장황하지만 그만큼 경우의 수도 굉장히 많고 복잡한 프로덕트이다.
다시 글의 목적으로 돌아와, 현재 상황에서 우리팀의 생산성 향상에 도움을 줄 수 있는 것은 무엇일까? 라고 생각해보니 아래 두가지가 있었고, 이를 개선하기 위해 크롬 익스텐션을 개발했다. (dev 환경 only)
- 임시저장 API 호출 시 리퀘스트 바디에 담긴 데이터를 파싱해서 보여주자. (근데 워낙 트리가 깊다보니 원하는 부분만 보여줄 수 있는 기능을 얹은..)
- 많은 입력폼들중 귀찮은 것들 자동 채워주기 위한 플래그 지원
먼저 아래 개발되어 있는 것을 살펴보자. (아직 기능이 많지는 않다. 여유시간이 생기게 되면 짬짬히 고도화 + 셀프서비스 확장도 염두해 두고 있어서, 미리 탭을 두었다.)
개발 과정 정리
방향성은 데이터를 보여주기 위해 해당 제품자체에 추가코드를 끼얹지 말자였다. 즉 크롬익스텐션이 알아서 특정 API 를 hooking해 리퀘스트 바디를 파싱하는 형태로 접근했고. 익스텐션 형태는 팝업형태를 원했다.
이를 위해 개발해야하는 것들 큰 꼭지를 나누자면 아래와 같다. 아래 더 세부적으로 다룰텐데, 전체를 다루진 않고 유의사항만 기록용으로 담으려고 한다.
*번들러 +개발환경(React + typescript)설정
(나같은 경우엔 감을 잃지 않을 겸, 최신 버전들도 사용해보고 싶어서 직접 다 세팅했지만, 보일러 플레이트도 있다고 한다.)* manifest.json 작성 (메타데이터들)
* background.ts(js)작성 (크롬과 소통 — 상황에 따라 content scripts 만 사용할 수 도 있음)
* popup 에 쓰일 popup.html + React 코드 작성
번들러 +개발환경(React + typescript)
사용한 번들러: webpack@5.38.1 (최신 버전 웹팩 사용해보고 싶어서 rollup 은 사용하지 않았습니다.)
기록1) zip-webpack-plugin — crx는 필요없고, 빌드된 결과물을 zip 파일로 받아, 크롬 익스텐션 개발자모드에서 드래그해서 동료들이 나누어 사용해도 충분하다고 생각 했기에, 번들러가 zip 파일로 내보낼 수 있도록 production build script 에 해당 플러그인을 추가하였다.
기록2) copy-webpack-plugin — 크롬 익스텐션의 경우 정해진 위치에 정해진 파일들이 있어야하는데, assets/* 와 같이 조금 정리를 하고 싶어서, 빌드시 정해진곳으로 알아서 위치를 옮겨주는 플러그인을 적용하였다.
좋았던점: 내가 사용한 webpack 버전에서는, 사용하지 않는 imported 모듈들이 존재할때 에러를 뱉고 알려주어서 좋았다.
manifest.json 작성(메타데이터)
{
"manifest_version": 2,
"name": "OLS-helper",
"description": "A chrome extension for OLS",
"version": "$", /** background scripts */
"background": {
"page": "background.html",
"persistent": true // 백그라운드에서 webRequest hooking 위해 true 필요
}, /** 아이콘 + 팝업 관련 html(안에 reactjs 빌드 결과물 서빙) */
"browser_action": {
"default_icon": "icon.png",
"default_popup": "popup.html"
}, /** 미리 기재되지 않은 명세들은 사용 불가 */
"permissions": [
"cookies", // 쿠키
"storage", // 크롬 스토리지
"webRequest", // 웹 리퀘스트
"activeTab", // 활성화 된 탭 추적
"clipboardRead", // 클립보드 리더
"https://..." // 사용할 도메인
]
}
background.ts 작성
특정 API 를 hooking해 리퀘스트 바디를 파싱하기 위해 아래 처럼 코드를 작성하였다. (코드 중 일부)
chrome.webRequest.onBeforeRequest.addListener(
/** 첫번째 인자: listener */
(details) => {
if (isPostEsignAPI(details)) {
const array = new Uint8Array(details.requestBody.raw[0].bytes);
const esignJSON = convertUint8ArrayToUTF8String(array);
StorageHelper.setStorage({
esignJSON,
timeStamp: details.timeStamp
});
}
}, /** 두번째 인자: url filter */
{ urls: [...필터링할 urls] }, /** 세번째 인자: 옵션들(requestBody - detail에 reqBody 포함여부) */
['requestBody']
);
이를 이용해 StorageHelper 를 별도로 만들어서(전역 저장소 같은 역할 — 필자는 chrome stroage 사용) setStorage 를 통해 받아온 request body 를 파싱후 저장해 데이터 트리를 그릴 때 사용했다.
React + Typescript 를 이용해 팝업 구현
특별하게 정리할 것은 없고, React + Typescript 를 이용해 UI 를 구현했다.
(+ styled-components + antd)
블로커 및 해결방안
- 리퀘스트 바디를 받아오면 ArrayBuffer 의 형태로 온다. 이를 순차적으로 Uint8Array로 변환 이후 string 으로 변환할 때 나같은 경우 utf-8 인코딩이 추가적으로 필요했다.(데이터 특정필드들 값이 한글이 있음) 이를 위해 함수를 하나 만들어서 해결했다.
/** Conversion: UInt8Array to utf-8 string */
function convertUint8ArrayToUTF8String(arr: Uint8Array) {
const utf8 = Array.from(arr)
.map((item) => String.fromCharCode(item))
.join(''); return decodeURIComponent(escape(utf8));
}
2. 기록을 개발먼저하고 추후에 정리할 겸 하다보니까 기억이 안나는데(ㅠㅠ흑흑), babel-polyfill 을 넣어줘야 error 가 발생하지 않는 경우가 있었다. (추후 기억나면 다시 글 수정하겠습니다.)
3. 일부러 감을 잃지 않기 위해 직접 바닥부터 프로젝트 개발환경 + 번들러를 한땀 한땀 세팅하다보니 시간이 꽤 걸렸었다.
맺으며
오랜만의 글 작성은 정말 어렵다…