본문 바로가기
C.W.K.
Stream
Lesson 03 of 06 · published

Panel vs Popup vs Full Tab — 맞는 surface 고르기

~10 min · side-panel, popup, tabs.create, ux, architecture

Level 0Extension 입덕
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"표면이 셋이고, 수명이 셋이고, 담을 수 있는 정보량도 셋이야. 이 lesson은 고르는 기준표야 — 어떤 ClipDeck 기능이 어디로 가야 하는지, 그리고 엉뚱한 표면에 엉뚱한 기능을 밀어 넣는 게 왜 extension UX 버그 1 위인지."

표면 셋

  • Popup. 작고 금방 사라져. 최대 800×600 픽셀쯤이고 적당한 크기는 320×480 정도야. 바깥을 누르면 죽어. toolbar icon을 누르면 뜨고, focus를 잃는 순간 사라져. 설정 토글, 상태 확인, 버튼 하나짜리 동작처럼 한 번 하고 끝나는 일에 딱이야.
  • Side panel. 길쭉하고 오래 버텨. 창 높이를 다 쓰고 너비는 user 취향에 따라 300–400 픽셀쯤. 페이지를 눌러도 안 죽고, tab을 바꿔도 안 죽고, user가 직접 닫아야 죽어. user가 곁눈질하며 계속 참고하는 정보에 딱이야.
  • Full tab. 제일 커. chrome.tabs.create({ url: 'options.html' })chrome_url_overrides로 열어. 평범한 웹 페이지랑 같은 종류의 자리고, 창 전체를 쓸 수 있어. 전용 앱, 설정 화면, 첫 사용 안내, 단계가 여럿인 복잡한 UI에 딱이고.

수명 비교

Surface열려 있는 동안Focus 잃을 때Re-mount 비용
Popup바깥 click 전까지모든 것매 open 마다 load + render
Side paneluser가 toggle off 할 때까지없음첫 open 시 load, 이후 persist
Full tabuser가 tab 닫을 때까지없음첫 open 시 load, 이후 persist

Popup은 뜨는 게 싸지만 자주 떠. panel 이랑 full tab은 뜨는 게 비싼 대신 드물게 떠. 결국 처리량이냐 반응 속도냐를 맞바꾸는 거야.

ClipDeck은 뭘 어디에 둘까

  • Popup — 빠른 동작이랑 요약. "지금 선택한 거 저장" 버튼 (실제 capture는 SW 한테 메시지로 넘기고), "clip 목록 열기" 버튼 (side panel을 열어), 오늘 개수, "카운터 초기화". 한 번 누르면 끝나고 계속 보고 있을 필요가 없는 것들.
  • Side panel — clip 창고. 시각, 출처 URL, 검색창, 필터, inline 편집과 삭제 (Track 7)까지 갖춘 전체 목록. user가 기사를 읽으면서 열어 두는 자리야.
  • Full tab — 나중에 만들 설정 화면 (Track 6에서 소개해)과, side panel 보다 넓은 자리가 필요한 내보내기/불러오기 화면 몫으로 남겨 둬. ClipDeck v1은 full-tab UI 없이 나가고, v2에서 붙을 수도 있어.

고르는 기준은 질문 하나로 줄어들어. user가 이 화면을 얼마나 오래 봐야 하지? 초 단위면 popup. 분 단위에서 한 세션이면 side panel. 오래 붙잡고 하는 작업이나 설정이면 full tab.

이러면 안 돼

  • clip 창고를 popup에 욱여넣기. 항목이 열 개만 넘어가도 popup은 답답해져. 증상은 이래 — 자꾸 스크롤해야 하고, 열 때마다 선택이 날아가고, 필터 둘 자리가 없어. side panel로 옮겨.
  • 한 번 누르면 끝날 일을 side panel에 두기. user 한테 닫으려고 panel을 여는 짓을 시키는 거야. 증상은 그 기능을 아무도 안 쓴다는 거고. popup으로 옮겨.
  • 설정을 popup 안에 만들기. 여러 구획으로 나뉜 form은 자리가 필요해. 증상은 입력창이 쥐꼬리만 해지고, form을 만지는 도중에 popup이 저절로 닫히는 거야. options_page로 full tab에 내보내.
  • 상태 한번 보자고 full tab 열기. 보던 페이지에서 user를 끌어내는 거라 방해가 커. 증상은 uninstall 이고. popup 이나 panel로 옮겨.
표면은 user가 그 일에 쓰는 시간에 맞춰 골라. 초면 popup, 분이면 panel, 한 세션이면 full tab. 표면을 잘못 고르는 건 extension이 낼 수 있는 가장 큰 UX 버그야.
표면끼리 코드를 같이 쓸 수 있을까? 응, 돼. popup도 side panel도 full-tab 화면도 같은 JS module을 import 할 수 있고 스타일도 같이 쓸 수 있어. ClipDeck의 clip 한 줄 그리는 코드는 clip-row.js에 두고 panel.js 랑 나중의 export-page.js가 갖다 쓰면 돼. popup은 보통 필요가 달라서 (상태만 보여 주니까) 그럴 일이 적지만, popup과 panel에 비슷한 렌더 로직이 자라기 시작하면 복붙 말고 공용 module로 빼.

Code

background.js — full-tab options page 열기·javascript
// background.js — 전용 full-tab page 열기 (나중 settings/export 용)
async function openOptionsTab() {
  // chrome.runtime.openOptionsPage 가 설정되면 manifest 의 options_page 사용,
  // 아니면 manifest.json 의 options_ui 선언 여는 걸로 fallback.
  if (chrome.runtime.openOptionsPage) {
    await chrome.runtime.openOptionsPage();
  } else {
    await chrome.tabs.create({ url: chrome.runtime.getURL("options.html") });
  }
}

// Manifest 가 options page 선언해야 함:
//   "options_page": "options.html"
// — 또는 더 풍부한:
//   "options_ui": { "page": "options.html", "open_in_tab": true }
popup.js — popup은 창고가 아니라 요약과 입구·javascript
// popup.js — library view 를 side panel 에 위임하는 minimal popup
async function quickStats() {
  const { clips = [], totalVisits = 0 } = await chrome.storage.local.get([
    "clips",
    "totalVisits",
  ]);
  document.getElementById("clipCount").textContent = String(clips.length);
  document.getElementById("visitCount").textContent = String(totalVisits);
}

document.getElementById("openPanelBtn").addEventListener("click", async () => {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  if (tab?.id) await chrome.sidePanel.open({ tabId: tab.id });
});

quickStats();
panel.js — 창고는 오래 버티는 표면에 머문다·javascript
// panel.js — full library 가 여기 살아
// (source title / timestamp / source URL 와 함께 모든 clip 렌더.)
async function render() {
  const { clips = [] } = await chrome.storage.local.get("clips");
  const root = document.getElementById("list");
  root.innerHTML = clips.length === 0 ? "<p class='empty'>No clips yet.</p>" : "";
  for (const clip of clips) {
    const row = document.createElement("div");
    row.className = "row";
    row.innerHTML = `
      <div class="meta">
        <a href="${clip.url}" target="_blank">${escapeHtml(clip.title || clip.url)}</a>
        <time>${new Date(clip.savedAt).toLocaleString()}</time>
      </div>
      <p>${escapeHtml(clip.text)}</p>`;
    root.appendChild(row);
  }
}

function escapeHtml(s) {
  return String(s).replace(/[&<>"']/g, (ch) => ({
    "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
  })[ch]);
}

chrome.storage.onChanged.addListener((c, a) => a === "local" && "clips" in c && render());
render();

External links

Exercise

clipdeck/popup.html을 정리해서 Clips: <span id="clipCount"></span>, Visits: <span id="visitCount"></span>, 기존 Reset / Ping 버튼, 그리고 새 Open Clip List 버튼만 남겨. clip 목록을 그리는 코드는 세 번째 code block을 써서 clipdeck/panel.js로 옮기고. Reload 한 다음 toolbar icon을 눌러 봐 — popup이 작아지고 요약이랑 동작만 남아 있어야 해. Open Clip List를 누르면 panel이 둘러보는 일을 넘겨받고. clip을 몇 개 저장하고 tab을 옮겨 다녀 봐. panel 내용이 onChanged를 타고 따라오는지 보고. 여유가 되면 manifest에 "options_page": "options.html" 을 넣고 <h1>ClipDeck Settings (coming Track 6)</h1> 만 있는 options.html을 만든 다음, popup에 chrome.runtime.openOptionsPage 버튼을 붙여서 Chrome이 진짜 tab을 여는지 확인해 봐.
Hint
정리하고 나서 popup이 답답해 보이면 body에 너비 280px과 안쪽 여백 12px을 직접 줘. popup은 내용에 맞춰 크기를 잡지만 최대치가 있거든. clip을 저장했는데 panel이 안 바뀌면 panel.js가 chrome.storage.onChanged를 구독하는지, 그리고 area === 'local'로 거르고 있는지 다시 봐. openOptionsPage 버튼은 요즘 Chrome에선 manifest를 안 고쳐도 그냥 도는 경우가 많아 (빈 화면으로 대신 열어 줘). 그래도 제대로 된 경험을 주려면 options_page를 명시해야 해. 그래야 user가 chrome://extensions → ClipDeck → Details → Extension options로도 찾아갈 수 있거든.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.