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

React-friendly fill input — Native setter trick

~11 min · react, fill-input, synthetic-events, framework

Level 0Extension 입덕
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"input.value = 'foo' 설정. UI 가 'foo' 보임. User 가 submit click 하고 server 가 빈 문자열 받음. React 가 이제 input 소유, 순진한 set 이 tracker 에 invisible. Lesson 5 가 production 에서 만날 모든 framework 에 fix 하는 한 줄 trick."

순진한 set 이 실패하는 이유

React 가 HTMLInputElementHTMLTextAreaElement 의 prototype 을 patch 해서 custom setter 통해 value 변경 추적. input.value = 'foo' 쓰면, patched setter 가 변경이 React component lifecycle 바깥에서 온 거 노트하고 change tracker 에서 silently discard. Visible value 가 update (underlying property 가 여전히 움직임), 하지만 React 의 internal state 가 뭐였든 유지, 다음 render 가 snap back.

Vue, Svelte, Solid 도 유사 mechanism 을 살짝 다른 shape 으로 가짐. Fix 는 같음: framework 가 install 한 어떤 wrapper 든 우회하고 native setter 에 직접 write.

Native setter trick

Framework 가 overwrite 할 기회 갖기 전 prototype 에서 원본 setter 얻기:

const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
  window.HTMLInputElement.prototype,
  'value'
).set;

nativeInputValueSetter.call(input, 'foo');
input.dispatchEvent(new Event('input', { bubbles: true }));

두 가지 일어남:

  1. Native setter 가 실제 property write. Tracker wrap 없음; DOM 이 소란 없이 update.
  2. Synthetic input event fire. React (와 다른 모든 framework) 가 internal state update 위해 native input event listen. Bubbling 이 중요 — 대부분 framework 가 input 자체 아닌 root 에 listener attach.

함께 실제 keystroke 와 구별 안 됨. React 의 onChange 가 돔, internal state update, 다음 render 가 새 value 렌더. Submit 동작.

Textarea 와 contenteditable

<textarea>HTMLTextAreaElement.prototype 사용:

const nativeTextareaValueSetter = Object.getOwnPropertyDescriptor(
  window.HTMLTextAreaElement.prototype,
  'value'
).set;

contenteditable 요소 (Slack, Notion, Gmail 의 compose) 엔 trick 이 다름:

el.focus();
document.execCommand('insertText', false, 'foo');

document.execCommand 가 deprecated 지만 모든 browser 에서 여전히 동작. Selection-API-based 대체 (navigator.clipboard.write + paste, 또는 InputEvent dispatch) 가 더 복잡하고 framework 별 다양. ClipDeck 엔 execCommand 가 실용적 선택.

이게 중요할 때

ClipDeck v1 이 input 안 채움 — clip 이 read-out, paste-in 아님. v2 의 roadmap 에 user 가 snippet 수집해서 text editor 나 chat app 에 paste 하는 workflow 위한 'focus 된 input 에 이 clip paste'. 그게 정확히 native setter trick 중요할 때; 그것 없이 Slack 이나 Gmail 에 paste 가 UI 에 텍스트 보이지만 실제 보낸 메시지 빈 채.

감지 — 이게 framework-managed 인가?

빠른 확인: input 의 __reactProps__reactInternalInstance read (React 의 internal field; 정확한 이름이 React 버전에 따라 다양). 있으면 framework 가 React. Vue 가 element 에 __vueParentComponent 사용. Svelte 는 obvious marker 없지만 input 이 native event 에 정상 응답, trick 이 detection 없이도 여전히 동작.

Feature-detect 도 가능: 순진한 set 시도, tick 후 value back read. Revert 되면 framework 가 소유. 실용에선 항상 native setter 사용 — 둘 다 케이스에서 올바름.

모두 cover 하는 한 helper

Wrap:

function fillInput(el, value) {
  if (el.tagName === 'INPUT') {
    const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
    setter.call(el, value);
    el.dispatchEvent(new Event('input', { bubbles: true }));
  } else if (el.tagName === 'TEXTAREA') {
    const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
    setter.call(el, value);
    el.dispatchEvent(new Event('input', { bubbles: true }));
  } else if (el.isContentEditable) {
    el.focus();
    document.execCommand('insertText', false, value);
  }
}

세 branch, 하나의 consistent caller. React / Vue / Svelte / Solid 와 대부분 contenteditable rich editor 에 동작.

Framework 가 state 추적 위해 input setter patch. 순진한 `input.value = ...` 가 silently tracker 우회. Fix 는 한 줄: prototype 에서 native setter 빼서 호출, bubbling input event dispatch. 실제 keystroke 와 구별 안 됨.
타이핑한 거 항상 보여 주기. Invisible fill — user 가 타이핑 된 value 안 보고 paste-then-submit — 이 user 한테 무서운 자동화이고 Chrome Web Store reviewer 한테 trip wire. 항상 inserted text 를 input 에 렌더하고 submit 전 user 한테 back out 기회 줘. ClipDeck v2 가 모든 paste action 에 preview-and-confirm overlay (Lesson 6) surface 예정.

Code

content.js — 세 input shape 모두 cover 하는 fillInput 도구·javascript
// content.js — input / textarea / contenteditable cover 하는 fillInput 도구
CD_TOOLS.fillInput = async ({ selector, value }) => {
  const el = document.querySelector(selector);
  if (!el) return { ok: false, reason: "no-element" };
  if (el.tagName === "INPUT") {
    const setter = Object.getOwnPropertyDescriptor(
      window.HTMLInputElement.prototype,
      "value"
    ).set;
    setter.call(el, value);
    el.dispatchEvent(new Event("input", { bubbles: true }));
    return { ok: true };
  }
  if (el.tagName === "TEXTAREA") {
    const setter = Object.getOwnPropertyDescriptor(
      window.HTMLTextAreaElement.prototype,
      "value"
    ).set;
    setter.call(el, value);
    el.dispatchEvent(new Event("input", { bubbles: true }));
    return { ok: true };
  }
  if (el.isContentEditable) {
    el.focus();
    const inserted = document.execCommand("insertText", false, value);
    return { ok: inserted };
  }
  return { ok: false, reason: "not-an-input" };
};
background.js — paste-clip-into-focused-input flow·javascript
// background.js or popup.js — focus 된 input 에 clip paste 위해 도구 호출
async function pasteClipIntoFocused(clipText) {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  if (!tab?.id) return;
  // Content script 에서 현재-focus 요소의 selector 얻기
  const [{ result: focusedSelector }] = await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => {
      const el = document.activeElement;
      if (!el || el === document.body) return null;
      // 순진: 가능하면 id selector, 아니면 tagname index.
      if (el.id) return `#${CSS.escape(el.id)}`;
      const tag = el.tagName.toLowerCase();
      const all = Array.from(document.querySelectorAll(tag));
      return `${tag}:nth-of-type(${all.indexOf(el) + 1})`;
    },
  });
  if (!focusedSelector) return;
  await chrome.tabs.sendMessage(tab.id, {
    type: "tool",
    name: "fillInput",
    args: { selector: focusedSelector, value: clipText },
  });
}
content.js — robust round-trip 위한 더 단단한 active-element selector·javascript
// content.js — bonus: active element 의 더 robust selector
function generateSelector(el) {
  if (el.id) return `#${CSS.escape(el.id)}`;
  const parts = [];
  let node = el;
  while (node && node.nodeType === 1 && node !== document.body) {
    const tag = node.tagName.toLowerCase();
    const sibs = node.parentElement
      ? Array.from(node.parentElement.children).filter((s) => s.tagName === node.tagName)
      : [node];
    const idx = sibs.indexOf(node) + 1;
    parts.unshift(`${tag}:nth-of-type(${idx})`);
    node = node.parentElement;
  }
  return parts.join(" > ");
}

External links

Exercise

첫 번째 code block (CD_TOOLS.fillInput) 을 clipdeck/content.js 에 추가. Text input 가진 React-based site 아무거나 (https://react.dev 의 playground 나 Twitter/X 의 compose 시도) 열기. Content script 의 DevTools console 에서 input query — 예 document.querySelector('input[type="text"]') — 다음 그 element 의 selector 와 CD_TOOLS.fillInput 직접 호출. 두 가지 확인: input 이 시각적으로 텍스트 보이고, AND tab 하거나 다른 데 click 할 때 framework 가 실제 edit 으로 다룸 (예: tweet 아래 character count update, validation icon flip 등). 순진한 set 과 비교: input.value = 'foo' 직접 시도 — value 나타나지만 framework 가 무시. Native setter trick 이 차이 만듬.
Hint
Contenteditable branch 가 silently 아무것도 안 하면, element 가 실제로 contenteditable 아닐 수도 (일부 framework 가 InputEvent 특히 intercept 하는 복잡한 slate/ProseMirror editor 사용). 그것들엔 framework-specific 지식 필요 — ClipDeck v1 scope 한참 넘음. Selector 가 SW round-trip 에서 null 반환하면, query 와 fillInput 실행 사이 active element 가 바뀐 것 — popup 열릴 때 흔함. 전체 flow 를 content-script-side handler 로 옮겨 active element 가 synchronously capture 되게.

Progress

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

댓글 0

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

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