[day-29] DOM, Event

Joohyung Park·2024년 2월 7일
0

[모두연] 오름캠프

목록 보기
43/95

이벤트 객체

이벤트에서 호출되는 핸들러에는 이벤트와 관련된 모든 정보를 가지고 있는 매개변수가 전송된다. 이벤트 핸들러란 이벤트가 발생(마우스 입력, 키보드 입력 등)하면 실행되는 코드블럭을 의미한다.

<article class="parent">
    <ol>
        <li><button class="btn-first" type="button">버튼1</button></li>
        <li><button type="button">버튼2</button></li>
        <li><button type="button">버튼3</button></li>
    </ol>
</article>
const btnFirst = document.querySelector('.btn-first');
btnFirst.addEventListener('click', (event) => {
    console.log(event);
});


첫번째 버튼이 눌릴 때마다 이벤트를 출력하는 코드이다. 첫번째 버튼의 클래스를 btnFirst객체에 저장하여 클릭 이벤트시에 이벤트 핸들러가 작동한다.

이벤트 흐름

브라우저는 화면에서 이벤트가 발생하면 가장 먼저 이벤트 대상을 찾기 시작한다.

가장 상위의 객체부터 순서대로 DOM 트리를 따라 내려가는 것을 캡처링 단계라고 한다.

이때 중간에 만나는 모든 캡처링 이벤트 리스너를 실행시키고 캡처링이 끝나면 다시 DOM 트리를 따라 올라가며 만나는 모든 버블링 이벤트 리스너를 실행한다. 이를 버블링 단계라고 한다.

이러한 과정에서 이벤트 리스너가 차례대로 실행되는 것을 이벤트 전파라고 한다.

<!DOCTYPE html>
<html lang="ko">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="../reset.css">
    <style>
    </style>
</head>

<body>
    <article class="parent">
        <button class="btn" type="button">버튼</button>
    </article>

    <script>
        const parent = document.querySelector('.parent');
        const btnFirst = document.querySelector('.btn');
        btnFirst.addEventListener('click', (event) => {
            console.log("btn capture!");
        })

        window.addEventListener('click', () => {
            console.log("window capture!");
        }, true); // true : 캡처링 단계의 이벤트가 발생하도록 합니다.

        document.addEventListener('click', () => {
            console.log("document capture!");
        }, true);

        parent.addEventListener('click', () => {
            console.log("parent capture!");
        }, true);

        btnFirst.addEventListener('click', (event) => {
            console.log("btn bubble!");
        })

        parent.addEventListener('click', () => {
            console.log("parent bubble!");
        });

        document.addEventListener('click', () => {
            console.log("document bubble!");
        });

        window.addEventListener('click', () => {
            console.log("window bubble!");
        });
    </script>
</body>

</html>

버튼이 아닌 윈도우(화면)를 누른 경우 다음과 같이 출력된다.

버튼을 누른 경우는 다음과 같다.

이벤트리스너의 인자로 true를 주면 캡쳐링(큰 단위 -> 작은 단위)을 사용한다는 얘기이고 인자를 주지 않거나 false로 주면 버블링(작은 단위 -> 큰 단위)을 사용한다는 이야기이다.

따라서 버튼을 클릭하지 않은 경우, 캡쳐링이 사용된 window -> document 순으로 이동하며 parent클래스와 버튼은 사용되지 않았기에 패스되는 모습이다.

버튼을 클릭한 경우, 버튼 캡쳐가 먼저 나올 것 같지만 캡쳐링이 true로 되있는 window -> document -> parent -> btn(버튼) 순으로 진행되며 버블링 코드는 false이므로 작은 단위인 버튼부터 윈도우까지 거꾸로 진행된다.

이벤트 target, currentTarget

부모부터 자식까지 요소를 모두 타고가며 진행되는 이벤트의 특징 덕분에 이벤트 객체에는 target, currentTarget이라는 속성이 존재한다.

target 속성에는 이벤트가 발생한 진원지(가장 깊은곳)의 정보가 담겨 있다. 이 target 속성을 통해 이벤트 리스너가 없는 요소의 이벤트가 발생한 경우에도 해당 요소에 접근할 수 있다.

currentTarget 속성에는 이벤트 리스너가 연결된 요소가 참조되어 있다.

<article class="parent">
    <ol>
        <li><button class="btn-first" type="button">버튼1</button></li>
        <li><button type="button">버튼2</button></li>
        <li><button type="button">버튼3</button></li>
    </ol>
</article>

<script>
    const parent = document.querySelector('.parent');
    parent.addEventListener('click', function (event) {
        console.log(event.target);
        console.log(event.currentTarget);
    })
</script>

버튼 1을 누른 경우 target에는 진원지(버튼1)의 정보가 담겨 있고 currentTarget에는 현재 객체인 parent의 정보가 담겨 있다.

이벤트 위임

targetcurrentTarget을 활용해 이벤트 리스너가 없어도 마치 리스너가 있는 것처럼 사용하는 테크닉을 이벤트 위임이라고 한다.

<!DOCTYPE html>
<html lang="ko">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="../reset.css">
    <style>
    </style>
</head>

<body>
    <article class="parent">
        <ol>
            <li><button class="btn-first" type="button">버튼1</button></li>
            <li><button type="button">버튼2</button></li>
            <li><button type="button">버튼3</button></li>
        </ol>
    </article>

    <script>
        const parent = document.querySelector('.parent');
        parent.addEventListener('click', function (event) {
            console.log(event.target);
            if (event.target.nodeName === "BUTTON") {
                event.target.textContent = "버튼4";
            }
        })
    </script>
</body>

</html>




버튼 1을 누른 결과, 버튼 4로 바뀐 모습을 확인할 수 있다. 코드를 보면 console.log 뒤에 target과 관련된 코드가 있지만 실행된 것을 알 수 있다.

이러한 위임을 사용하면 코드가 줄어들고 이벤트 처리가 수월해진다는 장점이 있다.

이벤트의 this

이벤트 리스너 함수 내부의 this값은 이벤트가 연결된 노드를 참조한다.

<!DOCTYPE html>
<html lang="ko">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>

    <body>
        <article class="parent">
            <ol>
                <li><button class="btn-first" type="button">버튼1</button></li>
                <li><button type="button">버튼2</button></li>
                <li><button type="button">버튼3</button></li>
            </ol>
        </article>

        <script>
            const parent = document.querySelector(".parent");
            parent.addEventListener("click", function (event) {
                console.log(this);
            });

            const myObj = {
                name: "jaehyun",
                walk() {
                    parent.addEventListener("click", () => {
                        console.log(this.name + " is walking");
                    });
                },
            };
            myObj.walk();
        </script>
    </body>
</html>

일반적인 함수로 선언된 이벤트 리스너의 thiscurrentTarget과 같으며 화살표 함수(람다 함수)로 선언된 this는 상위 개념을 가르킨다.

preventDefault

브라우저의 기본 이벤트 동작을 취소하는 기능이다.

<!-- 앵커의 기본 동작을 중지 -->
<a href="https://www.naver.com" class="link">네이버 링크입니다만..</a>
<script>
    const link = document.querySelector('.link');
    link.addEventListener('click', (event) => {
        console.log('clicked');
        event.preventDefault();
    })
</script>

<!-- submit 의 기본 동작을 중지 -->
<form action="">
    <button type="submit" class="submit">제출</button>
</form>
<script>
    const submit = document.querySelector('.submit');
    submit.addEventListener('click', (event) => {
        console.log('clicked');
        event.preventDefault();
    })
</script>

정상적인 경우라면 링크로 넘어가야 하지만 그렇지 않은 모습을 보인다.

마우스 우클릭 방지

<!DOCTYPE html>
<html lang="ko">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <a href="https://www.naver.com" class="link">네이버 링크입니다만..</a>
        <script>
            document.addEventListener("contextmenu", (event) => {
                event.preventDefault();
                console.log("우클릭!");
                alert("해당 페이지에서는 오른쪽 클릭을 제한합니다.");
            });
        </script>
    </body>
</html>

stopPropagation

앞에서 봤던 preventDefault는 브라우저의 기본 이벤트 동작을 취소할 수 있다. 그렇지만 이벤트 흐름(이벤트 전파)는 막지 못한다.

이러한 이벤트 전파를 막고 싶다면 event.stopPropagation()을 추가하면 된다.

<form action="">
	<button type="submit" class="submit">제출</button>
</form>

<script>
	const submit = document.querySelector('.submit');
	submit.addEventListener('click', (event) => {
	    console.log('clicked');
	    event.preventDefault();
			// event.stopPropagation();
	});
	
	document.body.addEventListener('click', () => {
	    console.log('event still alive!');
	});
</script>


stopPropagation이 없으면 순서대로 출력이 되지만 있으면 다르다.

submit 객체 이후의 이벤트는 전파가 막혀 출력이 되지 않은 모습이다.

다른 예제

<h1>나의 todo list</h1>
<p>1. 오늘 저녁에는 부대찌게를 끓여 먹겠다.<button type="button">삭제</button></p>
<p>2. 후식으로 슈팅스타를 먹겠다.<button type="button">삭제</button></p>
<p>3. 자기 전에 반드시 내일 아침 메뉴를 생각해두겠다.<button type="button">삭제</button></p>

위와 같은 HTML코드 구조가 있을 때

  1. P 태그를 클릭하면 p 태그의 컨텐츠를 출력하는 alert 창을 띄워주고
  2. 삭제 버튼을 클릭하면 삭제할 것인지를 물어보는 confirm 창을 띄워주고, 확인을 누르면 P태그를 삭제한다.
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <h1>나의 todo list</h1>
        <p>
            1. 오늘 저녁에는 부대찌개를 끓여 먹겠다.<button type="button">
                삭제
            </button>
        </p>
        <p>2. 후식으로 슈팅스타를 먹겠다.<button type="button">삭제</button></p>
        <p>
            3. 자기 전에 반드시 내일 아침 메뉴를 생각해두겠다.<button
                type="button"
            >
                삭제
            </button>
        </p>
        <script>
            const txts = document.querySelectorAll("p");
            const btns = document.querySelectorAll("button");

            txts.forEach((item) => {
                item.addEventListener("click", () => {
                    // console.log(item.childNodes[0]);
                    alert(item.childNodes[0].textContent);
                });
            });

            btns.forEach((item) => {
                item.addEventListener("click", (event) => {
                    const result = confirm("삭제하시겠습니까?");
                    event.stopPropagation();
                    if (result) {
                        item.parentElement.remove();
                    }
                });
            });
        </script>
    </body>
</html>



confirm은 boolean값을 반환함을 알아두자.

profile
익숙해지기 위해 기록합니다

0개의 댓글