자바스크립트 퀴즈북 리마인드 Day 13: 모듈은 파일 묶음이.
ESM live binding, default/named export, circular dependency, dynamic import, CJS와 ESM 차이를 프론트엔드 코드 리뷰 관점에서 정리합니다.
오늘의 질문: “버튼에 클릭 핸들러를 달았으니 이 이벤트는 버튼에서만 처리된다고 봐도 될까?”
const list = document.querySelector(".todo-list");
list.addEventListener("click", (event) => {
if (event.target.matches("button[data-delete]")) {
deleteTodo(event.target.dataset.id);
}
});
삭제 버튼마다 핸들러를 붙인 코드가 아닙니다. 리스트 하나에 핸들러를 붙여 두고, 클릭 이벤트가 위로 올라오는 흐름을 이용합니다. 이것이 event delegation입니다. 새 항목이 나중에 추가되어도 리스트의 핸들러 하나가 계속 처리할 수 있습니다.
브라우저 이벤트는 보통 세 단계를 거칩니다.
| 단계 | 방향 | 코드 리뷰에서 볼 질문 |
|---|---|---|
| capture | 바깥 → target | 부모가 먼저 가로채야 하는 이유가 있는가? |
| target | 실제 이벤트 발생 요소 | event.target과 현재 핸들러 위치가 같은가? |
| bubble | target → 바깥 | delegation이나 부모 공통 처리를 의도했는가? |
기본 addEventListener("click", handler)는 bubble 단계에서 동작합니다. capture 단계에서 듣고 싶다면 세 번째 인자나 options를 써야 합니다.
document.addEventListener("click", logBeforeTarget, { capture: true });
document.addEventListener("click", logAfterTarget);
이 차이를 모르면 “왜 부모 핸들러가 먼저 실행되지?” 또는 “왜 modal 바깥 클릭이 버튼 클릭까지 같이 닫아 버리지?” 같은 버그를 잘못 읽습니다.
이벤트 핸들러에서 자주 섞이는 값이 target과 currentTarget입니다.
list.addEventListener("click", (event) => {
console.log(event.target); // 실제 클릭된 가장 안쪽 요소
console.log(event.currentTarget); // 핸들러가 붙어 있는 list
});
버튼 안에 아이콘 <span>이 있으면 event.target은 버튼이 아니라 span일 수 있습니다. 그래서 delegation 코드는 대개 closest로 의도를 조금 더 안전하게 표현합니다.
list.addEventListener("click", (event) => {
const button = event.target.closest("button[data-delete]");
if (!button || !list.contains(button)) return;
deleteTodo(button.dataset.id);
});
리뷰 질문은 간단합니다.
이 코드는 실제 클릭된 노드가 바뀌어도 의도한 요소를 찾을 수 있는가?
event.stopPropagation()은 이벤트가 더 위로 올라가지 못하게 합니다. modal, dropdown, nested clickable card 같은 곳에서 필요할 수 있습니다. 하지만 습관적으로 넣으면 부모의 접근성 처리, analytics, delegation 핸들러를 조용히 끊을 수 있습니다.
button.addEventListener("click", (event) => {
event.stopPropagation();
openMenu();
});
리뷰에서는 “정말 전파를 막아야 하는가?”를 봅니다. 단순히 부모 핸들러가 귀찮아서 막는다면 구조를 다시 잡는 편이 낫습니다. 예를 들어 부모가 클릭 위치를 더 명확히 검사하거나, 버튼과 카드의 역할을 분리할 수 있습니다.
preventDefault()는 전파를 막는 함수가 아닙니다. 링크 이동, 폼 제출, 체크박스 토글 같은 브라우저의 기본 동작을 막습니다.
form.addEventListener("submit", async (event) => {
event.preventDefault();
await saveForm(new FormData(form));
});
프론트엔드에서 이 둘을 섞으면 버그가 생깁니다.
| 원하는 일 | 맞는 도구 |
|---|---|
| 폼의 새로고침 제출 막기 | preventDefault() |
| 부모 클릭 핸들러로 안 올리기 | stopPropagation() |
| 둘 다 필요한지 판단하기 | 의도를 별도로 설명하기 |
React에서 onClick은 브라우저의 native event를 그대로 노출하기보다 React의 synthetic event 시스템을 거칩니다. 최신 React에서는 예전처럼 event pooling 때문에 값을 비동기에서 못 읽는 문제는 크게 줄었지만, 리뷰 포인트는 여전히 남습니다.
function Card({ onOpen }: { onOpen: () => void }) {
return (
<article onClick={onOpen}>
<button
onClick={(event) => {
event.stopPropagation();
bookmark();
}}
>
저장
</button>
</article>
);
}
여기서 버튼 클릭이 카드 열기까지 이어지면 UX가 깨집니다. 반대로 모든 내부 버튼에 stopPropagation을 흩뿌리면 나중에 analytics나 키보드 접근성 흐름이 끊길 수 있습니다. React 코드에서도 결국 질문은 같습니다.
target, closest, contains 검사가 충분한가?stopPropagation이 부모의 공통 처리나 analytics를 끊지 않는가?preventDefault와 stopPropagation을 목적에 맞게 구분했는가?이벤트 코드는 “핸들러가 어디 붙었는가”만 보면 부족합니다. 실제 클릭된 노드, 현재 핸들러 위치, 전파 단계, 기본 동작, 프레임워크의 이벤트 레이어까지 이어서 읽어야 합니다.
Post Q&A
자바스크립트 퀴즈북 리마인드 Day 12: 이벤트는 target에서 끝나지 않는다 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!
ESM live binding, default/named export, circular dependency, dynamic import, CJS와 ESM 차이를 프론트엔드 코드 리뷰 관점에서 정리합니다.
DOM 이벤트의 capture/bubble, delegation, default action, React synthetic event 경계를 프론트엔드 코드 리뷰 관점에서 정리합니다.
프로토타입 조회, class 문법, 인스턴스 필드와 prototype 메서드 차이를 프론트엔드 코드 리뷰 관점에서 정리합니다.
고차 함수, 순수 함수, 부수 효과, async 함수의 Promise 반환을 코드 리뷰에서 어떻게 읽을지 정리합니다.
this 바인딩을 정의 위치가 아니라 호출 형태, 메서드 분리, arrow function, bind 기준으로 읽는 방법을 코드 리뷰 관점에서 정리합니다.
콜 스택, 태스크 큐, 마이크로태스크 큐를 기준으로 Promise와 setTimeout 실행 순서를 코드 리뷰 관점에서 정리합니다.
클로저가 값 복사가 아니라 렉시컬 환경 참조라는 점을 stale closure, 루프 콜백, React 이벤트 리뷰 관점에서 정리합니다.
스코프 체인, 렉시컬 환경, var/let/const 호이스팅 차이를 콜백·상태 버그 리뷰 관점에서 다시 정리합니다.
객체 참조와 얕은 복사, Object와 Map/Set의 차이, WeakMap/WeakSet이 필요한 메모리 상황을 프론트엔드 코드 리뷰 관점에서 정리합니다.
객체 프로퍼티를 key/value가 아니라 descriptor로 읽는 법, getter/setter와 defineProperty, preventExtensions/seal/freeze의 경계를 코드 리뷰 관점에서 정리합니다.
객체가 원시값으로 바뀌는 ToPrimitive 흐름, + 연산자의 문자열 연결과 숫자 덧셈 분기, Symbol.toPrimitive가 코드 리뷰에서 왜 중요한지 정리합니다.
==, ===, Object.is, SameValueZero가 각각 어떤 비교 알고리즘을 쓰는지 정리하고, includes와 indexOf, Map/Set 키 비교에서 생기는 프론트엔드 리뷰 포인트를 잡습니다.
자바스크립트의 Number가 왜 정수처럼 보여도 부동소수점 모델 위에서 움직이는지, NaN과 -0, safe integer, BigInt를 프론트엔드 코드 리뷰 관점에서 다시 정리합니다.