본문 바로가기
빌드노트 검색

빌드노트

input.value를 바꿨는데 input 이벤트가 오지 않을 때

input.value 대입은 현재 값만 바꾸며 input 이벤트를 만들지 않는다. 같은 텍스트 입력에서 키 입력·대입·합성 이벤트의 횟수와 isTrusted를 비교하고, 공유 갱신 함수를 직접 호출할지 다른 DOM 리스너에 알릴지 판단한다.

스크립트로 입력창에 값을 채웠는데, input 리스너가 갱신하던 미리보기는 이전 내용으로 남아 있을 수 있다. input.value = ...는 현재 값을 바꾸지만 input 이벤트를 발생시키지 않는다. 미리보기 갱신을 이벤트 안에만 두었다면 스크립트로 값을 넣는 경로에서도 그 갱신 함수를 호출해야 한다.

MDN의 input 이벤트 설명 (새 탭에서 열림)은 사용자 편집과 프로그램의 값 대입을 구별한다. HTML 표준의 공통 이벤트 규칙 (새 탭에서 열림)도 스크립트가 폼 컨트롤의 값을 바꿀 때 이 이벤트들을 발생시키지 않는다고 명시한다. 대입 뒤에 이벤트를 기다리는 코드가 있다면, 기다리는 시간보다 갱신 경로부터 확인할 일이다.

같은 입력에서 값과 이벤트를 따로 확인한다

문제가 있는 코드에서 값을 채우는 대입문과 미리보기를 바꾸는 input 콜백을 찾아본다. 대입문은 실행됐는데 콜백이 실행되지 않았다면, 다음 작은 페이지로 두 동작의 차이를 확인할 수 있다. 네이티브 텍스트 입력을 대상으로 하며, 프레임워크나 외부 라이브러리는 사용하지 않는다.

아래 HTML을 UTF-8의 demo.html로 저장해 브라우저에서 열고 개발자 도구 Console도 연다. 입력할 때 미리보기를 바꾸고, Console에는 이벤트 누적 횟수·현재 값·isTrusted를 출력한다.

<!doctype html>
<meta charset="utf-8">
<title>value and input event</title>
<label>메시지 <input id="message" type="text" autocomplete="off"></label>
<p id="preview"></p>
<script>
const input = document.querySelector("#message");
const preview = document.querySelector("#preview");
let count = 0;
function syncPreview() {
  preview.textContent = input.value;
}
input.addEventListener("input", syncPreview);
input.addEventListener("input", event => {
  count += 1;
  console.log(count, input.value, event.isTrusted);
});
</script>

먼저 빈 입력창을 클릭하고 영문 a 한 글자를 입력한다. 입력창과 미리보기 모두 a가 되고 Console에는 1 a true가 찍힌다. 이어 Console에서 다음 두 줄을 한 줄씩 실행하며 화면과 출력을 비교한다. 첫 줄은 값 대입만, 둘째 줄은 같은 요소에 대한 명시적인 이벤트 발송이다.

input.value = "script";
input.dispatchEvent(new Event("input", { bubbles: true }));

이 글을 위해 Windows 11, Playwright 1.59.0, headless Chromium 156.0.8078.4의 새 비영속 컨텍스트에서 같은 비교를 실행했다. loopback 정적 페이지에 실험용 문자열만 사용했다. 키 입력은 자동화의 키보드 명령으로 전달했으며 물리 키보드로 직접 입력한 측정은 아니다. 관찰은 다음과 같았다.

  • a 키 입력 뒤: 현재 값과 미리보기 모두 a, 누적 이벤트 1회, 받은 이벤트의 isTrusted는 true였다.
  • value = "script" 뒤: 현재 값만 script로 바뀌고 미리보기는 a였다. 누적 횟수는 1회 그대로였고, 새 이벤트가 없으므로 새 isTrusted 값도 없었다. 150ms 뒤 조회에서도 같았다.
  • dispatchEvent(...) 뒤: 현재 값은 script 그대로이고 미리보기가 script로 바뀌었다. Console에는 2 script false가 추가됐다.

마지막 단계에서 미리보기를 바꾼 것은 dispatchEvent()가 호출한 기존 리스너다. 이벤트 자체가 새 문자열을 입력한 것은 아니다. 값을 다시 바꾸지 않고 같은 dispatch를 한 번 더 실행해도 누적 횟수는 3회로 늘었다. 따라서 합성 input을 받았다는 사실만으로 값이 직전과 달라졌다고 판단할 수도 없다.

자기 페이지에서 대입만 했는데도 리스너가 실행된다면 같은 현상으로 단정하지 않는다. 다른 코드의 이벤트 발송이나 프레임워크의 래퍼가 개입하는지 확인한다. 반대로 키보드로 입력해도 리스너가 실행되지 않는다면 선택자가 가리키는 요소와 리스너가 붙은 요소가 같은지부터 확인한다.

미리보기를 관리하는 코드라면 함수를 직접 호출한다

위 예제에서는 미리보기를 바꾸는 코드가 이미 syncPreview()로 분리돼 있다. 프로그램으로 값을 채우는 쪽에서도 이 함수를 호출하면 된다. 기존 페이지의 스크립트에 다음 함수를 추가해 값 대입과 미리보기 갱신을 함께 수행한다.

function setMessage(value) {
  input.value = value;
  syncPreview();
}
setMessage("direct");

직접 실행했을 때 입력창과 미리보기 모두 direct로 바뀌었고 이벤트 횟수는 늘지 않았다. 이후 다시 키보드로 편집했을 때도 기존 input 리스너가 미리보기를 갱신했다. 사용자 편집은 리스너에서, 프로그램의 값 변경은 setMessage()에서 같은 갱신 함수를 사용하는 방식이다.

자신이 관리하는 미리보기나 계산 결과만 갱신하면 되는 경우에 이 경로를 쓸 수 있다. 기존 코드의 갱신 로직이 익명 콜백 안에 있다면 먼저 함수로 꺼낸다. 이벤트 객체를 흉내 내서 넘길 필요 없이, 갱신 함수가 현재 값이나 필요한 인자를 읽도록 만든다.

다른 input 리스너에도 알려야 한다면

여러 코드가 네이티브 input 이벤트를 받기로 약속돼 있고, 프로그램의 값 변경도 그 경로로 알릴 필요가 있다면 앞서 실행한 dispatchEvent(new Event("input", { bubbles: true }))를 값 대입 뒤에 사용한다. 이 예제처럼 리스너가 현재 value를 읽는 경우에는 그 알림으로 갱신할 수 있다.

bubbles: true는 부모에 등록한 버블 단계 리스너에도 알리기 위한 옵션이다. 별도 관찰에서 이 옵션을 끄자 입력 자체의 리스너는 실행됐지만 부모의 버블 단계 리스너는 실행되지 않았다. 이벤트 위임을 쓰는 화면이라면 이 차이가 갱신 여부를 바꾼다.

MDN의 dispatchEvent 설명 (새 탭에서 열림)에 따르면 영향을 받는 리스너는 동기적으로 호출된다. 실험에서도 호출 전 기록 → 리스너 기록 → 호출 후 기록 순서였다. 다만 리스너가 시작한 비동기 작업까지 완료됐다는 뜻은 아니다. 직접 syncPreview()를 호출하고 같은 갱신 리스너를 dispatch로 다시 호출하면 갱신이 중복되므로, 이 예제에서는 두 경로 중 필요한 하나를 선택한다.

합성 이벤트와 브라우저의 편집 이벤트는 구분해서 다뤄야 한다. 실험에서 키 입력으로 받은 객체는 InputEvent였고, 코드로 만든 객체는 Event였다. 이 Event에는 텍스트 편집의 data나 inputType이 없고, 발송해도 keydown·keyup이 추가되지 않았다. 그런 정보까지 사용하는 리스너에 이 한 줄이 충분하다고 가정하면 안 된다.

MDN의 isTrusted 설명 (새 탭에서 열림)에 따르면 dispatchEvent()로 보낸 이벤트의 isTrusted는 false다. true는 브라우저가 생성했다는 구분이며 사람이 물리 키보드를 눌렀다는 인증은 아니다. 이번 자동화 키 입력에서도 true가 관찰됐다.

프레임워크가 관리하는 입력에서는 해당 프레임워크의 상태 갱신 경로도 확인해야 한다. 이 실험은 네이티브 DOM의 값과 리스너 실행을 확인한 것으로, 값 대입과 합성 이벤트만으로 프레임워크 상태까지 자동 반영된다는 근거는 없다. Firefox·Safari·모바일·IME 조합 입력과 자동완성은 검증하지 않았다. 여기서 선택할 것은 현재 값을 채운 뒤 공유 함수를 호출할지, 다른 DOM 리스너에 합성 알림을 보낼지다.

← 전체 글 보기