Eli의 여백

바쁜 나날들 사이에서 생각났던 이런저런 것들을 적어봅니다.

프로그래밍

manifest v3 Chrome extension에서 service worker와 indexedDB를 올바르게 사용하기

Eli♪ 2022. 2. 16. 06:56

들어가며

최근에 크롬 익스텐션 만들 일이 하나 생겨서 좀 찾아보니 표준이 manifest v3로 버전 업이 되어있었다. 마지막으로 chrome extension 관련해서 깨작거려본지 몇 년 된지라 역변한 기술을 받아들이기가 힘들었다. 그래도 신문물을 써보려고 가이드를 참고하여 manifest.json에 version 3을 입력하고 background.js도 worker로 변경을 했는데 보안 이슈로 웬만한걸 다 막아놔서 extension에서 접근이 안되는게 너무 많았다. 특히 특정 사이트를 스크래핑해서 DB에 정리하고 그 DB를 써서 다른 페이지의 html을 변경하는 것이 목적이었는데 자꾸 CORS인지 하는 보안경고를 띄우니 답답 그 자체였다. 몇시간의 삽질 끝에 정석적으로 올바르게 통신하는 chrome extension을 만든 것 같아서 구현 과정에서 깨달은 점을 정리해 본다.

 

manifest v3에서 service worker는 background.html을 가지지 않음. 따라서 document에도 접근 불가. alert도 불가. 대신 desktop notification 사용

기존에 manifest v2 앱에서는 background.html을 가지기 때문에 그 안에 정보를 세팅하고 background.js에서 해당 문서의 document에 접근할 수 있었는데 manifest v3의 service worker는 갖고있는 페이지도 없고, 항상 돌지도 않는다. 즉 v2에서 쓰던 방법이 전혀 안먹힌다. 또한 페이지가 없기 때문에 window.alert도 쓸 수 없다. alert를 못쓰는 대신에 desktop notification을 다음과 같이 사용하면 알림을 표시할 수 있다.

 

alert 대신 desktop 알림을 표시하기 위해 manifest.json 파일에 permission으로 notifications를 넣어 준다.

// manifest.json
{
    ...
    "manifest_version":3,
    "permissions": [
        "notifications"
    ],
    "background": {
        "service_worker":"background.js"
    },
    ...
}

 

데스크톱 알림은 c# window.Forms app에서의 NotifyIcon.ShowBallonTip과 동일하게 생긴 메시지를 띄울 수 있다. notification Id (숫자, 문자 다 됨)로 구분하며, 버튼을 넣을 수도 있고, 예약 시간을 걸 수도 있다. 명시적으로 clear하지 않으면 알림에 계속 남아있고, 누르면 없어진다. dwellingTime 뒤에 사라지는 걸 만들었는데 동일 id에 대해 그보다 짧은 간격으로 반복 알림시 이전껄 지워야 새로 나와서 앞쪽에 클리어를 먼저 넣었다. 알림에 버튼 넣는것도 가능하다.

// background.js
function sendNotification(notiId,msg,dwellingTimeMs) {
    //clear noti for fast notification
    chrome.notifications.clear(
        notificationId= notiId,
        callback= () =>{}
    );

    chrome.notifications.create(notiId, {
        type: 'basic',
        title: 'mytitle',
        iconUrl: "Notification.png",
        message: msg,
        priority: 2, // -2 to 2 (highest)
    
        // buttons: [
        //     {
        //         title: '저장'
        //     },
        //     {
        //         title: '취소'
        //     }
        // ],
    
        eventTime: Date.now()
    });
    
    setTimeout(() => {
        chrome.notifications.clear(
            notificationId= notiId,
            callback= () =>{}
        );
    },dwellingTimeMs);
}

 

manifest v3의 service worker에서는 listener만 넣는다

event listener만 붙여서 특정 이벤트가 발생할 때만 돌게 하는 것이 정석이다. background.js 안에 자체 실행코드를 두지 않고 listener만 있는게 제일 좋다. 나의 경우 다음의 두 가지 listener를 넣었다.

 

chrome.runtime.onInstalled.addListener를 쓰면 설치 또는 업데이트시 다른 작업을 할 수 있다.

// background.js
chrome.runtime.onInstalled.addListener(function(details){
    if(details.reason == "install"){
        // 첫 설치시 실행할 코드
    }
    else if(details.reason == "update"){
        // 버전 업데이트 또는 확장 프로그램에서 새로고침시
    }
});

 

그리고 chrome.runtime.onMessage.addListener를 쓰면 service worker에서도 다른 content script (manifest에 명시된 특정 사이트와 매칭시켜 쓰는 js)와 통신할 수 있다. 이전 기억으로는 chrome.extension.onMessage.addListener를 썼었는데 요즘엔 runtime을 쓰라고 해서 변경했다. 기본형은 뭐 쉽지만 이게 async return을 sendResponse 안에 넣어야 하면 매우 골치가 아파진다. (뒤에서 다룬다)

// background.js
chrome.runtime.onMessage.addListener( (request,sender,sendResponse) => {
    switch (request.attr) {
        case 'case1':
            sendResponse( myResponse );
            break;
    }
});

 

manifest v3에서는 보안이 강화되어 외부 extension이나 app과 통신하려면 명시적으로 external을 써주고 양쪽에서 app Id를 써줘야 한다.

관련 문서: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessageExternal

 

runtime.onMessageExternal - Mozilla | MDN

Use this event to listen for messages from another extension.

developer.mozilla.org

그래서 external이 없는 chrome.runtime.onMessage는 자기 extension 내에서만 (service worker 및 content script 간 통신) 이벤트가 송수신 되는게 보장된다.

 

IndexedDB를 service worker에 정의하고 다른 스크립트와는 sendMessage로 통신

indexedDB의 장점은 localstorage와는 달리 용량제한이 없고, json도 저장 가능한 진짜 DB라는 점이다. 그리고 여러 브라우저들에서 밀어주는 것 같기도 하고. 기본 사용법은 다음의 링크에서 2022년 기준 최신식으로 js 튜토리얼이 잘 설명되어 있다.

https://ko.javascript.info/indexeddb

 

IndexedDB

 

ko.javascript.info

문제는 대충 content script에서 쓰면 해당 웹페이지의 indexedDB로 저장되어 버린다. 이러면 extension의 domain에 있는 service worker에서는 직접 접근이 안된다.

예를 들면 google.com/* 에 match해서 쓰는 myScript.js가 있다고 했을 때 이 안에서 indexedDB를 만들고 저장해버리면 chrome-extension://어쩌고저쩌고 에 속해있는 내가 만든 extension은 도메인이 달라서 해당 indexedDB를 열 수가 없다. manifest v2에서는 됐을지 몰라도 v3에서는 어림도 없다.

 

잘 생각해 보면 다른 웹사이트에 노출시키지 않고 내 extension에서만 indexedDB를 관리하고 싶을거기 때문에 service worker에서 indexedDB를 만들고 저장해 주는 것이 맞을 것이다. 앞에서 언급했던 background.js 안의 chrome.runtime.onInstalled.addListener에 DB 초기화를 해 두면 확장 프로그램 설치시에 자동으로 기본형을 만들어주므로 DB 없다고 징징대는 오류들이 사라진다. 만약 이걸 content script에서 하게 되면 매번 페이지 새로고침한다거나 할때 DB 함수가 실행되므로 초기화가 어렵다. popup.html에 물려 있는 popup.js에서 하는 것도 팝업창을 띄울때마다 실행되므로 추천하지 않는다. background.js에서 DB를 초기화하는 대략적인 코드는 다음과 같다.

// background.js
chrome.runtime.onInstalled.addListener(function(details){
    if(details.reason == "install"){ // 첫 설치
        sendNotification('설치 완료','앱이 설치되었습니다.',dwellingTimeMs=5000);
        setupDB();
    }else if(details.reason == "update"){
        sendNotification('업데이트 완료','앱이 업데이트되었습니다.',dwellingTimeMs=5000);
    }
});

function setupDB() {
    console.log('DB 셋업 확인');
    const DB_VERSION =1;
    const DB_NAME = "myDbName"
    const STORE_NAME = "settings"

    let dbRequest = indexedDB.open(DB_NAME,DB_VERSION);

    dbRequest.onupgradeneeded = () => {
        // trigger when db not exists
        let db = dbRequest.result;
        if(!db.objectStoreNames.contains(STORE_NAME)) {
            db.createObjectStore(STORE_NAME,{keyPath: 'id'});

            db.onversionchange = function() {
                db.close();
                alert("Database is outdated, please reload the page.");
            };
            console.log('DB 생성 완료');
        }
    };

 

그래서 앞에서 설명했던 것처럼 chrome.runtime.onMessage.addListener를 service worker로 쓰는 background.js에 정의하고 content script에 chrome.runtime.sendMessage를 넣어서 통신하는 것이 바람직하다. 문제는 indexedDB가 비동기로 작동하기 때문에 생각없이 리턴을 하거나 response를 보냈다가는 undefined를 맛보게 된다. 이를 해결하려면 async, await, Promise를 적절히 활용해야 한다.

 

Content script에서 가공한 data를 sendMessage로 extension의 indexedDB에 저장

DB에 넣을 값이 한 개일 때

값이 한 개인 경우는 인터넷에도 정보가 널려 있지만 간단하게 정리해 본다. 특정 웹사이트에서 작동하는 content_script.js에서 열심히 정보를 잘 가공해서 단일 값을 얻었고 DB에 넣고 싶다면 대략 다음과 같이 구현 가능하다.

// content_script.js

// response 안받는 경우
chrome.runtime.sendMessage({myKey:'mytitle', data:myObj});

// response 받는 경우
chrome.runtime.sendMessage({myKey:'mytitle', data:myObj}, (response) => {
    myfunction(response);
});

 

content script에서 response 받아오는 것은 필수가 아니다. 그리고 key 이름은 아무렇게나 정해도 된다. 그냥 데이터만 보내도 되고. 하여간 background.js에서만 잘 구별할 수만 있으면 된다.

 

// background.js
chrome.runtime.onMessage.addListener( (request,sender,sendResponse) => {
    switch (request.myKey) {
        case 'case1':
            putDB(request.data);
            break;
        case 'case2':
            doSomething1(request);
            break;
    }
});

function putDB(item) {
    const DB_VERSION =1;
    const DB_NAME = "myDbName"
    const STORE_NAME = "myStoreName"
    let dbRequest = indexedDB.open(DB_NAME,DB_VERSION);

	// 혹시 DB 없는상태에서 실행하게 되면 이것도 같이 실행되면서 DB 만들어줌
    dbRequest.onupgradeneeded = () => {
        // trigger when db not exists
        let db = dbRequest.result;
        if(!db.objectStoreNames.contains(STORE_NAME)) {
            db.createObjectStore(STORE_NAME,{keyPath: 'id'});
        }
    };

    dbRequest.onerror = () => {
        console.error(`Error in ${DB_NAME}: `, dbRequest.error);
    };

    dbRequest.onsuccess = () => {
        let db = dbRequest.result;

        db.onversionchange = function() {
            db.close();
            alert("Database is outdated, please reload the page.");
        };

        let transaction = db.transaction(STORE_NAME,"readwrite");
        let objStore = transaction.objectStore(STORE_NAME);
        
        let request = objStore.put(item);
        
        request.onsuccess = () => {
           console.log('Addded: ', request.result);
        };
        request.onerror = () => {
            console.log('Error: ',request.error);
        });

        transaction.oncomplete = () => {
            console.log('completed transaction');
        };
    };
}

앞에서 설정해둔 request.myKey를 이용해서 구분하였다. if를 써도 되는데 switch로 쓰면 타입 매칭까지(===) 알아서 해준다고 한다. 특히 js는 비교 연산자가 이상한 결과를 내놓는게 많으니 switch를 쓰면 좋을 것 같다. 다만 switch를 썼을 때 주의사항은 case 안에서 작업을 다 마치고 break를 꼭 해줘야된다는거다. 개발중에 이걸 안해서 다음 case에 있는것도 실행이 계속 됐었다.

관련링크: https://ko.javascript.info/switch

 

switch문

 

ko.javascript.info

putDB() 의 경우 딱 한 개만 넣고 리턴이 없기 때문에 문제가 없다. 참고로 add가 아니라 put을 쓰면 이미 같은 id가 있는 경우에 덮어쓰면서 업데이트를 한다. 그리고 주의할 점은 request.onsuccess만 실행되는게 아니라 다른 것들도 다 event listener라서 put 한번 하는데 여러 event가 발생할 수 있다. 예를 들면 DB가 없는 상태에서 putDB(item)을 하면 DB가 없기 때문에 dbRequest.onupgradeneeded가 발동해서 DB를 만들고, 여기서 끝나는게 아니라 이후의 절차들까지 다 진행하면서 실제로 objStore에 put이 들어간다.

 

DB에 넣을 값이 여러 개일 때

DB를 열고 작업하는 과정은 꽤 시간과 자원을 잡아먹기 때문에 값이 여러 개라면 다음과 같이 리스트로 한 번에 전달해서 한방에 넣어주는 것이 실행시간을 줄이는 길일 것이다. 아래에서는 input이 list로 들어왔을 때 모든 준비를 시켜놓고 마지막에 forEach로 iteration을 돌리면서 넣는 과정이다. 이 경우에 request.onsuccess는 put을 할 때마다 실행되고, transaction.oncomplete는 모든 put이 완료되었을 때 실행되는 것을 확연하게 구분해서 볼 수 있다. 이렇게 리턴 없이 넣는 것까지는 문제가 없다. 문제는 DB에서 값을 받아올 때이다.

// background.js
function putDB(itemList) {
    const DB_VERSION =1;
    const DB_NAME = "myDbName"
    const STORE_NAME = "myStoreName"
    let dbRequest = indexedDB.open(DB_NAME,DB_VERSION);

	// 혹시 DB 없는상태에서 실행하게 되면 이것도 같이 실행되면서 DB 만들어줌
    dbRequest.onupgradeneeded = () => {
        // trigger when db not exists
        let db = dbRequest.result;
        if(!db.objectStoreNames.contains(STORE_NAME)) {
            db.createObjectStore(STORE_NAME,{keyPath: 'id'});
        }
    };

    dbRequest.onerror = () => {
        console.error(`Error in ${DB_NAME}: `, dbRequest.error);
    };

    dbRequest.onsuccess = () => {
        let db = dbRequest.result;

        db.onversionchange = function() {
            db.close();
            alert("Database is outdated, please reload the page.");
        };

        let transaction = db.transaction(STORE_NAME,"readwrite");
        let objStore = transaction.objectStore(STORE_NAME);
        
        itemList.forEach(item => {
            let request = objStore.put(someFunction(item));
            
            request.onsuccess = () => {
                console.log('Addded: ', request.result);
            };
            request.onerror = () => {
                console.log('Error: ',request.error);
            });
        }
        transaction.oncomplete = () => {
            console.log('completed transaction');
        };
    };
}

 

Content script에서 sendMessage로 extension의 indexedDB에 저장된 데이터를 요청

DB에서 뽑을 값이 한 개일 때

mainUI.js라는 content script에서 sendMessage로 다음과 같이 정보를 받아온다고 하자.

// mainUI.js
chrome.runtime.sendMessage({title:'메인화면 세팅 요청'}, (response) => {
    let mainUiSettings = response.mainUiSettings;
    modifyMainUi(mainUiSettings);
});

여기서는 {title:'메인화면 세팅 요청'} 이라는 json 객체를 보내는 이벤트를 발생시켜서 service worker인 background.js가 감지하게 하고, 거기서 작업한 indexedDB의 mainUiSettings 값을 response를 받아오는 것이 목적이다.

 

이걸 하기 위해 아래와 같이 짰다간 undefined를 보게 될 것이다.

// background.js
chrome.runtime.onMessage.addListener( (request,sender,sendResponse) => {
    switch (request.myKey) {
        case '메인화면 세팅 요청':
            sendResponse({mainUiSettings: getItemFromDB()});
            break;
    }
});

function getItemFromDB() {
    const DB_VERSION =1;
    const DB_NAME = "myDbName"
    const STORE_NAME = "myStoreName"
    let dbRequest = indexedDB.open(DB_NAME,DB_VERSION);

    dbRequest.onupgradeneeded = () => {
        // trigger when db not exists
        let db = dbRequest.result;
        if(!db.objectStoreNames.contains(STORE_NAME)) {
            db.createObjectStore(STORE_NAME,{keyPath: 'id'});
        }
    };
    dbRequest.onerror = () => {
        console.error(`Error in ${DB_NAME}: `, dbRequest.error);
    };
    dbRequest.onsuccess = () => {
        let db = dbRequest.result;
        db.onversionchange = function() {
            db.close();
            alert("Database is outdated, please reload the page.");
        };
        let transaction = db.transaction(STORE_NAME,"readwrite");
        let objStore = transaction.objectStore(STORE_NAME);
        
        let request = objStore.get(myPreDefinedId);
        request.onsuccess = () => {
           console.log('Addded: ', request.result);
           return request.result; // 이게 완전 잘못
        };
        request.onerror = () => {
            console.log('Error: ',request.error);
        });

        transaction.oncomplete = () => {
            console.log('completed transaction');
        };
    };
}

위와 같이 짜면 얼핏 보기엔 get을 성공했으니 request.onsuccess안에서 request.result가 return될 것 같지만 실제로 해보면 sendResponse로 들어가는 값은 undefined가 나온다. console.log로 함수 내부에서는 정상으로 찍혀나와도 undefined가 전송되는건 DB를 그런식으로 쓰는게 아니라는거다. 실제로 indexedDB는 비동기 작업을 하기 때문에 실제 transaction.oncomplete가 실행되기 전에 return이 먼저 되어 값이 날아간다. 그렇다고 transaction.oncomplete에 request.result를 넣을 수도 없는게 event가 달라서 scope가 맞지 않는다. 그래서 나는 뽑은 값을 리턴할 때까지 잘 기다리기 위해 아래와 같이 Promise를 사용하였다.

// background.js
function getItemFromDB() {
    const DB_VERSION =1;
    const DB_NAME = "myDbName"
    const STORE_NAME = "myStoreName"
    return new Promise( (resolve, reject) => {
        let dbRequest = indexedDB.open(DB_NAME,DB_VERSION);
        dbRequest.onupgradeneeded = (event) => {
            // trigger when db not exists
            console.log("Settings DB not exists!");
            reject(event);
        };

        dbRequest.onerror = (event) => {
            console.error(`Error in ${DB_NAME}: `, dbRequest.error);
            reject(event);
        };

        dbRequest.onsuccess = () => {
            let db = dbRequest.result;

            db.onversionchange = (event) => {
                db.close();
                alert("데이터베이스 버전 갱신됨. 페이지를 새로고침해주세요.");
            };

            let transaction = db.transaction(STORE_NAME,"readonly");
            let objStore = transaction.objectStore(STORE_NAME);

            let request = objStore.get(someId);
                
            request.onsuccess = (event) => {
                resolve(request.result);
            };
            request.onerror = (event) => {
                console.log('Error: ',request.error);
                reject(event);
            };

            transaction.oncomplete = (event) => {
                db.close();
            };
        };
    });
}

return에 new Promise를 사용하고 안에 있는 값들을 resolve(성공한경우 리턴에 쓸 값) 와 reject (에러 뿜기) 로 구별한다. 이러면 resolve 안에 있는 값이 실제 값이 아니라 Promise로 전달된다. 그 과정에 많은 일들은 Promise의 resolve와 reject가 알아서 해 준다. 

 

문제는 이렇게만 하고 sendResponse부분을 바꾸지 않으면 값이 아니라 promise인 채로 날아가기 때문에 역시나 이상한 값이 날아가거나 보내기 전에 연결이 끊겼다는 런타임 에러 메세지를 볼 수 있다. 그래서 값이 나올 때까지 기다려야 한다. 언제 생긴지는 모르겠지만 인터넷 뒤져보니 then이라는 문법이 있었다. VS code에서는 promise를 리턴하는 함수에 알아서 then이 추천으로 뜬다. then안에는 앞 함수의 리턴을 rename해서 사용할 수 있고, 이 때 해당 변수에 값이 들어올 때까지 then이 기다려 준다. 그리고 나서 result를 보내면 된다. 이 때 then 다음에 return true를 해줘야지만이 result가 보내지는 때까지 기다리는 동안 sendMessage와의 연결이 끊기지 않는다. 

// background.js
chrome.runtime.onMessage.addListener( (request,sender,sendResponse) => {
    switch (request.myKey) {
        case '메인화면 세팅 요청':
            getItemFromDB()
            .then( result => {
                sendResponse({mainUiSettings: result});
            });
            return true; // keep the messaging channel open for sendResponse
    }
});

이렇게 DB에서 값을 뽑아서 기다렸다 주게 되면 mainUI.js에서는 코드를 안바꿔도 된다. 또한 위의 getItemFromDB에서도 값을 딱 하나만 뽑았기 때문에 리턴할 때 값이 하나여야 한다는 제약조건을 위반하지 않고 깔끔하게 처리가 되었다. 문제는 DB에서 값을 여러 개 뽑아서 리턴해야 될 때이다.

 

DB에서 뽑을 값이 여러 개일 때

만약에 mainUI.js에서 itemList를 넣고 각 element별로 DB에서 나오는 서로 다른 값을 요구한다면, 이 경우에는 리턴이 여러 개여야 한다. 문제는 내가 아는 선에서는 return new Promise의 resolve는 값을 한 개만 리턴한다는 것이다. 이 경우 여러 개를 Promise로 동시에 뽑을 수가 없어보인다. 그리고 await를 쓰다 보면 for 안에 await가 들어가는 경우가 생기는데 chrome의 경우 async function의 top level에서만 await를 허용하고 있어서 에러를 뿜는다. 이것때문에 고민을 매우 많이 했는데 최종적으로 찾아낸 답은 다음과 같다.

 

getMatchById(id)는 정의에 따라 Promise를 return한다. 그러면 원리상 for (const id of idList)는 const result에 값이 받아질 때까지 기다리지 않고 result에 Promise만 return한 채로 계속 돈다. 이 Promise를 results 라는 list에 모은다. 그리고 이걸 return하기 전에 Promise.all(results)로 모아준다. 그리고 나온 값을 then에서 가공한다. 이대로 리턴하면 될거같지만 리턴은 Promise.all을 기다려주지 않기 때문에 return에 또 Promise를 만들어서 가공된 데이터를 담은 marketStocks에 대한 promise를 만들고 리턴한다. 

// background.js
function matchedMarketList(idList) {
    let results = [];
    let marketStocks = [];

    for (const id of idList) {
        const result = getMatchById(id);
        results.push(result);
    } // results is list of promise
    return new Promise( (resolve, reject) => {
        Promise.all(results)
        .then( (matches) => {
            // console.log(matches);
            matches.forEach( match => {
                if (match) {
                    let marketStock = match.stocks;
                    let lastUpdated = ms2TimeString(match.lastUpdated);
                    marketStocks.push([marketStock,lastUpdated]);
                }
                else {
                    marketStocks.push([null,null]);
                }
            });
            // console.log(marketStocks);
            resolve(marketStocks);
        });
    });
}

function getMatchById(id) {
    const DB_VERSION =1;
    const DB_NAME = "market"
    const STORE_NAME = "goods"
    return new Promise( (resolve, reject) => {
        let dbRequest = indexedDB.open(DB_NAME,DB_VERSION);

        dbRequest.onupgradeneeded = (event) => {
            // trigger when db not exists
            console.log("market DB not exists!")
            reject(event);
        };

        dbRequest.onerror = (event) => {
            console.error(`Error in ${DB_NAME}: `, dbRequest.error);
            reject(event);
        };

        dbRequest.onsuccess = () => {
            let db = dbRequest.result;

            db.onversionchange = function() {
                db.close();
                alert("데이터베이스 버전 갱신됨. 페이지를 새로고침해주세요.");
            };

            let transaction = db.transaction(STORE_NAME,"readonly");
            let goods = transaction.objectStore(STORE_NAME);

            let request = goods.get(id);
            request.onsuccess = (event) => {
                resolve(request.result);
            };
            request.onerror = (event) => {
                console.log('Error: ',request.error);
                reject(event);
            };

            transaction.oncomplete = (event) => {
                db.close();
            };
        };
    });
}

function ms2TimeString(ms) {
    let d = new Date(ms);
    let year = d.getFullYear();
    let month = `0${d.getMonth()}`.slice(-2);
    let date = `0${d.getDate()}`.slice(-2);
    let hours = `0${d.getHours()}`.slice(-2);
    let minutes = `0${d.getMinutes()}`.slice(-2);
    let seconds = `0${d.getSeconds()}`.slice(-2);
    return `${year}-${month}-${date} ${hours}:${minutes}:${seconds}`;
}

이렇게 나온 Promise를 sendMessage에 반응하는 response로 보낼 때 앞에서 했던 테크닉과 같은 방식으로 아래와 같이 처리해 준다.

chrome.runtime.onMessage.addListener( (request,sender,sendResponse) => {
    console.log(request);
    switch (request.title) {
        //somecode
            
        case 'mycase':
            let goodsIds = request.goodsIds;
            matchedMarketList(goodsIds)
            .then( result => {
                sendResponse({matchedGoodsList: result});
            });
            return true; // keep the messaging channel open for sendResponse
    }
});

이렇게 하면 처리된 matchedGoodsList를 sendMessage의 response로 전달할 수 있다. 만약에 matchedMarketList의 리턴이 모든 로직 path를 꼼꼼히 체크하지 않아서 resolve나 reject를 하지 않은 채로 끝났다면 리턴이 없으므로 undefined를 반환할 것이고, 이게 result의 element에 undefined인 상태로 push되었을 것이다. 이것을 방지하기 위해 앞의 getMatchById에서 if (match)로 체크해서 명시적으로 null을 추가해 주었다. 원래 input으로 들어왔던거와 index를 미칭시키기 위해 null을 넣었었고 parsing은 mainUI.js에서 길이에 맞춰 index로 for loop를 돌면서 null check를 통해 적당히 걸러주는 것으로 마무리했다.

 

후기

상술한 과정을 통해 manifest version 3로 chrome extension을 만들고 indexedDB를 활용해 보면서 간만에 비동기에 대한 깊은 고민을 해 보았다. 결국 문법 문제였는데 인터넷 찾아봐도 manifest v2 위주로 나오거나 아예 안찾아지거나, 아니면 잘못된 해결법 또는 꼼수같은게 나와서 정석을 찾는 데 한참 걸린 것 같다. 물론 내 주력 언어가 js도 아니고 Promise에 대해서도 익숙하지 않았기 때문에 그랬을 수 있다. 특히 웹은 아직도 내가 이해되지 않는 많은 부분들이 남아있고 엄청난 deprecate와 함께 하위호환을 버리는 표준으로 역변하고 있기 때문에 더더욱 잘 모르겠는 것 같다. 여튼 이번엔 이렇게 해결했고 async에 대한 이해를 늘릴 수 있었다는 데에 의미가 있다고 본다. 혹시나 내용에 대해 궁금하거나 잘못된 부분이 있는 경우 댓글 등으로 남겨주면 시간날 때 확인해 보겠다.

'프로그래밍' 카테고리의 다른 글

오디오 파일 읽어서 SoundCloud Waveform처럼 만들기 관련 자료  (0) 2022.02.21
MATLAB으로 웹캠 OCR 하기  (2) 2020.01.03
MATLAB으로 머신러닝 입문(?)  (0) 2019.12.13
GitLab 설정  (0) 2018.04.07
BMS구동기 v0.4  (4) 2016.02.13