"input.value = 'foo' 를 넣으면 화면엔 'foo' 가 보여. 그런데 user가 전송을 누르면 서버엔 빈 문자열이 도착해. 이제 input의 주인은 React 고, 그냥 값을 꽂는 건 React 눈에 안 보이거든. 이 lesson은 현업에서 만날 framework를 거의 다 커버하는 한 줄짜리 요령이야."
순진한 set이 실패하는 이유
React는 HTMLInputElement 랑 HTMLTextAreaElement의 prototype을 갈아 끼워서, 자기가 만든 setter로 값 변화를 따라가. 그래서 input.value = 'foo'를 쓰면, 그 setter가 '이건 React 바깥에서 들어온 변화네' 하고 표시해 두고 변경 목록에서 조용히 빼 버려. 눈에 보이는 값은 바뀌어 (속성 자체는 움직이니까). 하지만 React의 속마음은 그대로라서, 다음번에 다시 그릴 때 원래대로 돌아가 버려.
Vue도 Svelte도 Solid도 모양만 조금 다를 뿐 비슷한 장치를 갖고 있어. 그래서 해법도 같아. framework가 씌워 놓은 껍데기를 건너뛰고 원래 setter에 직접 쓰는 거야.
Native setter trick
framework가 덮어쓰기 전의 원본 setter를 prototype에서 꺼내 오는 거야:
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value'
).set;
nativeInputValueSetter.call(input, 'foo');
input.dispatchEvent(new Event('input', { bubbles: true }));
여기서 두 가지가 일어나:
- 원본 setter가 값을 진짜로 써. 감시하는 껍데기를 안 거치니까 DOM이 조용히 갱신돼.
inputevent를 직접 쏴 줘. React도 다른 framework도 속마음을 갱신할 때 원래의inputevent를 듣거든. 이때 bubbles가 중요해. 대부분의 framework는 listener를 input 자체가 아니라 뿌리 쪽에 걸어 두니까.
이 둘을 같이 하면 실제로 키를 친 것과 구별이 안 돼. React의 onChange가 돌고, 속마음이 갱신되고, 다음에 그릴 때 새 값이 나와. 전송도 제대로 되고.
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는 이미 폐기 예정이라고 붙어 있는데도 아직 모든 브라우저에서 돌아가. Selection API로 대신하는 방법들 (navigator.clipboard.write 후 붙여넣기, 아니면 InputEvent를 직접 쏘기)은 더 복잡한 데다 framework 마다 결과가 달라. ClipDeck 한테는 execCommand가 현실적인 선택이야.
이게 중요할 때
ClipDeck v1은 입력창을 안 채워. clip은 꺼내 읽는 거지 밀어 넣는 게 아니거든. v2 계획표에 '지금 커서가 있는 입력창에 이 clip 붙이기' 가 올라와 있어. 조각을 모아 뒀다가 편집기나 채팅 앱에 붙여 쓰는 흐름을 위한 거야. 바로 그때 이 요령이 필요해져. 이걸 안 쓰면 Slack 이나 Gmail에 붙였을 때 화면엔 글자가 보이는데 실제로 전송되는 메시지는 텅 비어 있거든.
감지 — 이게 framework-managed 인가?
빠르게 보려면 input의 __reactProps 나 __reactInternalInstance를 읽어 봐 (React 내부 항목이라 이름은 버전마다 조금씩 달라). 그게 있으면 React 야. Vue는 요소에 __vueParentComponent를 달아 두고. Svelte는 눈에 띄는 표식이 없는데, 대신 input이 원래 event에 얌전히 반응해서 굳이 알아내지 않아도 이 요령이 그냥 통해.
직접 떠볼 수도 있어. 그냥 값을 꽂아 보고 한 박자 뒤에 다시 읽는 거야. 되돌아가 있으면 framework가 쥐고 있는 거지. 근데 실전에선 그냥 항상 원본 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);
}
}
갈래는 셋인데 부르는 쪽은 하나야. React 든 Vue 든 Svelte 든 Solid 든 통하고, contenteditable을 쓰는 편집기도 대부분 받아 줘.
input.value = ...를 그냥 쓰면 감시망을 조용히 비껴가 버려. 해법은 한 줄이야 — prototype에서 원본 setter를 꺼내 부르고, bubbles를 켠 input event를 쏴. 그러면 진짜로 키를 친 것과 구별이 안 돼.