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

Anchor 6 — Viewport context payload shape

~11 min · payload, schema, context, case-study, v0.2.1

Level 0Extension 입덕
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Lesson 6가 ChromeEmbed의 모든 부분 흐르는 JSON object zoom in: host-context payload. 모든 field 정당화, 모든 field가 panel iframe이 의존하는 계약."

Full shape

Content script의 extractContext()가 반환:

{
  type: 'pippa:host-context',
  payload: {
    host_kind: 'web-page',
    snapshot_at: '2026-05-16T01:23:45.000Z',
    host_id: chrome.runtime.id,
    source: location.href,
    human_label: document.title || location.hostname,
    favicon: faviconHref(),
    selection,
    viewportText,
    readableText: viewportText, // compat alias
    wordCount,
    viewport: { width, height, scrollX, scrollY, devicePixelRatio },
    scrollPosition: { y: scrollY, ofPageHeight: pageHeight },
    language: document.documentElement?.lang || navigator.language || '',
    frameUrl: location.href,
    isTopFrame: window.top === window
  }
}

Identity field

  • host_kind — ChromeEmbed 엔 항상 'web-page'. PIPPA-EMBEDS framework가 이거 기대. Adobe-embed가 'adobe-app', Calendar가 'gcal-event' 등. Downstream 렌더링 위한 discriminator.
  • host_id — extension의 runtime id. cwkPippa가 다른 installed instance (multi-Chrome-profile setup) 의 메시지 구별하게.
  • source — user가 있는 canonical URL. cwkPippa가 이걸 'capturing from example.com/article' 로 surface.
  • human_label — fallback-chained title. Chat thread label로 사용.
  • favicon — UI affordance 위해.

Content field

  • selection — 현재 highlighted 텍스트 (또는 최대 5 분 전의 cached selection).
  • viewportText — user의 현재 viewport 안 텍스트, walker-추출 spatially sorted (Lesson 3).
  • readableText — viewportText의 다른 이름이야. cwkPippa의 옛 코드가 'readableText' 를 읽거든. 이름을 하나 더 달아 둔 덕분에 ChromeEmbed가 그쪽을 안 깨뜨려. cwkPippa가 'viewportText' 로 완전히 넘어가면 그때 걷어낼 계획이고.
  • wordCount — cwkPippa UI 위한 quick metric ('1,247 words in view').

Viewport field

  • viewport: { width, height, scrollX, scrollY, devicePixelRatio } — user 창을 실제 픽셀로 잰 값이야. 이걸로 cwkPippa가 좁은 화면이나 고해상도 디스플레이에 맞춰 UI를 바꿀 수 있어.
  • scrollPosition: { y, ofPageHeight } — user가 페이지 세로 어디쯤 와 있는지랑 전체 높이야. 이걸로 cwkPippa가 '이 글의 60% 쯤 왔어' 같은 걸 말해 줄 수 있어.

Metadata

  • snapshot_at — ISO 형식 시각이야. cwkPippa가 '5 초 전에 본 화면 기준이야' 라고 알려 줄 수 있게 해 줘.
  • language — cwkPippa가 매칭 locale로 응답 format 에 유용.
  • frameUrlisTopFrame — Lesson 2에서 본 SW의 합치는 로직에 쓰여. 안쪽 frame이 올려 보낸 것도 이 값을 달고 오니까, SW가 그걸로 맨 위 frame의 스냅숏을 덮어쓸지 말지 판단할 수 있어.

왜 schema, '그냥 DOM 보내기' 아님

Fixed payload shape의 규율이 이럴 때 payback:

  • 다른 embed가 compose. PIPPA-EMBEDS framework가 모든 embed (Chrome, Adobe, Mail, Calendar) 가 같은 field 이름의 host-context 생산 기대. cwkPippa가 uniformly 렌더.
  • Versioning. Payload v1 → v2 bump 필요하면, 오늘 version: 1 field 추가하고 내일 확인.
  • Compatibility. readableText alias가 정확히 fixed schema가 enable 하는 종류의 migration aid. Schema 없이는 모든 consumer가 추측해야.
  • 들여다보기 좋아. cwkPippa 로그에 이 context가 찍혔을 때, 무슨 항목이 있는지 추측할 필요 없이 정돈된 기록처럼 그냥 읽히거든.

Payload 에 없는 것

일부러 부재:

  • 문서 HTML 전체 — 너무 크기도 하거니와, '지금 보이는 것' 이라는 의미 자체가 사라져.
  • 화면 사진 — 저장 비용이 커. 잘라낸 조각 정도라면 나중에 항목으로 붙일 수는 있고.
  • tab 방문 기록 — ChromeEmbed가 다룰 범위 밖이야. cwkPippa는 이미 자기 대화 기록을 갖고 있고.
  • user가 누구인지 — 피파는 cwkPippa session에서 이미 알고 있어. extension이 실어 보낼 정보가 아니야.

Field 추가가 feature로 정당화될 때 저렴. speculatively 추가는 schema 시끄럽고 bridge 느리게 만듬.

한 payload shape, 모든 field 정당화, 모든 field가 계약. Schema가 ChromeEmbed를 다른 embed와 composable 하게 만드는 것. 그것 없이는 각 embed가 'context' 재발명 하고 cwkPippa가 다 special-case 해야.
PIPPA-EMBEDS framework 각도. ChromeEmbed v0.1이 첫 PIPPA embed. Ship 하는 host-context schema가 future embed (Adobe, Mail, Calendar, IDE) 위한 제안된 계약. cwkPippa가 uniformly 렌더. embed의 유일한 job이 native host가 제공하는 어떤 것이든 schema field 채우기. 그게 win — 각 새 embed가 from-scratch UI가 아닌 한 payload format 가진 content-script-and-bus shape.

Code

content-script.js — schema, prototype 가 write 한 정확히 그대로·javascript
// embeds/chrome/content-script.js — extractContext 의 shape 된 output
function extractContext() {
  const viewportText = collectViewportText();
  const selection = currentSelectionText();
  const pageHeight = Math.max(
    document.documentElement?.scrollHeight || 0,
    document.body?.scrollHeight || 0,
    window.innerHeight || 0
  );
  return {
    type: 'pippa:host-context',
    payload: {
      host_kind: 'web-page',
      snapshot_at: new Date().toISOString(),
      host_id: chrome.runtime.id,
      source: location.href,
      human_label: document.title || location.hostname,
      favicon: faviconHref(),
      selection,
      viewportText,
      readableText: viewportText, // back-compat alias
      wordCount: viewportText ? viewportText.split(/\s+/).filter(Boolean).length : 0,
      viewport: {
        width: Math.round(window.innerWidth || 0),
        height: Math.round(window.innerHeight || 0),
        scrollX: Math.round(window.scrollX || 0),
        scrollY: Math.round(window.scrollY || 0),
        devicePixelRatio: Number((window.devicePixelRatio || 1).toFixed(2))
      },
      scrollPosition: { y: window.scrollY || 0, ofPageHeight: pageHeight },
      language: document.documentElement?.lang || navigator.language || '',
      frameUrl: location.href,
      isTopFrame: window.top === window
    }
  };
}
v0.2.1 checkpoint — ChromeEmbed payload — page fact 먼저, tab identity 다음·javascript
// content script: page facts, provisional identity
const pagePayload = {
  host_kind: "web-page",
  host_id: chrome.runtime.id,
  source: location.href,
  human_label: document.title || location.hostname,
  selection,
  selectionFresh,
  viewportText,
  readableText: viewportText,
  wordCount,
  viewport: { width, height, scrollX, scrollY, devicePixelRatio },
  scrollPosition: { y: scrollY, ofPageHeight: pageHeight },
  frameUrl: location.href,
  isTopFrame: true
}

// service worker: authoritative tab-scoped identity
const decoratedPayload = {
  ...payload,
  host_id: `chrome-tab:${sessionId}:window:${tab.windowId}:tab:${tab.id}`,
  browser_session_id: sessionId,
  chrome_tab_id: String(tab.id),
  chrome_window_id: String(tab.windowId),
  chrome_tab_key: tabKey,
  incognito: Boolean(tab.incognito)
}

Exercise

Live panel의 pippa:host-context payload 하나를 잡고 field마다 content-script-authored인지 service-worker-decorated인지 표시해. 같은 tab을 새 URL로 이동한 뒤 host_id, chrome_tab_key, source, snapshot_at을 비교하고, 같은 URL을 다른 tab에 열어서 다시 비교해.
Hint
Content script가 chrome.runtime.id를 임시로 내도 panel-facing payload는 decorated chrome-tab key를 가져야 해. Viewport 크기는 CSS pixel이고 DPR은 metadata야.

Progress

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

댓글 0

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

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