마오지? Jira 일감을 크롬 확장앱에서! [설계자편]

JeonYB
CJ 온스타일 기술 블로그
16 min readOct 17, 2023

“차렷 배꼽인사~!”

안녕하세요, CJ온스타일 프레임워크와 각종 플랫폼, 아이스 브레이킹과 메이킹을 담당하고 있는 영배입니다.
우리 기술 블로그에 올리는 첫 글이네요, 앞으로 더 자주 올릴 수 있도록 노력하겠습니다.

마오지 [기획자편]에 이어 [설계자편]을 게시하게 되어 영광입니다 😄

“마오지가 뭔가유?”

마오지(MyOpenJira) 서비스는 JIRA플랫폼에 대한 접근성과 업무 생산성을 높인 도구입니다.
Chrome Extension으로 제작하여 Chromium 기반 브라우저에서 사용할 수 있습니다. (참고로 웹스토어에 없습니다.)

JIRA에 로그인 되어있다면, 마오지 확장 앱에서 JIRA API를 통해 새로운 이슈, 업데이트 된 이슈가 있는지 주기적으로 체크하여 크롬 뱃지로 알려줍니다.

마오지 앱을 누르게 되면 팝업으로 ( 오픈 | 진행중 | 끗) 상태의 이슈 등을 분리하여 나/님의 이슈를 직관적으로 알 수 있습니다.
게다가 새로운 코멘트까지 확인할 수 있다니 이처럼 편리할 수가..?

Playing 마오지 on whale browser

“왜 만들었슈?”

저희 CJ온스타일에 지라가 도입된 지 그리 오래되지 않았기에, 아직 익숙지 못하신 분들이 많아 “오픈 미처리 이슈”가 많다는 고민에서부터 시작하였습니다.

아울러 이 고민을 [DT:Lab]이라는 조직 내 스터디 문화를 활용하여 관심 있는 분들(하늘 , EUNJI CHOI) 과 모여 스터디랩의 아웃풋으로 나오게 되었습니다.

“워터케 만들었슈?”

각 잡기

마오지 팀에서는 [DT:Lab]의 스터디 포맷에 맞춰 일정을 준비하였습니다.
(랩장
하늘 님 스터디문화도 기술블로그 써주실꺼죠?)

그리고 어플리케이션의 기획 및 설계, 개발에 대한 개략적인 내용 공감을 통해 우리가 어떤 서비스를 만들어야 하는가? 에 대한 목표를 다지는 시간을 가졌습니다.

스타디

또한 마오지 어플리케이션은 Jira의 데이터를 서비스하는 크롬 확장앱이기 때문에 두 가지를 공부해야 했습니다.

- JIRA API
- Chrome Extension

🌒 JIRA API

정의된 기능들을 만족하기 위해 어떤 API들이 필요할까?

(사용하는 Jira의 플랫폼과 버전을 확인하여야합니다!)

JIRA에서 제공하는 API를 찾아보았고 Search, Get issue, Get Comments 정도가 추려졌습니다.

🌒 Chrome Extension

구글링을 통해 검색 좀 해보니…

manifest.json, background.js, popup.html 끗

어라, 어렵지 않은데?

드디어 개발할 시간

먼저 manifest.json 작성 background.js 구성 화면 등 좋은 사례를 찾기 위해
깃헙의 Topics으로 “chrome-extension” 프로젝트를 찾아보았습니다.

깃헙을 돌아보는데.. 아니 자바스크립트가 이렇게 어려웠나?

Vue, React, Typescript… 들어보기만 한 것들의 소스를 보니.. 머리가 어질어질했습니다.

프레임워크까지 적용하기엔 6주란 시간이 짧고 스터디의 본질을 벗어나는 것 같아서 자바스크립트 프레임워크 적용은 과감히 포기했습니다. (핑계)

깃헙에서는 크롬 API를 어떻게 사용하고 manifest.json 등의 구조를 확인해 보았습니다.

자 일단 한번 앱이 돌아가게끔 만들어 보겠습니다.

🌒 샘플 백그라운드

생 자바스크립트로 background.js를 작성해 보았습니다.

// 지라 오픈이슈 건수만 갖고오기
var getCount = ()=> {
var url = "https://JIRADOMAIN/jira/rest/api/2/search?jql=assignee%20%3D%20currentUser()%20and%20status%20%3D%20Open&fields=key&maxResults=0";
let data = fetch( url, { method:'GET'})
.then((response) => response.json())
.then((data)=>{
// 뱃지에 open 건수 노출
chrome.action.setBadgeText({ text: (data.total)});
})
}

// 알람 설정 1분마다!
chrome.alarms.create('openIssueCntAlarm', {periodInMinutes: 1});

// 1분마다 getCount 사용하여 오픈이슈 건수 노출
chrome.alarms.onAlarm.addListener(alarm => {
if (alarm.name === 'openIssueCntAlarm') {
getCount();
}
});
알람으로 뱃지가 동작한 결과

뱃지가 잘 동작하는 것을 확인하였고, 브라우저에 JIRA 로그인이 되어있다면 딱히 header를 통해 추가 인증 절차는 안 해도 되는 것을 확인하였습니다.

ServiceWorker 에서 백그라운드 요청을 할 때 브라우저가 관리하는 쿠키 정보를 같이 보내준다는 뜻이 될 것 같습니다.

🌒 샘플 팝업화면

<!DOCTYPE html>
<html lang=en>
<meta charset=utf-8>
<head>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div id="container" class="text-center">
<div class="row">
<div class="col" id="tab_1">OPEN</div>
<div class="col" id="tab_2">PROGRESS</div>
<div class="col" id="tab_3">CLOSE</div>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
텅 빈 popup.html

음.. 잘 뜨는것은 확인 하였습니다. 나머지 그리는 영역은 은지님께 부탁을 하고…ㅎㅎ

일단 화면에서 데이터를 요청하고 받는 구조가 필요했습니다.

🌒 구조 설계

결과적으로 이런 구조

View 영역에서는 최대한 화면 그리는 일에 집중하였고, 데이터요청은 backgroundHandler.js 를 통해 정리하였습니다.

Module 영역인 backgroundHandler.js 에선 화면에서 필요한 데이터를 요청받아 서비스 워커에게 전달하고 응답을 되돌려주는 창구 역할을 하였습니다.
아무래도 화면과 서비스 워커 간 인터페이스가 많아지다 보니 데이터를 요청하는 방식과 응답에 대한 정리가 필요하였습니다.

요청 시 데이터 구조는 쓰이는 화면 영역과 기능, 요청 데이터로 구분하였으며 chrome.runtime.sendMessage() api를 통해 전달합니다.

// popup 화면 영역에서 데이터 요청 형식
{
area : "popup",
key : "getIssue",
data : "MYPROJECT-112"
}
// Background.js 소스

// 백그라운드로 요청
async send(payload){
let result = await new Promise((resolve, reject) => {
chrome.runtime.sendMessage({
area: payload.area
, key: payload.key
, data: payload.data
}, (response) => {
if( response === undefined || response?.hasError){
reject(response);
}else{
resolve(response);
}
});
});
return result;
}

// 화면에서 사용하는 펑션 (이슈타입에 따른 건수 조회)
async getIssuesTotalCnt(issueType){
return await this.send({
area : MsgArea.COMMON
, key: MsgKey.ISSUE_TOTAL_COUNT
, data : issueType
});
}

서비스워커에선 backgroundHandler로부터 받은 요청을 처리하고 chrome.runtime.onMessage의 sendResponse()를 통해 전달합니다.
서비스 워커가 받는 요청은 주로 JIRA API로 검색, storage에 저장하고 조회하는 캐싱 등이 있습니다.
그리고 새로 등록된 이슈가 있는지 체크하며 뱃지에 오픈 건수, 새 이슈가 있을 경우 색으로 강조해 줍니다.

// background.js 소스

// 1분마다 발생하는 오픈이슈 카운트
chrome.alarms.onAlarm.addListener(alarm => {
if (alarm.name === 'openIssueCntAlarm') {
this.getMyOpenJiraCount();
}
});

// 새롭게 오픈된 이슈가 있으면 뱃지에 표시해준다.
async getMyOpenJiraCount() {
// 오픈이슈 갖고오는 JQL
let jql = new Jql()
.andNotLabels(igLabels)
.and()
.statusCategory(StatusCategory.TO_DO)
.orderBy("CREATED").getQuery();
// JIRA search API 콜
let badgeData = await this.jiraAPI.search(jql, {
fields: [Type.KEY, Type.UPDATE_DATE]
, maxResults: 1
});

// 뱃지 세팅
Com.setBadgeColor(BadgeColor.DEFAULT);
Com.setBadgeText(badgeData.total);

if ( ! Com.isEmpty(badgeData.issues) ) {
// 마지막 서치한 이슈의 수정시각(updated) 캐시에서 갖고오기
const cacheUpdated = await commonHandler.getCache(cacheKey) || '0';
// 지금 조회한 이슈의 수정시각
const updated = new Date(badgeData.issues[0].fields.updated).getTime();
if ( updated > cacheUpdated ) {
Com.setBadgeColor(BadgeColor.RED);
commonHandler.setCache(CacheKey.ALERT_COMMENT_MY_IS_ALERT, true);
}
}
}

마오지의 기능은 JQL통해 JIRA search API를 사용하는 경우가 많습니다.
따라서 JQL 작성할 일이 많은데, 이것을 String으로만 관리하기엔 보기에도 쓰기에도 편치 않아 JQL을 제공하는 클래스를 만들었습니다.

class JQL{
constructor() {
this.query = ``;
}
append(str){this.query+=str}
clear(){
this.query=``;
return this;
}
and(){
this.append(AND)
return this;
}
in_( arr ){
this.append(`${IN} (`)
arr.forEach(s => this.append(`"${s}" ,` ))
this.query = this.query.substring(0,this.query.length-1)
this.append(") ")
return this;
}
not_(){
this.append(NOT)
return this;
}
assignee(user){
this.append(` assignee = ${user} `)
return this;
}
myIssue(){
this.append(`assignee = currentUser() `)
return this;
}
watchIssue(){
this.append(`watcher = currentUser() and (assignee != currentUser() or assignee is EMPTY)`)
return this;
}
allIssue(){
this.append(`(assignee = currentUser() or reporter = currentUser() or watcher = currentUser())`)
return this;
}
statusCategory(statusCategory) {
this.append(`statusCategory = "${statusCategory}" `)
return this;
}
search(searchText){
this.append(`summary ~ ${searchText} `)
return this;
}
orderBy(condition, type){
this.append(`order by ${condition} ${type || ""}`)
return this;
}
getQuery(){
return Com.enc(this.query);
}
}

JQL 클래스의 사용

/** 
* 검색 Jql
*/
getSearchJql(searchText){
return new JQL()
.allIssue()
.and()
.search(`"${searchText}"`)
.orderBy("UPDATED")
.getQuery();
}

그리고 backgound에서 JIRA API를 호출하는 클래스도 만들었습니다.

class JiraAPI {
constructor() {
this.API_URL = "https://CJENMJIRA.COM/rest/api";
}

search(jql, option) {
if (!option) {
option = {
startAt: 0
, fields: [Type.KEY, Type.SUMMARY, Type.ASSIGNEE, Type.REPORTER, Type.UPDATE_DATE, Type.CREATE_DATE, Type.STATUS, Type.ISSUETYPE, Type.COMMENT]
, maxResults: 20
}
}
return fetch(`${this.API_URL}/2/search?jql=${jql}&${Com.json2QueryParam(option)}`, {method: 'GET'})
.then(this.responseHandler);
}

getIssue(id) {
return fetch(`${this.API_URL}/2/issue/${id}`, {method: 'GET'})
.then(this.responseHandler);
}

getMySelf() {
return fetch(`${this.API_URL}/2/myself`, {method: 'GET'})
.then(this.responseHandler);
}
responseHandler(res) {
if (res.status != 200) {
throw new Error('failed jira api');
}
return res.json();
}
}

이로써 화면에서 필요한 데이터를 조금 더 효과적으로 관리할 수 있도록 구성하였습니다.

후기

초기 마오지 팀원들과 6주라는 기간에 우리가 정의한 기능을 다 구현할 수 있을지 생각해 보았는데..
힘들겠다… 마이 힘들겠다 예상되었습니다.ㅎㅎ

하지만, 은지님과 하늘님, 윤구님의 넘치는 열정으로.. 주요 기능은 한 4주차? 에 마무리되었고!!
추가 아이디어를 적용하고, 비효율적인 기능들을 수정하며 성능까지 잡아 완성도를 300% 끌어올렸습니다.

뛰어난 팀웍과 대단한 열정, 넘치는 능력! 함께하는 시간 내내 영광이었습니다.

이후로 스터디를 마친 우리는 마오지를 오픈하고, 담당 타운홀에서 발표를! 사업부 타운홀에서 수상을 받는 값진 경험을 하였습니다

그리고 마오지 팀원들과 맛있는 회식!

This delicious picture has nothing to do with 마오지 dinner menu

기술블로그 설계자편 쓰는게 너~~~~~~ 무 늦어졌는데 죄송합니다 ㅠㅠㅠㅠ 바빴어요 ㅠㅠㅠ (귀찮음 1 바쁨 99)

글 다쓰고 읽어보니 뒤죽박죽 엉망진창이네요 ㅎㅎ 다음 블로그는 더 잘 써보겠습니다!

CJ온스타일 화이팅!!!

[ DT:Lab ] 팀 마오지, CJ ENM COMMERCE DIV.

--

--

JeonYB
CJ 온스타일 기술 블로그

백육십자까지적을수있으면어디한번끝까지적어보자과연이게저장이잘되고노출도잘되는지안되는지확인을위해서는이렇게길게도적어보고저렇게도적어보고해야지미디엄이과연이거를진짜백육십자까지적을수있게끔해놨을까두근두근세근네근내엠비티아이는이이이뭐더라길게쓰기힘드네아이스아메리카노는음료수고따듯한아메리카노가커피입니다여러분덥죠훗