Pinterest 메모 Chrome 확장 프로그램 만들기

Salo 작성자: Salo 읽는 데 5분
Pinterest 메모 Chrome 확장 프로그램 만들기

이 튜토리얼에서는 쓸모 있는 최소 규모의 Chrome Manifest V3 프로토타입을 만듭니다. Pinterest 핀 페이지에 비공개 텍스트 입력란을 넣고 숫자 핀 ID를 키로 사용해 데이터를 chrome.storage.local에저장합니다. 의도적으로 프로토타입까지만 다루며 Pinterest의 예전 메모 기능을 복제한 상용 제품은 아닙니다.

Pinterest의 DOM은 예고 없이 바뀔 수 있습니다. 선택자를 한곳에 격리하고 Pinterest가 보호하는 브랜드 표현을 흉내 내지 말며 지원한다고 밝힌 모든 페이지 유형과 언어 환경에서 시험하세요.

1. Manifest V3 확장 프로그램 만들기

다음 두 파일을 담은 폴더를 만드세요: manifest.jsoncontent.js. 사용할 수 있는 키는 Chrome의 매니페스트 참조 문서를 기준으로 확인하세요.

{
  "manifest_version": 3,
  "name": "핀 맥락 프로토타입",
  "version": "0.1.0",
  "description": "Pinterest 핀에 비공개 로컬 메모를 저장합니다.",
  "permissions": ["storage"],
  "content_scripts": [{
    "matches": ["https://*.pinterest.com/*"],
    "js": ["content.js"],
    "run_at": "document_idle"
  }]
}

Pinterest가 클라이언트 측 탐색을 사용하므로 호스트 매칭 범위를 넓게 잡았습니다. 기능이 일부 경로나 국가 도메인만 지원한다면 범위를 좁히고 지원 대상을 문서에 명시하세요.

2. 핀 URL에서 안정적인 키 추출하기

카드 위치나 화면에 보이는 제목을 메모 키로 사용하지 마세요. 표준 /pin/…/ 경로에서 숫자 ID를 추출하고 그 밖의 모든 경우에는 null을 반환하세요.

function getPinId(url = location.href) {
  try {
    const { pathname } = new URL(url);
    const match = pathname.match(/\/pin\/(\d+)\/?/);
    return match ? match[1] : null;
  } catch {
    return null;
  }
}

올바른 상세 페이지 URL, 쿼리 문자열, 핀이 아닌 페이지, 잘못된 URL, 지원한다고 밝힌 모든 언어별 경로 형식을 시험하세요. ID가 확실하지 않은데도 조용히 메모를 연결해서는 안 됩니다.

3. 버전이 있는 레코드 저장하기

나중에 마이그레이션할 수 있도록 스키마를 명시적으로 정의하세요.

async function readNote(pinId) {
  const key = 'pin-note:' + pinId;
  const result = await chrome.storage.local.get(key);
  return result[key] ?? { version: 1, pinId, text: '', updatedAt: null };
}

async function writeNote(pinId, text) {
  const key = 'pin-note:' + pinId;
  const record = {
    version: 1,
    pinId,
    text,
    updatedAt: new Date().toISOString()
  };
  await chrome.storage.local.set({ [key]: record });
  return record;
}

Chrome 문서에 따르면 storage.local의 기본 한도는 10MB이며 별도의 unlimitedStorage 권한을 사용해 용량을 늘릴 수 있습니다. 확장 프로그램을 제거하면 로컬 확장 프로그램 저장소가 삭제된다는 점도 문서에 명시되어 있습니다. 최신 Storage API 문서를 읽고 사용자가 중요한 데이터를 저장하기 전에 내보내기 기능부터 설계하세요.

4. 격리되고 접근 가능한 편집기 장착하기

Shadow DOM은 의도하지 않은 스타일 충돌을 줄여 줍니다. 아래 예시는 쉽게 바뀌는 Pinterest 클래스명에 의존하지 않도록 페이지 body에 요소를 추가합니다.

async function mountEditor(pinId) {
  if (document.querySelector('[data-pin-context-root]')) return;

  const host = document.createElement('aside');
  host.dataset.pinContextRoot = '';
  host.setAttribute('aria-label', '비공개 핀 메모');
  document.body.append(host);

  const root = host.attachShadow({ mode: 'open' });
  const label = document.createElement('label');
  label.textContent = '비공개 메모';
  const editor = document.createElement('textarea');
  editor.id = 'pin-note-editor';
  label.htmlFor = editor.id;
  editor.setAttribute('aria-label', '이 핀의 비공개 메모');
  const status = document.createElement('span');
  status.setAttribute('role', 'status');

  const saved = await readNote(pinId);
  editor.value = saved.text;
  root.append(label, editor, status);
  bindAutosave(editor, status, pinId);
}

인터페이스 텍스트는 textContent로 만들고 신뢰할 수 없는 값을 innerHTML에 넣지 마세요. 눈에 잘 보이는 포커스 스타일, 충분한 명암비, 실제 레이블, 키보드 접근, 포커스를 빼앗지 않는 상태 텍스트도 추가합니다.

5. 키를 누를 때마다 쓰지 않고 저장하기

function bindAutosave(editor, status, pinId) {
  let saveTimer;
  editor.addEventListener('input', () => {
    status.textContent = '저장되지 않음';
    clearTimeout(saveTimer);
    saveTimer = setTimeout(async () => {
      try {
        await writeNote(pinId, editor.value);
        status.textContent = '로컬에 저장됨';
      } catch (error) {
        console.error('핀 메모 저장 실패', error);
        status.textContent = '저장할 수 없음';
      }
    }, 400);
  });
}

상용 버전에서는 페이지를 전환할 때 대기 중인 입력을 저장하고, 두 탭이 서로의 내용을 덮어쓰지 않도록 보호하고, 최대 메모 크기를 정의하고, 용량 한도 오류를 사용자에게 보여 줘야 합니다.

6. Pinterest의 클라이언트 측 탐색 처리하기

페이지를 불러올 때 한 번만 실행하는 핸들러는 피드와 상세 보기 사이의 이동을 놓칩니다. 여러 번 실행해도 결과가 같은 조정 함수를 만들고 DOM이 바뀔 때마다 UI를 다시 장착하지 않으면서 변화를 관찰하세요.

let scheduled = false;

async function reconcile() {
  scheduled = false;
  const pinId = getPinId();
  const existing = document.querySelector('[data-pin-context-root]');
  if (!pinId) {
    existing?.remove();
    return;
  }
  if (existing?.dataset.pinId === pinId) return;
  existing?.remove();
  await mountEditor(pinId);
  const mounted = document.querySelector('[data-pin-context-root]');
  if (mounted) mounted.dataset.pinId = pinId;
}

new MutationObserver(() => {
  if (scheduled) return;
  scheduled = true;
  requestAnimationFrame(reconcile);
}).observe(document.documentElement, { childList: true, subtree: true });

reconcile();

상용 버전은 가능한 경우 탐색 상태를 직접 감시하고 더 이상 필요 없는 옵서버를 해제해야 합니다. 긴 피드에서 코드 성능을 측정하세요. 기능이 올바르게 작동해도 페이지 성능을 떨어뜨린다면 좋은 확장 프로그램이 아닙니다.

7. Manifest V3 보안 제약 지키기

Manifest V3에서는 원격으로 호스팅한 실행 코드를 사용할 수 없습니다. 실행할 JavaScript를 확장 프로그램에 포함하고, 변경할 정당한 이유가 없다면 기본 콘텐츠 보안 정책을 유지하고, 동적 코드 실행을 피하고, 기능에 꼭 필요한 권한만 요청하세요. 플랫폼 모델은 Chrome의 Manifest V3 개요에서 설명합니다.

8. 실제 제품에 필요한 기능 추가하기

9. 실패 경로 시험하기

  1. 다음 페이지에서 압축 해제된 확장 프로그램을 불러옵니다: chrome://extensions.
  2. 피드, 보드, 핀 상세 페이지, 핀이 아닌 경로를 각각 방문합니다.
  3. 새로고침 없이 두 핀 사이를 이동하고 메모가 서로 분리되는지 확인합니다.
  4. 같은 핀을 탭 두 개에서 열고 충돌 처리를 시험합니다.
  5. 저장소를 비활성화하거나 용량 한도 오류를 재현하고 경고가 표시되는지 확인합니다.
  6. 내보낸 뒤 테스트 레코드를 삭제하고 다시 복구합니다.
  7. 키보드 탐색, 확대 및 축소, 스크린 리더 레이블, 어두운 페이지와 밝은 페이지에서의 표시를 시험합니다.

개발에서 얻을 교훈

텍스트 입력란은 쉬운 부분입니다. 신뢰할 수 있는 Pinterest 확장 프로그램에는 안정적인 식별자, 견고한 수명 주기 처리, 접근 가능한 UI, 제한된 권한, 문서화된 저장 방식, 검증된 데이터 이전 경로가 필요합니다. 기능 목록을 늘리기 전에 이런 기반부터 만드세요.

Pinterest 활용 체계 계속 만들기

다음 단계에 맞는 가이드를 이어서 읽어 보세요.

Salo

작성자 소개

Salo는 프로덕트 디자이너이자 Pinterest 파워 유저입니다. 디지털 정보 정리, Pinterest 활용법, 더 나은 사고를 돕는 도구에 관해 글을 씁니다.

Chrome용 Notestopin 사용해 보기

핀에 비공개 메모와 태그를 추가하고, 저장한 자료를 나중에 빠르게 찾아보세요.

Chrome에 추가