ondelva

← Archive

핫딜시트 알림 크롬 확장을 웹 스토어에 올렸다

새 딜을 브라우저 알림으로 띄우는 크롬 확장을 만들어 웹 스토어에 올렸다. 코드는 60줄이 안 됐는데 첫 제출이 키워드 스터핑으로 반려됐고, 공개된 뒤에도 검색에 안 떠서 한참 기다렸다.

egg4분 읽기

핫딜시트 미니앱이 반려되고 웹만 남긴 뒤로 새 딜을 받아볼 방법을 몇 가지 붙였다. 9월 초에 Slack·Discord 알림을 먼저 열었고, 그다음이 크롬 확장이다.

Slack이나 Discord는 받을 채널이 있어야 한다. 계정도 채널도 없이 설치만 하면 되는 길이 하나 더 있었으면 했다.

9월 3일에 만들어 그날 제출했고 같은 날 반려됐다. 고쳐서 다시 넣은 게 4일에 통과했다. 오늘 구독 페이지에 공식 배지를 다는 것까지 해서 대충 일단락된 것 같아 정리해둔다.

서버는 건드리지 않았다

핫딜시트에는 미니앱용으로 열어둔 공개 딜 API(/api/deals)가 있다. 확장은 이걸 10분마다 읽어서 마지막으로 알린 딜 이후에 올라온 것만 알림으로 띄운다.

Slack·Discord 알림은 구독 레코드를 서버에 두고 워커가 웹훅을 쏘는 구조다. 확장에는 그런 게 없다. 누가 설치했는지 서버는 모르고, D1·KV 사용량도 늘지 않는다.

파일은 manifest.json, background.js, 아이콘 셋이 전부다. MV3라 백그라운드는 서비스 워커로 돌고, 주기 실행은 chrome.alarms로 건다. 권한은 셋만 받았다.

{
  "manifest_version": 3,
  "name": "핫딜시트 알림",
  "version": "0.1.0",
  "background": { "service_worker": "background.js" },
  "permissions": ["alarms", "notifications", "storage"]
}

주기를 10분으로 잡은 건 사이트 엣지 캐시가 5분이라서다. 5분보다 자주 물어봐도 같은 응답이 온다. 알림이 조금 늦어도 되는 용도라 그 두 배로 뒀다.

host_permissions는 선언하지 않았다

확장에서 다른 도메인을 fetch하려면 보통 host_permissions를 받는다. CORS를 우회하려고 받는 권한인데, /api/deals는 이미 Access-Control-Allow-Origin: *를 보내고 있었다. 미니앱 API를 열 때 엣지 캐시가 Origin별로 갈리지 않게 와일드카드로 고정해둔 값이다.

우회할 게 없으니 권한을 뺐고 덕분에 설치 화면 경고가 “알림 표시” 한 줄로 끝났다. 사이트 접근 경고가 뜨는 확장은 나라도 한 번 망설이게 되더라.

대신 누가 API의 CORS 헤더를 바꾸면 확장이 조용히 멈춘다. 이건 문서에 적어두는 것 말고는 막을 방법이 없는 것 같다.

설치 직후에는 알리지 않는다

그냥 두면 설치하자마자 최근 딜 10건이 전부 새 딜로 쏟아진다. 마지막으로 알린 딜이 기록에 없으니까.

그래서 설치·업데이트 직후 한 번은 최신 딜의 슬러그만 저장하고 넘어간다. 그다음 알람부터 저장된 슬러그보다 앞에 있는 딜만 알린다.

chrome.runtime.onInstalled.addListener(() => {
  chrome.alarms.create(ALARM, { periodInMinutes: PERIOD_MINUTES });
  check(true); // 설치·업데이트 직후는 시드만
});
chrome.alarms.onAlarm.addListener((a) => a.name === ALARM && check(false));

대신 못 하는 것도 있다.

  • 한 번에 10건까지만 본다. 10분 사이에 10건 넘게 올라오면 초과분은 알리지 않는다
  • 브라우저가 꺼져 있는 동안에는 알림도 없다
  • 가격대나 카테고리 조건을 걸 수 없다. 서버에 구독이 없으니 조건을 실을 데가 없다

개인정보처리방침부터 고쳤다

확장은 chrome.storage.local에 슬러그 한 줄을 저장한다. 그런데 사이트 개인정보처리방침 3항에는 “로컬 스토리지를 사용하지 않습니다”라고 단언해둔 상태였다. 확장을 내면서 그대로 두면 틀린 문장이 된다.

3항의 범위를 웹사이트로 좁히고 확장 몫은 따로 항목을 만들어 적었다. 방침 시행일을 이용약관·운영자 정보와 한 상수로 같이 쓰고 있어서 방침만 날짜를 올리려고 상수도 갈랐다.

storage.sync가 아니라 local인 것도 일부러다. sync는 구글 서버로 동기화되니 기기 밖으로 나가는 데이터가 된다. 저장하는 게 슬러그 하나라 동기화할 이유도 없었다.

스토어 등록 정보 채우기

개발자 대시보드에 넣을 값이 생각보다 많았다(2026년 9월 기준). 이름·설명·카테고리·스크린샷 말고도 단일 목적, 권한마다의 정당화, 원격 코드 사용 여부, 수집 데이터 항목을 하나하나 채워야 한다.

단일 목적과 권한 정당화는 영어로 적었다. 이용자에게는 안 보이고 심사관만 읽는 칸이라서다. 한국어를 읽는 심사관이 배정된다는 보장이 없으니 영어로 써서 잃을 게 없다.

이름과 설명은 한국 이용자가 보는 칸이라 한글로 뒀다.

원격 코드 사용 여부는 헷갈리기 쉬운 질문이다. API를 fetch하니 원격인가 싶지만 여기서 말하는 원격 코드는 패키지에 없는 스크립트를 가져와 실행하는 것이다. 데이터를 받는 건 해당하지 않고 실행되는 건 zip 안의 background.js 하나뿐이라 No를 골랐다.

스크린샷이 하나 애매했다. 브라우저 알림은 페이지 밖 OS 알림 센터에 그려져서 사이트를 캡처하는 방식으로는 같이 찍히지 않는다.

그래서 실제 사이트를 캡처한 화면 위에 알림 카드만 따로 그려 얹었다. 제목과 쇼핑몰 · 가격 형식은 확장이 실제로 띄우는 것과 같게 맞췄다.

크롬 웹 스토어에 올린 스크린샷. 핫딜시트 최신 수집 표 위 오른쪽에 새 딜 알림 카드가 떠 있다.

오른쪽 위 카드가 그려 넣은 것이고 뒤는 진짜 사이트다. 아래 문구는 반려를 한 번 겪고 고친 버전이다.

첫 제출은 반려됐다

제출한 날 바로 반려됐다. 사유는 Spam and Placement in the Store.

Having excessive keywords in the item’s description

지목된 건 설명에 적어둔 뽐뿌, 루리웹, 클리앙, 다모앙, 어미새, 퀘이사존, 아카라이브였다. 어디서 모아오는지 보여주려고 늘어놓은 건데 심사 쪽에서는 키워드 스터핑으로 읽은 것이다.

짧은 설명과 상세 설명에서 이름을 다 빼고 “여러 커뮤니티”로 바꿨다. 정책 문구에 스크린샷도 대상으로 적혀 있어서 스크린샷 캡션도 같이 고쳤다. 설명만 고치면 같은 사유로 또 걸릴 것 같았다.

패키지는 그대로라 버전은 올리지 않고 재제출했다. 다음 날 통과.

위 스크린샷 배경에는 커뮤니티 이름이 그대로 보인다. 이것까지 걸리면 스크롤을 내려서 다시 찍으려 했는데 그냥 넘어갔다. 제품의 실제 화면과 설명 문구는 구분해서 보는 것 같다.

공개하고 나서도 손볼 게 남았다

4일에 공개됐는데 정작 사이트에는 설치하러 갈 링크가 없었다. 공지에는 “설치할 수 있게 되면 다시 공지하겠습니다”라고 써놓은 채였다. 이건 10일에야 구독 페이지와 공지에 반영했다.

스토어 검색도 문제였다. 상세 페이지는 공개 즉시 열리는데, 핫딜시트로 검색하면 6일이 지나도 결과가 0건이었다.

경쟁 항목도 없는 검색어라 순위 문제는 아니고 색인이 안 된 것이다. 배포 지역도 전체로 열어둔 상태라 기다리는 것 말고는 할 게 없었다. 오늘 다시 확인해보니 이제는 검색된다.

웹 스토어 배지가 폐기된 줄 알았다

구독 페이지에는 처음에 텍스트 링크로 넣었다. Slack·Discord는 각자 브랜딩 가이드가 공식 버튼을 쓰라고 해서 버튼으로 뒀지만 크롬 웹 스토어 배지는 2019년에 폐기된 것으로 알고 있었다. 가이드도 없는데 브랜드 색을 하나 더 들이면 디자인 규칙의 예외만 늘어난다고 봤다.

그런데 이 페이지를 Google Ads 랜딩으로 써보니 텍스트 링크는 설치 지점으로 인지되지 않았다. 다시 찾아보니 웹 스토어 브랜딩 가이드에 공식 배지가 멀쩡히 있더라.

오늘 그 배지로 바꿨다. 가이드가 크기 조절 말고는 변형을 금지하고 다른 로고와 거리를 두라고 해서 Slack·Discord 버튼 줄에 끼우지 않고 따로 뒀다. 다크 모드에서 흰 배지가 배경에 녹지 않게 테두리가 있는 판을 골랐다.

여하튼 코드보다는 그 둘레에서 시간을 더 쓴 것 같다.

조건 필터는 넣으려면 서버에 구독을 둬야 해서 이번 구조와 정면으로 부딪친다. 스토어에서 설치하는 사람이 좀 생기면 그때 다시 고민해봐야겠다.

Filed under 개발

ShareXFacebookLinkedIn