본문으로 건너뛰기

JavaScript 치트시트

9. 이벤트

9.1 마우스 이벤트

<h3>마우스 이벤트 테스트</h3>
<button id="myBtn" style="padding: 10px; margin: 5px; cursor: pointer;">
마우스 이벤트 테스트
</button>
<div
id="log"
style="border: 1px solid #ccc; padding: 10px; height: 100px; overflow-y: auto; font-family: monospace;"
></div>

<script>
const btn = document.querySelector('#myBtn');
const log = document.querySelector('#log');

function addLog(msg) {
log.innerHTML += msg + '<br>';
log.scrollTop = log.scrollHeight;
}

btn.addEventListener('click', () => {
console.log('버튼 클릭!');
addLog('클릭!');
});

btn.addEventListener('dblclick', () => {
console.log('더블 클릭!');
addLog('더블클릭!');
});

btn.addEventListener('mouseover', () => {
btn.style.backgroundColor = 'yellow';
addLog('마우스 오버');
});

btn.addEventListener('mouseout', () => {
btn.style.backgroundColor = '';
addLog('마우스 아웃');
});

btn.addEventListener('mousedown', () => {
console.log('마우스 버튼 누름');
addLog('마우스 다운');
});

btn.addEventListener('mouseup', () => {
console.log('마우스 버튼 놓음');
addLog('마우스 업');
});

btn.addEventListener('mousemove', e => {
console.log(`마우스 위치: ${e.clientX}, ${e.clientY}`);
if (Math.random() < 0.1) {
addLog(`마우스 위치: ${e.clientX}, ${e.clientY}`);
}
});

btn.addEventListener('contextmenu', e => {
e.preventDefault();
console.log('우클릭 방지됨');
addLog('우클릭 방지됨');
});
</script>

9.2 키보드 이벤트

<h3>키보드 이벤트 테스트</h3>
<input
id="myInput"
placeholder="여기에 타이핑해보세요 (Ctrl+S, Alt+Enter 시도)"
style="padding: 8px; width: 300px; margin: 5px;"
/>
<div
id="keyLog"
style="border: 1px solid #ccc; padding: 10px; height: 100px; overflow-y: auto; font-family: monospace;"
></div>

<script>
const input = document.querySelector('#myInput');
const keyLog = document.querySelector('#keyLog');

function addKeyLog(msg) {
keyLog.innerHTML += msg + '<br>';
keyLog.scrollTop = keyLog.scrollHeight;
}

input.addEventListener('keydown', e => {
console.log('Key down:', e.key);
addKeyLog(`keydown: ${e.key}`);

if (e.ctrlKey && e.key === 's') {
e.preventDefault();
console.log('Ctrl+S 저장 단축키 감지');
addKeyLog('Ctrl+S 저장 단축키 감지!');
}
});

input.addEventListener('keyup', e => {
console.log('Key up:', e.key);
addKeyLog(`keyup: ${e.key}`);
});

input.addEventListener('keypress', e => {
console.log('Key press:', e.key);
addKeyLog(`keypress: ${e.key}`);
});

// 전역 키 조합 감지
document.addEventListener('keydown', e => {
if (e.altKey && e.key === 'Enter') {
console.log('Alt+Enter 조합 감지');
addKeyLog('Alt+Enter 조합 감지!');
}
if (e.shiftKey && e.key === 'F10') {
console.log('Shift+F10 조합 감지');
addKeyLog('Shift+F10 조합 감지!');
}
});
</script>

9.3 폼 이벤트

<h3>폼 이벤트 테스트</h3>
<form id="myForm" style="margin: 10px 0;">
<input
id="myText"
type="text"
placeholder="텍스트 입력 (포커스 테스트)"
style="padding: 5px; margin: 2px;"
/><br />
<select id="mySelect" style="padding: 5px; margin: 2px;">
<option value="">선택하세요</option>
<option value="apple">사과</option>
<option value="banana">바나나</option>
<option value="orange">오렌지</option></select
><br />
<button type="submit" style="padding: 5px 10px; margin: 2px;">제출</button>
<button type="reset" style="padding: 5px 10px; margin: 2px;">리셋</button>
</form>
<div
id="formLog"
style="border: 1px solid #ccc; padding: 10px; height: 100px; overflow-y: auto; font-family: monospace;"
></div>

<script>
const form = document.querySelector('#myForm');
const select = document.querySelector('#mySelect');
const textInput = document.querySelector('#myText');
const formLog = document.querySelector('#formLog');

function addFormLog(msg) {
formLog.innerHTML += msg + '<br>';
formLog.scrollTop = formLog.scrollHeight;
}

form.addEventListener('submit', e => {
e.preventDefault();
console.log('폼 제출!');
addFormLog('폼 제출!');
});

select.addEventListener('change', e => {
console.log('선택 변경:', e.target.value);
addFormLog(`선택 변경: ${e.target.value}`);
});

textInput.addEventListener('input', e => {
console.log('입력 중:', e.target.value);
addFormLog(`입력 중: ${e.target.value}`);
});

textInput.addEventListener('focus', () => {
console.log('입력 필드에 포커스');
addFormLog('포커스 획득');
});

textInput.addEventListener('blur', () => {
console.log('입력 필드에서 포커스 잃음');
addFormLog('포커스 잃음');
});

textInput.addEventListener('select', () => {
console.log('텍스트 선택됨');
addFormLog('텍스트 선택됨');
});

form.addEventListener('reset', () => {
console.log('폼 리셋됨');
addFormLog('폼 리셋됨');
});
</script>

9.4 윈도우 및 문서 이벤트

<h3>윈도우 및 문서 이벤트 테스트</h3>
<button onclick="window.location.hash = 'test' + Date.now()" style="padding: 5px; margin: 2px;">
해시 변경
</button>
<button onclick="alert('창 크기를 변경하거나 스크롤해보세요!')" style="padding: 5px; margin: 2px;">
도움말
</button>
<div style="height: 200px; overflow-y: auto; border: 1px solid #ccc;" id="scrollArea">
<div style="height: 500px; padding: 20px;">
스크롤 테스트 영역<br />
스크롤을 해보세요!<br /><br />
더 많은 내용...<br /><br />
더 많은 내용...<br /><br />
더 많은 내용...<br /><br />
더 많은 내용...<br /><br />
더 많은 내용...<br /><br />
맨 아래!
</div>
</div>
<div
id="windowLog"
style="border: 1px solid #ccc; padding: 10px; height: 100px; overflow-y: auto; font-family: monospace;"
></div>

<script>
const windowLog = document.querySelector('#windowLog');
const scrollArea = document.querySelector('#scrollArea');

function addWindowLog(msg) {
windowLog.innerHTML += msg + '<br>';
windowLog.scrollTop = windowLog.scrollHeight;
}

// 페이지 로드 이벤트
window.addEventListener('load', () => {
console.log('페이지 완전 로드됨');
addWindowLog('페이지 완전 로드됨');
});

document.addEventListener('DOMContentLoaded', () => {
console.log('DOM 로드 완료');
addWindowLog('DOM 로드 완료');
});

// 리사이즈 이벤트
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
console.log(`창 크기: ${window.innerWidth} x ${window.innerHeight}`);
addWindowLog(`창 크기: ${window.innerWidth} x ${window.innerHeight}`);
}, 100);
});

// 스크롤 이벤트 (스크롤 영역)
let scrollTimeout;
scrollArea.addEventListener('scroll', () => {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
console.log(`스크롤 위치: ${scrollArea.scrollTop}`);
addWindowLog(`스크롤: ${scrollArea.scrollTop}`);
}, 100);
});

// 해시 변경 이벤트
window.addEventListener('hashchange', () => {
console.log('URL 해시 변경:', window.location.hash);
addWindowLog(`해시 변경: ${window.location.hash}`);
});

// 페이지 가시성 변경
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('페이지가 숨겨짐');
addWindowLog('페이지 숨겨짐');
} else {
console.log('페이지가 다시 보임');
addWindowLog('페이지 다시 보임');
}
});
</script>

9.5 터치 이벤트 (모바일)

<h3>터치 이벤트 테스트 (모바일)</h3>
<div
id="touchArea"
style="width: 200px; height: 100px; border: 2px dashed #ccc; padding: 20px; text-align: center; margin: 10px 0; background: #f9f9f9; user-select: none;"
>
터치 영역
</div>
<div
id="touchLog"
style="border: 1px solid #ccc; padding: 10px; height: 100px; overflow-y: auto; font-family: monospace;"
></div>

<script>
const touchArea = document.querySelector('#touchArea');
const touchLog = document.querySelector('#touchLog');

function addTouchLog(msg) {
touchLog.innerHTML += msg + '<br>';
touchLog.scrollTop = touchLog.scrollHeight;
}

touchArea.addEventListener('touchstart', e => {
console.log('터치 시작');
console.log('터치 개수:', e.touches.length);
addTouchLog(`터치 시작 (${e.touches.length}개)`);
});

touchArea.addEventListener('touchmove', e => {
e.preventDefault(); // 스크롤 방지
const touch = e.touches[0];
console.log(`터치 위치: ${touch.clientX}, ${touch.clientY}`);
if (Math.random() < 0.3) {
addTouchLog(`터치 이동: ${touch.clientX}, ${touch.clientY}`);
}
});

touchArea.addEventListener('touchend', () => {
console.log('터치 종료');
addTouchLog('터치 종료');
});

touchArea.addEventListener('touchcancel', () => {
console.log('터치 취소됨');
addTouchLog('터치 취소됨');
});

// 마우스 이벤트로도 테스트 가능하도록
touchArea.addEventListener('mousedown', () => addTouchLog('마우스 다운 (터치 대신)'));
touchArea.addEventListener('mouseup', () => addTouchLog('마우스 업 (터치 대신)'));
</script>

9.6 드래그 앤 드롭 이벤트

<h3>드래그 앤 드롭 테스트</h3>
<div
id="draggable"
draggable="true"
style="width: 100px; height: 50px; background: #007bff; color: white; text-align: center; line-height: 50px; margin: 5px; cursor: move;"
>
드래그
</div>
<div
id="dropZone"
style="width: 200px; height: 100px; border: 2px dashed #28a745; padding: 20px; text-align: center; line-height: 60px; margin: 10px 0;"
>
드롭 존
</div>
<div
id="dragLog"
style="border: 1px solid #ccc; padding: 10px; height: 100px; overflow-y: auto; font-family: monospace;"
></div>

<script>
const draggable = document.querySelector('#draggable');
const dropZone = document.querySelector('#dropZone');
const dragLog = document.querySelector('#dragLog');

function addDragLog(msg) {
dragLog.innerHTML += msg + '<br>';
dragLog.scrollTop = dragLog.scrollHeight;
}

// 드래그 시작
draggable.addEventListener('dragstart', e => {
e.dataTransfer.setData('text/plain', e.target.id);
console.log('드래그 시작');
addDragLog('드래그 시작');
});

// 드래그 종료
draggable.addEventListener('dragend', () => {
console.log('드래그 종료');
addDragLog('드래그 종료');
});

// 드롭 존에 진입
dropZone.addEventListener('dragenter', e => {
e.preventDefault();
console.log('드롭 존 진입');
addDragLog('드롭 존 진입');
dropZone.style.backgroundColor = '#d4edda';
});

// 드롭 존 위에서 드래그
dropZone.addEventListener('dragover', e => {
e.preventDefault();
});

// 드롭 존에서 나감
dropZone.addEventListener('dragleave', () => {
console.log('드롭 존 나감');
addDragLog('드롭 존 나감');
dropZone.style.backgroundColor = '';
});

// 드롭
dropZone.addEventListener('drop', e => {
e.preventDefault();
const data = e.dataTransfer.getData('text/plain');
console.log('드롭됨:', data);
addDragLog('드롭 완료!');
dropZone.style.backgroundColor = '#c3e6cb';
setTimeout(() => {
dropZone.style.backgroundColor = '';
}, 1000);
});
</script>

9.7 미디어 이벤트

<h3>미디어 이벤트 테스트</h3>
<audio id="myAudio" controls style="margin: 10px 0; width: 300px;">
<source
src="data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsFJHfH8N2QQAoUXrTp66hVFApGn+DyvmYhBTaN1O/RgSsF"
type="audio/wav"
/>
브라우저가 오디오를 지원하지 않습니다.
</audio>
<div
id="mediaLog"
style="border: 1px solid #ccc; padding: 10px; height: 100px; overflow-y: auto; font-family: monospace;"
></div>

<script>
const audio = document.querySelector('#myAudio');
const mediaLog = document.querySelector('#mediaLog');

function addMediaLog(msg) {
mediaLog.innerHTML += msg + '<br>';
mediaLog.scrollTop = mediaLog.scrollHeight;
}

audio.addEventListener('play', () => {
console.log('오디오 재생 시작');
addMediaLog('재생 시작');
});

audio.addEventListener('pause', () => {
console.log('오디오 일시정지');
addMediaLog('일시정지');
});

audio.addEventListener('ended', () => {
console.log('오디오 재생 완료');
addMediaLog('재생 완료');
});

audio.addEventListener('timeupdate', () => {
if (Math.random() < 0.1) {
console.log(`재생 시간: ${audio.currentTime}`);
addMediaLog(`시간: ${audio.currentTime.toFixed(1)}s`);
}
});

audio.addEventListener('volumechange', () => {
console.log(`볼륨: ${audio.volume}`);
addMediaLog(`볼륨: ${audio.volume}`);
});

audio.addEventListener('loadstart', () => {
console.log('로딩 시작');
addMediaLog('로딩 시작');
});

audio.addEventListener('canplay', () => {
console.log('재생 가능');
addMediaLog('재생 가능');
});
</script>

9.8 네트워크 이벤트

<h3>네트워크 이벤트 테스트</h3>
<p>온라인 상태: <span id="onlineStatus">확인 중...</span></p>
<button onclick="alert('다른 탭으로 이동했다가 돌아와보세요!')" style="padding: 5px;">
가시성 테스트 도움말
</button>
<div
id="networkLog"
style="border: 1px solid #ccc; padding: 10px; height: 100px; overflow-y: auto; font-family: monospace;"
></div>

<script>
const networkLog = document.querySelector('#networkLog');
const onlineStatus = document.querySelector('#onlineStatus');

function addNetworkLog(msg) {
networkLog.innerHTML += msg + '<br>';
networkLog.scrollTop = networkLog.scrollHeight;
}

// 초기 온라인 상태 설정
onlineStatus.textContent = navigator.onLine ? '온라인' : '오프라인';
onlineStatus.style.color = navigator.onLine ? 'green' : 'red';

// 온라인/오프라인 상태 감지
window.addEventListener('online', () => {
console.log('온라인 상태');
addNetworkLog('온라인 상태');
onlineStatus.textContent = '온라인';
onlineStatus.style.color = 'green';
});

window.addEventListener('offline', () => {
console.log('오프라인 상태');
addNetworkLog('오프라인 상태');
onlineStatus.textContent = '오프라인';
onlineStatus.style.color = 'red';
});

// 페이지 가시성 변경
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('페이지가 숨겨짐');
addNetworkLog('페이지 숨겨짐');
} else {
console.log('페이지가 다시 보임');
addNetworkLog('페이지 다시 보임');
}
});

addNetworkLog('네트워크 이벤트 감지 시작');
</script>

9.9 클립보드 이벤트

<h3>클립보드 이벤트 테스트</h3>
<input
id="clipboardInput"
placeholder="텍스트 복사/붙여넣기 테스트"
style="padding: 8px; width: 300px; margin: 5px;"
value="이 텍스트를 복사해보세요!"
/>
<div
id="clipboardLog"
style="border: 1px solid #ccc; padding: 10px; height: 100px; overflow-y: auto; font-family: monospace;"
></div>

<script>
const clipboardInput = document.querySelector('#clipboardInput');
const clipboardLog = document.querySelector('#clipboardLog');

function addClipboardLog(msg) {
clipboardLog.innerHTML += msg + '<br>';
clipboardLog.scrollTop = clipboardLog.scrollHeight;
}

clipboardInput.addEventListener('copy', e => {
console.log('복사됨:', e.clipboardData);
addClipboardLog('복사됨');
});

clipboardInput.addEventListener('cut', e => {
console.log('잘라내기됨:', e.clipboardData);
addClipboardLog('잘라내기됨');
});

clipboardInput.addEventListener('paste', e => {
const pastedText = e.clipboardData.getData('text');
console.log('붙여넣기됨:', pastedText);
addClipboardLog(`붙여넣기: ${pastedText}`);
});

addClipboardLog('클립보드 이벤트 감지 시작');
</script>

9.10 이벤트 버블링과 캡처링

<h3>이벤트 버블링과 캡처링 테스트</h3>
<div id="parent" style="padding: 20px; border: 2px solid #007bff; background: #e7f3ff;">
부모 요소 (파란색)
<div
id="child"
style="padding: 20px; border: 2px solid #28a745; background: #d4edda; margin: 10px;"
>
자식 요소 (녹색) - 클릭해보세요!
</div>
</div>
<button id="togglePropagation" style="margin: 5px; padding: 5px;">버블링 중지 토글</button>
<div
id="bubbleLog"
style="border: 1px solid #ccc; padding: 10px; height: 100px; overflow-y: auto; font-family: monospace;"
></div>

<script>
const parent = document.querySelector('#parent');
const child = document.querySelector('#child');
const toggleBtn = document.querySelector('#togglePropagation');
const bubbleLog = document.querySelector('#bubbleLog');
let stopPropagation = false;

function addBubbleLog(msg) {
bubbleLog.innerHTML += msg + '<br>';
bubbleLog.scrollTop = bubbleLog.scrollHeight;
}

// 캡처링 단계
parent.addEventListener(
'click',
() => {
console.log('부모 캡처링');
addBubbleLog('부모 캡처링');
},
true
);

child.addEventListener(
'click',
() => {
console.log('자식 캡처링');
addBubbleLog('자식 캡처링');
},
true
);

// 버블링 단계
child.addEventListener(
'click',
e => {
console.log('자식 클릭');
addBubbleLog('자식 클릭');
if (stopPropagation) {
e.stopPropagation();
addBubbleLog('버블링 중지됨');
}
},
false
);

parent.addEventListener(
'click',
() => {
console.log('부모 클릭');
addBubbleLog('부모 버블링');
},
false
);

toggleBtn.addEventListener('click', () => {
stopPropagation = !stopPropagation;
toggleBtn.textContent = stopPropagation ? '버블링 중지 해제' : '버블링 중지 토글';
addBubbleLog(`버블링 중지: ${stopPropagation ? 'ON' : 'OFF'}`);
});

addBubbleLog('이벤트 전파 테스트 시작');
</script>

9.11 이벤트 객체 사용

<h3>이벤트 객체 테스트</h3>
<a
id="myLink"
href="https://example.com"
style="display: inline-block; padding: 10px; background: #007bff; color: white; text-decoration: none; margin: 5px;"
>링크 (기본 동작 방지됨)</a
>
<button id="eventBtn" style="padding: 10px; margin: 5px;">이벤트 정보 버튼</button>
<div
id="eventLog"
style="border: 1px solid #ccc; padding: 10px; height: 100px; overflow-y: auto; font-family: monospace;"
></div>

<script>
const link = document.querySelector('#myLink');
const eventBtn = document.querySelector('#eventBtn');
const eventLog = document.querySelector('#eventLog');

function addEventLog(msg) {
eventLog.innerHTML += msg + '<br>';
eventLog.scrollTop = eventLog.scrollHeight;
}

link.addEventListener('click', event => {
event.preventDefault(); // 기본 동작 막기
console.log('링크 클릭 방지됨');
addEventLog('링크 클릭 방지됨');
addEventLog(`이벤트 타입: ${event.type}`);
addEventLog(`타겟: ${event.target.tagName}`);
addEventLog(`현재 타겟: ${event.currentTarget.tagName}`);
});

eventBtn.addEventListener('click', event => {
console.log('이벤트 정보:', {
type: event.type,
target: event.target,
currentTarget: event.currentTarget,
clientX: event.clientX,
clientY: event.clientY,
});
addEventLog(`클릭 위치: (${event.clientX}, ${event.clientY})`);
addEventLog(`타임스탬프: ${event.timeStamp.toFixed(0)}`);
addEventLog(`버튼: ${event.button}`);
});

addEventLog('이벤트 객체 정보 테스트 시작');
</script>

9.12 커스텀 이벤트

<h3>커스텀 이벤트 테스트</h3>
<button id="triggerCustom" style="padding: 10px; margin: 5px;">커스텀 이벤트 발생</button>
<button id="triggerLogin" style="padding: 10px; margin: 5px;">로그인 이벤트 발생</button>
<div
id="customLog"
style="border: 1px solid #ccc; padding: 10px; height: 100px; overflow-y: auto; font-family: monospace;"
></div>

<script>
const triggerCustom = document.querySelector('#triggerCustom');
const triggerLogin = document.querySelector('#triggerLogin');
const customLog = document.querySelector('#customLog');

function addCustomLog(msg) {
customLog.innerHTML += msg + '<br>';
customLog.scrollTop = customLog.scrollHeight;
}

// 커스텀 이벤트 리스너 등록
document.addEventListener('myCustomEvent', e => {
console.log('커스텀 이벤트 발생:', e.detail.message);
addCustomLog(`커스텀 이벤트: ${e.detail.message}`);
});

document.addEventListener('userLogin', e => {
console.log(`사용자 ${e.detail.username}이 로그인했습니다.`);
addCustomLog(`${e.detail.username} 로그인`);
addCustomLog(`시간: ${new Date(e.detail.timestamp).toLocaleTimeString()}`);
});

// 커스텀 이벤트 발생 버튼
triggerCustom.addEventListener('click', () => {
const customEvent = new CustomEvent('myCustomEvent', {
detail: { message: '안녕하세요! 커스텀 이벤트입니다.' },
});
document.dispatchEvent(customEvent);
});

triggerLogin.addEventListener('click', () => {
const userLoginEvent = new CustomEvent('userLogin', {
detail: {
username: 'john_doe',
timestamp: new Date(),
userAgent: navigator.userAgent,
},
bubbles: true,
cancelable: true,
});
document.dispatchEvent(userLoginEvent);
});

addCustomLog('커스텀 이벤트 리스너 등록 완료');
</script>

9.13 이벤트 위임 (Event Delegation)

<h3>이벤트 위임 테스트</h3>
<ul id="myList" style="border: 1px solid #ccc; padding: 10px; min-height: 100px;">
<li style="padding: 5px; margin: 2px; background: #f8f9fa; cursor: pointer;">기존 항목 1</li>
<li style="padding: 5px; margin: 2px; background: #f8f9fa; cursor: pointer;">기존 항목 2</li>
<li style="padding: 5px; margin: 2px; background: #f8f9fa; cursor: pointer;">기존 항목 3</li>
</ul>
<button id="addItem" style="padding: 5px; margin: 5px;">새 항목 추가</button>
<div
id="delegationLog"
style="border: 1px solid #ccc; padding: 10px; height: 100px; overflow-y: auto; font-family: monospace;"
></div>

<script>
const list = document.querySelector('#myList');
const addItem = document.querySelector('#addItem');
const delegationLog = document.querySelector('#delegationLog');
let itemCount = 3;

function addDelegationLog(msg) {
delegationLog.innerHTML += msg + '<br>';
delegationLog.scrollTop = delegationLog.scrollHeight;
}

// 이벤트 위임: 부모 요소에 리스너 등록
list.addEventListener('click', e => {
if (e.target.tagName === 'LI') {
console.log('리스트 아이템 클릭:', e.target.textContent);
addDelegationLog(`클릭: ${e.target.textContent}`);
e.target.style.backgroundColor = '#d4edda';
setTimeout(() => {
e.target.style.backgroundColor = '#f8f9fa';
}, 500);
}
});

// 동적으로 아이템 추가
addItem.addEventListener('click', () => {
itemCount++;
const newItem = document.createElement('li');
newItem.textContent = `새 항목 ${itemCount}`;
newItem.style.cssText = 'padding: 5px; margin: 2px; background: #f8f9fa; cursor: pointer;';
list.appendChild(newItem);
addDelegationLog(`새 항목 추가: 새 항목 ${itemCount}`);
});

addDelegationLog('이벤트 위임 테스트 시작');
addDelegationLog('리스트 항목을 클릭해보세요!');
</script>

9.14 이벤트 리스너 제거

<h3>이벤트 리스너 제거 테스트</h3>
<button id="myButton" style="padding: 10px; margin: 5px;">이벤트 리스너 테스트</button>
<button id="removeListener" style="padding: 10px; margin: 5px;">리스너 제거</button>
<button id="addListener" style="padding: 10px; margin: 5px;">리스너 추가</button>
<button id="onceButton" style="padding: 10px; margin: 5px;">한 번만 실행 (once)</button>
<div
id="listenerLog"
style="border: 1px solid #ccc; padding: 10px; height: 100px; overflow-y: auto; font-family: monospace;"
></div>

<script>
const button = document.querySelector('#myButton');
const removeListener = document.querySelector('#removeListener');
const addListener = document.querySelector('#addListener');
const onceButton = document.querySelector('#onceButton');
const listenerLog = document.querySelector('#listenerLog');

function addListenerLog(msg) {
listenerLog.innerHTML += msg + '<br>';
listenerLog.scrollTop = listenerLog.scrollHeight;
}

function handleClick() {
console.log('클릭 처리됨');
addListenerLog('클릭 처리됨');
}

// 초기 이벤트 리스너 추가
button.addEventListener('click', handleClick);

removeListener.addEventListener('click', () => {
button.removeEventListener('click', handleClick);
addListenerLog('이벤트 리스너 제거됨');
});

addListener.addEventListener('click', () => {
button.addEventListener('click', handleClick);
addListenerLog('이벤트 리스너 추가됨');
});

// once 옵션 사용
onceButton.addEventListener(
'click',
() => {
console.log('한 번만 실행됨 (once 옵션)');
addListenerLog('한 번만 실행됨 (once 옵션)');
},
{ once: true }
);

addListenerLog('이벤트 리스너 관리 테스트 시작');
addListenerLog('버튼들을 클릭해서 테스트해보세요!');
</script>