티스토리 뷰

프론트엔드/DOM

[DOM] ⑤ DOM 이벤트

doeunnkimm 2022. 7. 29. 00:08

▶DOM 이벤트

이벤트(Event)는 어떠한 일(또는 인터랙션)이 발생했을 때, 그 시점에 발생하는 신호를 의미합니다.

DOM에서는 요소를 클릭하거나, 특정 키를 입력했을 때처럼 다양한 상황에서 이벤트가 발생합니다.

 

프런트엔드 개발자에게 이벤트를 제어하는 것은 굉장히 중요한 부분입니다.

이벤트의 발생 시점에 맞춰 여러 가지 동작을 추가하여 사용하자와 인터랙션할 수 있기 때문입니다.

 

1) Event 객체

Event 객체는 DOM 내에서 발생한 이벤트에 대한 정보를 담고 있습니다.

Event 객체는 발생한 이벤트의 종류부터 요소에 대한 정보, 갭처링 여부, 이벤트 발생 위치 등 여러 정보를 갖고 있습니다.

▷target과 currentTarget

이벤트가 발생한 요소에 접근하고 싶은 경우, target 혹은 currentTarget 프로퍼티를 통해 접근할 수 있습니다.

두 프로퍼티는 같은 값을 갖기도 하고 다른 값을 갖기도 해 많은 사람이 헷갈려하는 프로퍼티 중 하나입니다.

각각 어떤 값을 갖는지 살펴보겠습니다.

  • target : 이벤트가 처음 발생했던 대상 DOM 요소의 참조를 갖습니다.
  • currentTarget : 발생한 이벤트가 등록된 DOM 요소의 참조를 갖습니다.
<ul id="list">
    <li id="1">
        list1
    </li>
    <li id="2">
        <!-- 클릭! -->
        list2
    </li>
    <li id="3">
        list3
    </li>
</ul>

<script>
    const list = document.getElementById("list");

    list.addEventListener('click', (ev) => {
        console.log(ev.target.id); // 2
        console.log(ev.currentTarget.id); // list
    });
</script>

이 예제 코드에서는 id가 list인 ul 요소에 이벤트를 추가했고 ul 요소 내부에 id가 2인 li 요소를 클릭했습니다.

이 경우 이벤트가 처음 발생한 요소는 li 요소이기 때문에 이 요소가 target이 되고, 이벤트가 등록된 ul 요소는 currentTarget이 됩니다.

addEventListener 부분을 다시 보면, 'click' 이벤트가 발생하면 ev라는 함수를 실행하는 것입니다. (함수 이름은 임의로)

이 예제 코드에서는 함수를 화살표 함수를 이용해 간단하게 작성했습니다.

즉 function ev() = {...} 으로 작성해서 ev를 인자로 넘겨주어도 무관하다는 의미가 되겠습니다.

 

▷stopPropagation()과 preventDefault()

Event 객체에는 여러 값뿐만 아니라 Event를 제어할 수 있는 메서드도 존재합니다.

대표적으로 stopPropagation()과 preventDefault()가 있습니다.

 

preventDefault()는 이벤트를 취소할 수 있는 경우 이벤트를 취소합니다.

대신 전파(캡처링, 버블링)되는 이벤트를 막지 않으며, 현재 이벤트의 기본 동작만 중단합니다.

stopPropagation()은 preventDefault()처럼 기본 동작을 중단하지는 못하지만 이벤트가 전파되는 것을 막습니다.

 

다음 예제 코드를 통해 각 이벤트가 어떤 순서로 발생하는지 살펴보겠습니다.

<div id="div-id">
    div 영역
    <p id="p-id">
        p 영역
        <a href="https://www.google.com/" id="a-id">google로 이동!</a>
    </p>
</div>

<script>
    const div = document.getElementById('div-id');
    const p = documnet.getElementById('p-id');
    const a = document.getElementById('a-id');

    div.addEventListener('click', () => {
        console.log('div 클릭!');
    });

    p.addEventListener('click', () => {
        consol.log('p 클릭!');
    });
    
    a.addEventListener('click', () => {
        console.log('a 클릭!')
    });
</script>

p 영역을 클릭할 경우 이벤트 버블링으로 인해 "p 클릭!", "div 클릭!"이 순서대로 발생합니다.

 

만약 p 영역에서 클릭 이벤트가 발생한 뒤 상위 요소로 이벤트가 전파되지 않도록 막고 싶다면, stopPropagation()을 호출하여 막을 수 있습니다.

 

아래 예제 코드의 p 영역을 클릭하면 "p 클릭!" 로그만 나타납니다.

하지만 stopPropagation()은 이벤트의 전파만 중단시킬 뿐 이벤트의 기본 동작을 막을 수 없습니다.

p.addEventListener('click', (ev) => {
    consol.log('p 클릭!');

    ev.stopPropagation();
});

a.addEventListener('click', () => {
    console.log('a 클릭!')
});

아래 예제 코드에서 a 요소('google로 이동')를 클릭했을 경우 "a 클릭!" 로그가 출력되며, 구글 페이지로 이동하게 됩니다.

이는 a 요소의 기본 동작이 링크를 통해 페이지를 이동하는 것이기 때문입니다.

그렇다면 이벤트 발생 시 이러한 기본 동작을 어떻게 막을 수 있을까요?

 

이러한 기본 동작을 중단시키고 싶을 경우 preventDefault()를 사용합니다.

a.addEventListener('click', (ev) => {
    console.log('a 클릭!')

    ev.proventDefault();
});

preventDefault()를 호출하면 a 요소의 기본 동작(링크 이동)을 막기 때문에 페이지를 이동하지 않습니다.

반면 a, p, div 순서대로 이벤트가 전파되어 클릭 이벤트 리스너 함수가 실행됩니다.

 

3) 이벤트 종류

▷User Interface 이벤트

User Interface(UI) 이벤트는 문서나 요소의 조작 시 발생하는 이벤트입니다.

이름 설명
load 문서나 종속된 리소스(이미지나 스크립트 파일, CSS 파일 등)가 완전히 로드되었을 때, 이 이벤트가 발생합니다. 일반적으로 window에 접근할 수 없어 주로 <body>에 이벤트 리스너에 할당해 사용합니다.
unload 문서나 종속된 리소스(이미지나 스크립트 파일, CSS 파일 등)가 완전히 제거되었을 때, 이 이벤트가 발생합니다. 주로 페이지를 완전히 종료하거나 다른 페이지로 이동 시 발생합니다. 메모리 누수를 방지하는 목적으로 주로 사용합니다.
.abort 리소스가 중단된 경우(로드가 진행되는 동안 사용자가 취소했을 때 같은 경우) 이 이벤트가 발생합니다.
error 리소스가 로드되었지만 유효하지 않을 때, 스크립트 실행 오류, 잘못된 형식 등의 에러가 발생했을 때 이 이벤트가 발생합니다.
select <input>, <textarea> 요소 안에 작성된 텍스트를 선택할 경우 발생합니다.

 

▷Focus 이벤트

Focus 이벤트는 대상이 포커스를 받거나 잃을 때 발생하는 이벤트입니다.

이름 설명
focusin 포커스를 받으려 할 때 이벤트가 발생합니다. 포커스가 실제로 가기 전 이벤트가 발생합니다. target은 이벤트를 받을 요소입니다.
focusout 포커스를 잃으려 할 때 이벤트가 발생합니다. 포커스를 실제로 잃기 전 이벤트가 발생합니다. target은 이벤트를 잃게 될 요소입니다.
focus 대상이 포커스를 받을 때 이벤트가 발생합니다. focusin과 비슷하지만 포커스를 받은 뒤 이벤트가 발생해 시점의 차이가 있습니다.
blur 대상이 포커스를 잃을 때 발생합니다. focusout과 비슷하지만 포커스를 잃은 뒤 이벤트가 발생해 시점의 차이가 있습니다.

만약 A 요소로 포커스를 잡은 뒤 B 요소로 포커스가 이동할 경우 이벤트의 순서는 다음과 같이 전파됩니다.

Focus 이벤트 전파

 

▷마우스 이벤트

마우스 이벤트는 마우스 또는 트랙패드, 트랙볼 같은 포인팅 입력 장치를 탐지합니다.

마우스 이벤트는 항상 가장 깊이 중첩된 요소를 대상으로 합니다.

이름 설명
mousedown 타깃을 눌렀을 때 이벤트가 발생합니다. mousedown 이벤트의 기본 동작을 취소할 경우(Event 객체 장에서 설명) click이나 dbclick 같은 이후 이벤트를 방지할 수 있습니다.
mouseup 타깃을 위에서 눌렀던 포인트가 해제될 때 이 이벤트가 발생합니다. mousedown과 마찬가지로 해당 이벤트의 기본 동작을 취소할 경우 이후 이벤트를 방지할 수 있습니다.
click 타깃에 클릭 동작을 했을 경우 이벤트가 발생합니다. mousedown과 mouseup 이벤트가 발생한 뒤 발생합니다.
dbclick 더블 클릭을 했을 경우 이벤트가 발생합니다. click 이벤트의 동작을 취소해도 dbclick 이벤트는 동일하게 발생합니다.
mousemove 타깃 내에서 포인터가 이동할 경우 이벤트가 발생합니다. 지속적으로 마우스 포인터를 추적할 필요가 있을 때 주로 사용합니다.
mouseenter 타깃 밖에서 타깃으로 처음 포인터가 이동할 때 발생합니다. 포인터가 지삭 요소에 올라갈 때는 발생하지 않습니다. 요소에 처음 진입했을 때를 탐지할 때 주로 사용합니다.
mouseleave 타깃 밖으로 이동할 때 발생합니다.
mouseover mouseenter와 동일한 조건에서 발생합니다. mouseenter와 다른 점은 이벤트가 버블링이 발생합니다.

마우스 이벤트의 경우 Event 객체에 이벤트가 발생한 시기의 마우스 좌표를 제공합니다.

대표적으로 client*, offset*, page* 등이 그에 해당하는 좌표입니다.

이 값들은 스크롤되어 있는 화면을 포함하는 영역을 기준으로 하는지, 현재 보이는 페이지만 기준으로 하는지, 모니터 영역을 기준으로 하는지 등 각각의 기준으로 값을 계산해 반환합니다.

<div 
    id="wrapper-id"
    style="height: 100px;
        width: 100px;
        border: 1px solid #ccc;">
    
    mouse move 탑지 영역
</div>

<script>
    const wrapper = document.getElementById('wrapper-id');

    // 영역 내에 이벤트가 발생한 지점의 좌표를 반환
    wrapper.addEventListener('mousemove', (ev) => {
        console.log(ev.offsetX, ev.offsetY);
        console.log(ev.clienX, ev.clientY);
    });
</script>

 

▷Input 이벤트

Input 이벤트는 편집 가능한 영역에서의 키보드 입력이나 삭제, 서식 지정 등을 통해 DOM 요소가 업데이트 될 때 발생하는 이벤트입니다.

이름 설명
beforeInput 입력 후 DOM 요소에 업데이트되기 전 이벤트가 발생합니다.
input 입력 후 DOM 요소에 업데이트된 뒤 바로 이벤트가 발생합니다.

 

▷키보드 이벤트

키보드 이벤트는 키보드를 누르는 시점과 떼는 시점에 따라 이벤트가 발생합니다.

이름 설명
keydown 키보드를 누를 때 발생합니다.
keyup 누른 키를 땔 때 발생합니다.

키보드 이벤트의 경우 Event 객체에서 어떤 키를 입력했는지, 함께 누른 키는 어떤 키인지에 대한 정보를 함께 제공합니다.

대표적으로 key, ctrlKey, altKey, shiftKey, metaKey 등이 존재합니다.

key는 입력한 키를 문자열로 반환하며 ctrlKey, altKey, shiftKey, metaKey 값은 해당 버튼을 입력했는지 불리언 값으로 반환합니다.

<input type="text" id="input-id">

<script>
    const input = document.getElementById('input-id');

    input.addEventListener('keydown', (ev) => {
        if (ev.key.toUpperCase() == "S" && ev.ctrlKey) {
            console.log("save!");
        }
    });
</script>

위의 예제 코드처럼 keydown 이벤트 리스너에게 특정 단축키를 조합하여 입력했을 때 원하는 동작을 정의할 수 있습니다.

 

지금까지 프론트엔드 개발 시 굉장히 자주 사용하느 UI 이벤트, 마우스, 키 이벤트에 대해 살펴보았습니다.

위에서 소개한 이벤트 외에 마우스 휠 이벤트나 IME를 처리하기 위한 composition 이벤트, 터치 스크린 모바일이나 패드의 촉을 감지하는 터치 이벤트, 제스쳐 이벤트 등 다양한 이벤트가 있습니다.

W3C의 events 명세나 MDN 문서에는 여러 이벤트에 대한 특징이 잘 정리되어 있으니, 추가적으로 살펴보는 것을 권장합니다.

 

3) 이벤트 리스너 추가하기

이벤트 리스너를 추가하는 방법은 크게 세 가지가 있습니다.

DOM 프로퍼티로 할당하는 방법, HTML 요소에 속성으로 할당하는 방법, 마지막으로 권장되는 방식인 addEventListener()가 있습니다.

▷HTML요소 속성으로 할당하기

on<evnet> 형태로 되어 있는 속성에 리스너를 할당할 수 있습니다.

속성에 들어가는 코드는 자바스크립트 코드입니다.

<button onClick="alert('Hello world!')">alert 띄우기</button>

별도로 작성한 자바스크립트 이벤트 리스너를 등록하고 싶을 경우 다음 예제 코드처럼 on<event> 속성에 삽입해야 합니다.

또한 HTML 속성을 할당할 때 event를 인자로 넘겨줄 경우 이벤트 정보를 가진 Event 객체를 사용할 수 있습니다.

<button id="button-id" onClick="clickEventHandler(event)">클릭!</button>

<script>
    function clickEventHandler(ev) {
        console.log(ev); // Evnet
    }
</script>

 

▷DOM 프로퍼티로 할당하기(DOM level 0)

DOM 프로퍼티를 할당하여 이벤트 리스너를 등록할 수 있습니다.

DOM 요소의 on<event>형태의 프로퍼티에 함수를 할당하면 이벤트 리스너로 등록됩니다.

<button id="button-id">alert 띄우기</button>

<script>
    document.getElementById('button-id').onclick = (event) => {
        alert("hello world!");
    }
</script>

이때 onclick에 할당되는 리스너 함수는 이벤트 정보를 담은 Event 객체를 매개변수로 갖습니다.

 

하지만 HTML 요소의 속성으로 이벤트 리스너를 등록하거나, DOM 프로퍼티로 이벤트 리스너를 등록하는 방법은 동일한 이벤트를 대상으로 여러 이벤트 리스너를 동시에 등록할 수 없다는 단점이 있습니다.

이벤트 리스너를 등록한 뒤 새로운 이벤트 리스너를 할당할 경우 기존 값은 덮어씌워지기 때문에 마지막에 할당된 이벤트 리스너만 실행됩니다.

 

▷addEventListener 사용하기(DOM level 2)

타깃이 되는 요소에 addEventListener()를 사용해 이벤트 리스너를 등록할 수 있습니다.

addEventListener()는 앞서 소개한 두 방식과 다르게 한 이벤트 타입에 여러 개의 리스너를 등록할 수 있으며 버블링을 사용할지, 캡쳐링을 사용할지 등 정밀한 제어를 할 수 있어 가장 권장되는 방식입니다.

 

이때 이벤트 리스너에는 발생한 이벤트의 정보가 담긴 Event 객체가 매개변수로 포함됩니다.

<button id="button-id">alert 띄우기</button>

<script>
    // 'click'으로 타입 명시
    document.getElementById('button-id').addEventListener('click', (ev) => {
        alert("Hello world!");
    }, true); // 캡처링(capturing) 사용
</script>

할당한 이벤트를 해제하려면 removeEventListener() 메서드를 사용합니다.

해제하고 싶은 이벤트 리스너의 참조를 인자로 넘겨주면 이벤트가 해제됩니다.

이때 가장 많이 하는 실수로, 등록한 이벤트 핸들러와 동일한 형태의 함수를 선언하여 넘겨주는 경우가 있는데 그런 경우 이벤트 리스너는 해제되지 않습니다.

반드시 동일한 참조를 가진 리스너를 넘겨주어야 올바르게 해제할 수 있습니다.

function sayHello() {
alert('Hello');
}

documnet.addEventListener('click', sayHello); // 이벤트 핸들러 등록
document.removeEventListener('click', sayHello); // 이벤트 핸들러 제거

 

4) 버블링과 캡처링

계층적 구조를 가진 DOM에 이벤트가 발생할 경우 연쇄적으로 이벤트가 전파(Propagation)됩니다.

그리고 이벤트가 전파되는 방향에 따라 버블링(Bubbling)과 캡처링(Capturing)으로 구분됩니다.

▷버블링(Bubbling)

DOM 요소에 이벤트가 발생할 경우 부모 요소로 올라가며 차례대로 이벤트가 전파되는 흐름을 버블링이라고 합니다.

대부분의 이벤트는 버블링을 기본 동작으로 갖습니다.

<form id="form-id">
    form 영역
    <div id="div-id">
        div 영역
        <p id="p-id">
            p 영역
        </p>
    </div>
</form>

<script>
    const form = document.getElementById('form-id');
    const div = document.getElementById('div-id');
    const p = document.getElementById('p-id');

    [form, div, p].forEach(target => {
        target.addEventListener('click', () => {
            console.log(`${String(target.tagName)} 클릭!`); // 클릭한 DOM 요소의 태그 이름 출력
        });
    });
</script>

이 예제 코드의 실행 결과는 다음과 같습니다.

예제 실행 결과

p 영역을 클릭할 경우 등록된 이벤트 리스너가 실행되며, 부모 요소에 등록된 클릭 이벤트 리스너 또한 차례대로 실행됩니다.

따라서 "P 클릭!", "DIV 클릭!", "FORM 클릭!" 이 순서대로 출력됩니다.

버블링 전파 과정

 

▷캡처링(Capturing)

버블링과 반대로 DOM 요소에 이벤트가 발생할 경우 가장 상위의 부모 요소부터 자식 요소로 내려가며 이벤트가 전파되는 것을 캡처링이라고 합니다.

addEventListener()의 세 번째 옵션을 통해 이벤트 리스너를 캡처링 단계에서 실행시킬 수 있습니다.

[form, div, p].forEach(target => {
    target.addEventListener('click', () => {
        console.log(`${String(target.tagName)} 클릭!`); // 클릭한 DOM 요소의 태그 이름 출력
    }, true); // 캡처링 이벤트 핸들러
});

캡처링 이벤트 리스너를 등록한 후 동일하게 p 영역을 클릭하면 버블링과 반대로 상위 요소인 <form>부터 <div>, <p>의 순서로 이벤트가 발생합니다.

캡처링 전파 과정

표준 DOM의 이벤트는 캡처링, 타깃, 버블링의 흐름 순서를 갖습니다.

만약 특정 DOM 요소에 등록된 이벤트 리스너가 버블링과 캡처링을 모두 사용한다면 DOM 이벤트 흐름 순서를 명확히 이해해야 혼란 없이 사용할 수 있습니다.

[form, div, p].forEach(target => {
    target.addEventListener('click', () => {
        console.log(`${String(target.tagName)} 클릭!`); // 클릭한 DOM 요소의 태그 이름 출력
    }, true); // 캡처링 이벤트 핸들러
});

[form, div, p].forEach(target => {
    target.addEventListener('click', () => {
        console.log(`${String(target.tagName)} 클릭!`); // 클릭한 DOM 요소의 태그 이름 출력
    }); // 버블링 이벤트 핸들러
});

이 예제 코드에서는 버블링 이벤트와 캡처링 이벤트 리스너가 모두 등록되어 있습니다.

p 요소를 클릭했을 경우 DOM 이벤트 발생 흐름에 따라 다음처럼 전파됩니다.

이벤트 전파 과정

 

 


여기까지 DOM 이벤트에 대해 알아보았습니다 :)

728x90
LIST

'프론트엔드 > DOM' 카테고리의 다른 글

[DOM] ④ DOM 요소 검색하기  (0) 2022.07.27
[DOM] ③ DOM Node 추가·제거  (0) 2022.07.27
[DOM] ② DOM Node  (0) 2022.07.26
[DOM] ① DOM 트리  (0) 2022.07.25
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함