C.W.K.
Stream
Lesson 07 of 07 · published

ClipDeck edit 와 delete — Undo 가진 CRUD-U + CRUD-D

~14 min · clipdeck, crud, edit, delete, undo

Level 0Extension 입덕
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Track 3 가 write. Track 4 가 read. Track 7 이 edit 와 delete — CRUD 의 마지막 두 글자 — 그리고 '내가 방금 clip nuked' 를 '5 초 안에 되찾을 거' 로 바꾸는 Undo toast 로 닫음. 끝나면 ClipDeck 이 full lifecycle 가짐."

Inline edit 가 뭐냐

Side panel 에서 clip 의 텍스트 click; 그 자리에서 editable. Type. Enter 누르면 저장, Escape 면 revert. Modal 없음, tab navigation 없음, 별도 form 없음. Clip 의 text 와 title 둘 다 inline edit 지원; URL 과 timestamp 는 immutable (provenance, user-owned data 아님).

두 경쟁 접근:

  • contenteditable — text element 에 contenteditable="true" 설정. Native browser editing. 장점: free formatting control (Cmd+B 등), 좋은 키보드 처리. 단점: HTML 처리, 저장 시 sanitize 필요.
  • textarea 로 swap — click 시 렌더 된 텍스트를 <textarea> 로 교체. 장점: 항상 plain text, 예측 가능. 단점: 더 많은 코드, formatting 없음, focus 관리 수동.

ClipDeck v1 이 textarea ship — plain text 가 schema, 단순성이 자기 비용 지불. v2 가 rich clip 위해 contenteditable 추가 가능.

Edit flow

  1. User 가 clip 의 텍스트 click.
  2. Click handler 가 <p> 를 현재 텍스트 가진 <textarea> 로 교체, focus, select all.
  3. Enter (Shift 없이) 가 commit; Escape 가 revert; 바깥 click 도 revert.
  4. Commit 시 SW 가 update 된 clip 을 storage 에 write back (clips 배열의 같은 id 교체). storage.onChanged fire, row 가 새 텍스트로 re-render.

Title 도 같은 shape — click → input → Enter/Escape. Title 이 textarea 아닌 single-line input.

Undo 가진 Delete flow

각 row 의 inline trash icon. Click → row fade out, Undo toast ("Clip deleted. Undo") 로 잠깐 교체. Toast 가 5 초 지속; user 가 Undo click 하면 clip 복원. Timeout 지나면 clip 영구 사라짐 (또는 purge 예약 — 아래 참조).

5-초 window 구현 두 방법:

  • Soft delete — 제거 대신 clip 을 deletedAt: timestamp 로 flag. Render 가 그것들 filter out. 5 초 후, 실제 remove 호출 돔. Undo 가 deletedAt 을 null 로 flip back.
  • Stash and restore — clips 에서 실제 제거, 제거된 clip 을 메모리에 hold; Undo 시 push back. Timeout 지나면 메모리에서 drop.

Soft delete 가 SW eviction 에 더 robust (in-memory stash 가 worker 와 함께 죽음). ClipDeck v1 이 soft delete 사용하고 deletedAt < now - 5s 인 거 정리하는 주기 cleanup alarm 돌림.

Multi-select 변형

Edit 와 delete 존재하면 다음 obvious feature 가 multi-select: "이 12 clip 한꺼번에 delete." Side panel 이 필요:

  • 각 row 의 checkbox 열, user 가 select mode 진입까지 hidden.
  • Select mode toggle 하는 'Select' 버튼 (row 들이 이제 checkbox 보임; row click 이 checkbox toggle).
  • 아무거나 선택될 때 상단의 bulk-action bar ("3 selected | Delete | Export | Tag").

같은 Undo 패턴: bulk delete 가 모든 선택 clip 을 같은 deletedAt 으로 soft-flag; Undo 버튼 하나가 모두 복원. ClipDeck v1 이 single-row delete ship; multi-select 는 v1.1 로.

Edit race

미묘한 버그: user 가 clip 의 edit mode 열기. 다른 window (또는 clip 추가하는 다른 tab) 에서 storage.onChanged fire. Render 돔, edit 중 textarea blow away. 방어: panel.js 메모리에 editingId 유지; onChanged-trigger 된 render 동안 editingId === clip.id 인 row 재렌더 skip.

이게 inline edit 와 live update 가진 어떤 list 에서도 흔한 gotcha. ClipDeck 의 storage 층이 popup / panel / content script 간 공유 — 그 어느 것이든 언제든 render trigger 가능.

Track 7 마무리

일곱 track 완성:

  • MV3 foundation / service worker / content script / side panel / action+popup / permission / DOM tool.
  • Full CRUD: Create (Track 3) / Read (Track 4) / Update + Delete (이 lesson).
  • Trigger: floating 버튼 / 키보드 / popup / context menu / omnibox.
  • Surface allocation: popup / side panel / options page placeholder.
  • Per-tab pause / just-in-time permission / Readability / screenshot / React-friendly fill / preview-and-confirm.

Track 8 이 packaging — build system, .crx vs unpacked, Chrome Web Store submission, icon / privacy policy / 동작 폴더를 다른 사람이 install 할 수 있는 것으로 만드는 모든 것.

Inline edit + soft delete + Undo toast = 대부분 user 가 실제로 원하는 full lifecycle. Edit 가 빠르게 commit, delete 가 5 초간 reversible, side panel 이 fragile 박물관 처럼 안 느껴짐.
Audit log option. 일부 user 가 뭘 언제 delete 했는지 알고 싶음 — 5-초 window 너머 가끔 recovery 필요한 clip-collection workflow 에 유용. v2 가 30-일 retention 의 trash bucket 추가 가능, Settings tab 에 surface. v1 이 단순 유지: 5 초, 다음 사라짐.

Code

panel.js — inline edit handler·javascript
// panel.js — inline edit + soft delete + Undo toast
let editingId = null;
const editingState = new WeakMap(); // textarea -> {originalText}

function onClipClick(event) {
  const row = event.target.closest(".row");
  if (!row) return;
  const id = row.dataset.id;
  if (event.target.classList.contains("copyBtn")) return; // Track 4 에서 처리
  if (event.target.classList.contains("deleteBtn")) return softDelete(id, row);
  if (event.target.matches(".text")) return startEdit(id, event.target);
}

function startEdit(id, p) {
  editingId = id;
  const ta = document.createElement("textarea");
  ta.value = p.textContent;
  ta.style.width = "100%";
  editingState.set(ta, { originalText: p.textContent });
  p.replaceWith(ta);
  ta.focus(); ta.select();
  ta.addEventListener("keydown", (e) => {
    if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); commitEdit(id, ta); }
    if (e.key === "Escape") { e.preventDefault(); cancelEdit(ta); }
  });
  ta.addEventListener("blur", () => cancelEdit(ta));
}

async function commitEdit(id, ta) {
  const newText = ta.value.trim();
  const { clips = [] } = await chrome.storage.local.get("clips");
  const next = clips.map((c) => c.id === id ? { ...c, text: newText, updatedAt: Date.now() } : c);
  editingId = null;
  await chrome.storage.local.set({ clips: next });
  // render 가 onChanged 통해 fire
}

function cancelEdit(ta) {
  const state = editingState.get(ta);
  editingId = null;
  if (!state) return;
  const p = document.createElement("p");
  p.className = "text";
  p.textContent = state.originalText;
  ta.replaceWith(p);
}
panel.js — Undo affordance 가진 soft delete·javascript
// panel.js — soft delete + Undo toast
async function softDelete(id, row) {
  const { clips = [] } = await chrome.storage.local.get("clips");
  const target = clips.find((c) => c.id === id);
  if (!target) return;
  const next = clips.map((c) => c.id === id ? { ...c, deletedAt: Date.now() } : c);
  await chrome.storage.local.set({ clips: next });
  showUndoToast(target);
}

function showUndoToast(clip) {
  const root = document.getElementById("toast");
  root.innerHTML = `Clip deleted. <button class="undoBtn">Undo</button>`;
  root.classList.add("show");
  const timer = setTimeout(() => {
    root.classList.remove("show");
    root.innerHTML = "";
  }, 5000);
  root.querySelector(".undoBtn").addEventListener("click", async () => {
    clearTimeout(timer);
    const { clips = [] } = await chrome.storage.local.get("clips");
    const restored = clips.map((c) => c.id === clip.id ? { ...c, deletedAt: undefined } : c);
    await chrome.storage.local.set({ clips: restored });
    root.classList.remove("show");
    root.innerHTML = "";
  });
}
background.js — 만료된 soft-deleted clip 의 주기 purge·javascript
// background.js — undo window 지난 soft-deleted clip purge 하는 alarm
chrome.runtime.onInstalled.addListener(() => {
  chrome.alarms.create("clipdeck-purge", { periodInMinutes: 1 });
});

chrome.alarms.onAlarm.addListener(async (alarm) => {
  if (alarm.name !== "clipdeck-purge") return;
  const cutoff = Date.now() - 5_500; // 5s + 작은 buffer
  const { clips = [] } = await chrome.storage.local.get("clips");
  const trimmed = clips.filter((c) => !c.deletedAt || c.deletedAt > cutoff);
  if (trimmed.length !== clips.length) {
    await chrome.storage.local.set({ clips: trimmed });
  }
});

// panel.js 의 render() 가 c.deletedAt filter 필수:
//   const visible = clips.filter((c) => !c.deletedAt);

External links

Exercise

첫 번째와 두 번째 code block 을 clipdeck/panel.js 에 추가. document.addEventListener('click', onClipClick) wire 해서 dispatcher 가 모든 관련 click 잡게. 기존 render() 의 clip row 당 작은 trash 버튼 (<button class="deleteBtn">🗑️</button>) 추가. render() update 해서 soft-deleted clip 거름: clips.filter((c) => !c.deletedAt). 세 번째 code block (purge alarm) 을 background.js 에 추가. Reload. Clip 몇 개 저장. 아무 clip 의 텍스트 click — textarea 됨, focus. Edit, Enter 누르기 — text 가 storage 에 update 되고 row re-render. Escape 시도 — revert. 다른 clip 의 trash click — row 사라짐, Undo toast 가 5 초 보임. 5 초 안에 Undo click — clip 돌아옴. 5 초 지나기 — toast 사라짐, alarm 이 다음 분 안에 fire 하고 clip 영구 purge.
Hint
다른 window 가 clip 저장할 때 edit textarea 가 edit 중 blow away 되면, editingId skip wire 안 한 거 — render 의 row update 를 wrap 해서 id 가 editingId 매칭하는 row 어느 것이든 skip. Undo 버튼이 아무것도 안 하면, toast root element 가 click handler fire 전 clear 됐을 수도 (timer race) — click handler 돈 후 timer 를 null 로 설정해서 두 번째 teardown 방지. Trash delete 가 jumpy 느낌이면, row fade-out animate: 제거 전 200ms opacity transition 이 즉시 사라짐보다 훨씬 부드럽게 보임.

Progress

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

댓글 0

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

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