JavaScript 공부하기_Event(EventTarget), Bubbling&Capturing, Target Element, 브라우저의 기본기능 취소, 이벤트위임, keydown&keyup, form태그

Lina Hongbi Ko·2023년 2월 21일
0

JavaScript-studying

목록 보기
4/6
post-thumbnail

🐶 Event Target

브라우저 위에서 발생할 수 있는 이벤트는 다양하다.

예를 들면,

mouse click, keyboard, resizing window, close window, page loading, form submission, video is being played, error ... 많은 이벤트들이 있고, 우리는 핸들링 하고 싶은 이벤트만 처리할 수 있다.

즉, 무수히 많은 이벤트들 중 우리가 핸들링하고 싶은 부분만 이벤트처리 할 수 있다는 뜻.

✏️ HOW's eventTarget working?

먼저 우리는 특정한 요소에 일단 eventHandler를 등록한다. 즉, 브라우저에게 버튼에서 클릭이벤트가 발생하면, 나중에 사용자가 클릭을 하게 되었을때 지금 등록한 eventHandler를 호출해줘 하고 등록시킨다는 말이다. 그리고나서 사용자가 클릭을 하게 되면 브라우저에서 event라는 object를 만들어서 이 event object에는 어떤 부분에서 클릭이 되었는지, 어떤 요소가 클릭 되었는지 등 다양한 정보가 들어있는 event object를 우리가 등록한 콜백함수에 전달한다.

Event Handler를 등록할 수 있는 요소(모든 Element)는 event Target에 상속되므로(앞전에 DOM 상속도를 봤을때) 우리는 모두 Event Handler를 등록 할 수 있다.

✏️ event Target의 종류

  • eventTarget.addEventListener() : 이벤트 추가
  • eventTarget.removeEventListener() : 이벤트 제거
  • eventTarget.dispatchEvent() : 인공적으로 event 발생시킴
const listener = () => {
	console.log("clicked");
}

Element.addEventListener("click", () => {
	listener;
}); // Element 클릭시 콘솔창에 "clicked" 찍힘

Element.dispatchEvent(new Event(click));

Element.removeEventListener("click, () => {
	listener;
});  // Element 클릭시 콘솔창에 "clicked" 찍히지 않음

🐶 Bubbling & Capturing

표준 DOM 이벤트에서 정의한 이벤트 흐름에는 3단계가 있다.
1. 캡쳐링단계 (Capture Phase) : 이벤트가 하위요소로 전파되는 단계
2. 타깃단계 (Target Phase) : 이벤트가 실제 타겟요소에 전달되는 단계
3. 버블링단계 (Bubling Phase) : 이벤트가 상위요소로 전파되는 단계

이렇게 브라우저에서는 이벤트가 처리될때 3단계를 거친다.


위의 사진처럼 3단계를 거쳐 이벤트가 처리되는데 일반적으로 이벤트 리스너(콜백함수)를 등록하게 되면, 기본적으로 버블링단계에서 등록된 콜백함수가 호출된다. 보통 캡쳐링 단계에서 무언가를 처리해줘야 한적은 없고, 기본적으로 버블링단계에서 특정이벤트를 처리해주게 되고, 이벤트 위임을 활용한다,
만약 event.addEventListener("event", callback, {capture : true}); 라고 하면 capture 단계에서 이 콜백함수가 호출되도록 등록되어진다.

✏️ 버블링(Bubbling)

한 요소에 이벤트가 발생하면 이 요소에 할당된 핸들러가 동작하고, 이어서 부모요소의 핸들러가 동작한다. 가장 최상단의 조상요소를 만날때까지 이 과정이 반복되면서 요소 각각에 할당된 핸들러가 동작한다.
(하위 이벤트가 상위로 전달되어가는것, 부모/자식구조이며 똑같은 이벤트를 가지고 있는 경우에만 발생)

ex)

<html>
	<body>
    	<div>
        	<p></p>
        </div>
    </body>
</html>
<script>
	const p = document.querySelector('p');
    const div = document.querySelector('div');
    const body = document.querySelector('body');
    function Alert(message) {
    	return function() {
        	alert(message);
        }
    }
    p.addEventListener("click", Alert('p tag event')); // 1
    div.addEventListener("click", Alert('div tag event')); // 2
    body.addEventListener("click", Alert('body tag event')); // 3
    
</script>

p태그에 클릭이벤트가 발생하면 해당 이벤트가 부모인 div에 전달되고, 최종적으로 body까지 전달된다. 따라서 Alert창이 1, 2 ,3번 순서대로 버블링 되어 나온다. 이를 통해 우리는 알 수 있다. 자식요소에 이벤트가 발생하면 부모 콜백함수도 실행되어진다는 것을.
💡 따라서 부모컨텐츠는 모든 자식들에게서 event가 발생하는 것을 들을 수 있다. 밑에서 한번더 언급하겠지만, 결론은 "이벤트위임을 발생시킴!!"

✏️ 캡쳐링(Capturing)

이벤트 캡쳐링은 특정 엘리먼트에 이벤트가 발생했을 경우, 이벤트가 최상단의 부모요소로부터 전달되어져 내려오는 현상이다. 따라서 전달되는 이벤트는 부모요소의 이벤트 핸들러를 작동시킨다. 캡쳐링을 하기 위해서는 이벤트 핸들러에 {capture: true} 혹은 true로 캡쳐링 옵션을 지정해준다. default는 false이므로 별다른 옵션을 설정하지 않으면 캡쳐링은 일어나지 않는다. (default는 버블링단계에서 동작)

ex)

const elements = document.querySelector("*");
for(let elem of elements) {
	elem.addEventListener('click', e => alert('캡쳐링: ${elem.tagName}'), true});	
}

p태그를 클릭하면, alert창이 html > body > div > p 순으로 나온다.

💡 하지만 캡쳐링 단계를 이용하는 경우는 거의 없다.

🐶 Target Element

이벤트 버블링이 일어났을때, 최초로 이벤트를 발생시킨 요소를(위의 예제에서는 p태그) 타겟 엘리먼트(Target Element)라고 한다. 이는 event.target을 통해 접근가능하다.

⭐️ event.target과 event.currentTarget의 차이점 ⭐️

  • event.target : 최초로 이벤트를 발생시킨 엘리먼트 (지금 클릭이 된 요소)
  • event.currentTarget(=this) : 현재 이벤트가 발생된 엘리먼트 (지금 클릭이 된 이벤트를 처리하고 있는, 즉 이벤트리스너가 등록되어있는 요소)

ex)

<html>
	<body>
    	<div>
        	<p></p>
        </div>
    </body>
</html>
const $body = document.querSelector('body');
function Alert(event) {
	alert('타겟 엘리먼트 : ${event.target.tagName},
    	   현재 엘리먼트 : ${this.tagName}')
}
$body.addEventListener('click', Alert);

p태그를 클릭하면, alert창에
타겟 엘리먼트 : P, 현재 엘리먼트 : BODY

div태그를 클릭하면, alert창에
타겟 엘리먼트 : DIV, 현재 엘리먼트 : BODY

body태그를 클릭하면, alert창에
타겟 엘리먼트 : BODY, 현재 엘리먼트: BODY

ex)

<script>
	const outer = document.querySelector('.outer');
    const middle = document.querySelector('.middle');
    const button = document.querySelector('button');
    
    outer.addEventListener('click', event => {
    	console.log(`outer: ${event.currentTarget}, ${event.target}`);
    });
    middle.addEventListener('click', event => {
    	console.log(`middle: ${event.currentTarget}, ${event.target}`);
    });
    button.addEventListener('click', event => {
    	console.log(`button1: ${evente.currentTarget}, ${event.target}`);
    })
    button.addEventListener('click', event => {
    	console.log(`button2: ${event.currentTarget}, ${event.target}`);
    });
</script>

위의 코드에서 버튼을 누르면 console창에는 아래처럼 출력된다.

button1: [object HTMLButtonElement], [object HTMLButtonElement]
button2: [object HTMLButtonElement], [object HTMLButtonElement]
middle: [object HTMLDivElement], [object HTMLButtonElement]
outer: [object HTMLDivElement], [object HTMLButtonElement]

그렇다면 button을 클릭했을때의 콘솔창만 띄우려면 어떻게 할까?

<script>
.
.
.
// 위와 동일
button.addEventistener('click', event => {
	console.log(`button1: ${event.currentTarget}, ${event.target}`);
	event.stopPropagation();
})
button.addEventistener('click', event => {
	console.log(`button2: ${event.currentTarget}, ${event.target}`);
    event.stopImmediatePropagation();
})
</script>

위처럼 event.stopPropagagation() API를 쓰면, 부모로 올라가는 버블링을 중단할 수 있다. 하지만 버튼에는 2개의 이벤트가 지정되어있으니까 button1, button2의 이벤트가 발생한다.
그렇다면 하나의 이벤트가 발생되게 하려면 어떻게 할까? event.stopImmediatePropagation() API를 쓰면 된다. 하지만 stopImmediatePropagation()은 쓴 위치에 따라 취소하는 것이 달라진다. button2에서 stopImmediatePropagation()을 쓰면 button1이 먼저 입력되었기때문에 button1의 이벤트는 계속 작동한다. 예를 들어 button3을 위의 코드에 추가하면 button2 다음에 등록된 이벤트를 갖고 있는 button3과 부모들의 이벤트(버블링)는 취소될 것이다.

✏️ event.stopPropagation() : 버블링에서는 타겟 엘리먼트에만 이벤트가 발생하도록 해주고, 캡쳐링에서는 타켓 엘리먼트 기준으로 최상단 엘리먼트에만 이벤트가 발생하도록 해준다.
✏️ event.stopImmediatePropagation() : 요소에 할당된 특정한 이벤트(같은 요소와 같은 단계에서 설정한 리스너) 빼고 나머지 핸들러는 모두 동작하지 않는다.

⭐️ 하지만 우리는 꼭 필요한 경우를 제외하고 버블링을 막지 않아야 한다. 위의 두가지 API는 되도록 사용하지 말자. 나만 처리하고 다른 부모요소는 이벤트를 발생하지 못하게 하면 어떤 이벤트를 했을때 꼭 일어나야하는 다른 부모요소의 이벤트까지 막는다.(ex. google 분석) ⭐️

그럼 어떻게 하면 될까?

<script>
	const outer = document.querySelector('.outer');
    const middle = document.querySelector('.middle');
    const button = document.querySelector('button');
    
    outer.addEventListener('click', event => {
    	if(event.target !== event.currentTarget) {
        	return;
        }
    	console.log(`outer: ${event.currentTarget}, ${event.target}`);
    });
    middle.addEventListener('click', event => {
    	if(event.target !== event.currentTarget) {
        	return;
        }
    	console.log(`middle: ${event.currentTarget}, ${event.target}`);
    });
    button.addEventListener('click', event => {
    	console.log(`button1: ${evente.currentTarget}, ${event.target}`);
    })
    button.addEventListener('click', event => {
    	console.log(`button2: ${event.currentTarget}, ${event.target}`);
    });
</script>

💡 버블링되는 코드에 아래의 코드를 추가한다.

if(event.target !== event.currentTarget) {
	return;
}

🐶 브라우저의 기본 기능 취소 (evenet prevent)

✏️ passive에 해당되는 리스너 : 브라우저가 리스너에게 이벤트가 일어났다는 것만 노티스하고 자기 할 일을 함 (예를 들어 스크롤링)
✏️ active에 해당되는 리스너 : 브라우저가 리스너가 할 일을 할떄까지 기다려주고 실행을 함. 대부분의 리스너들은 active (예를 들어 체크박스에 체크표시하기)

💡 e.preventDefault() : 브라우저에서 기본적으로 발생하는 동작을 취소하는 API. 예를 들어 많이 사용되는 경우에는 a태그를 눌렀을 때 href링크로 이동하지 않게 하는 경우, formdksdp submit역할을 하는 버튼을 눌렀어도 새로고침하여 실행하지 않게 하고싶은 경우(제출은 되지만 새로고침은 안되게)

⭐️ passvie 리스너에 preventDefault()를 쓸 경우에는 에러가 발생한다. 그러므로 passive에는 preventDefault()를 쓰지 않을 것을 권장 / active에 사용하는것 ok ⭐️

ex)

<script>
document.addEventListener('wheel', event => {
	console.log('scrolling'');
    event.preventDefault();
});
// passive -> 스크롤링은 되지만 콘솔에 error 나옴

const checkbox = document.querySelector('input');
checkbox.addEventListener('click', event => {
	console.log('checked');
    event.preventDefault();
})
// active -> 콘솔창에 'checked'가 나오긴 하지만 
// 브라우저의 체크박스 체크 안됨

</script>

굳이 passive에 preventDefault()를 써야겠다면 option 중 'passive:false' 추가한 뒤 사용가능하다.

<script>
document.addEventListener('wheel', event => {
	console.log('scrolling');
    event.preventDefault();
}, {passive: false}
);
</script>

active하게 설정해줘서 에러는 나지 않지만 브라우저 성능에 좋지 않다. 그러므로 웬만해서는 설정하지 말자.

🐶 이벤트 위임 (Event Delegation)

이벤트 버블링을 이용한 방법 "부모 컨테이너는 어떤 자식요소에서 이벤트가 발생하든, 모든 이벤트를 다 들을 수 있다."를 이용.

<body>
	<ul>
    	<li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>5</li>
        <li>6</li>
    </ul>
</body>

우리는 보통 이런 구조가 있으면 보통 아래처럼 코드를 썼다.

const lis = document.querySelectorAll('li');
lis.forEach(li => {
	li.addEventListener('click', () => {
    	li.classList.add('selected');
    });
});

// BAD

하지만 이것보다 좋은 방법이 있다.

const ul = document.querySelector('ul');
ul.addEventListener('click', (e) => {
	if(e.target.tagName == 'LI') {
    	event.target.classList.add('selected');
    }
});

// GOOD

이처럼 반복되는 이벤트를 처리할 때, 부모 안의 자식들에게 공통적으로 무언가를 해줘야할때는 일일이 자식에게 이벤트 리스너를 추가하는 것보다 부모 하나에게 등록하는 것이 좋다. 그래야 메모리에도 더 좋고. -> 동적인 엘리먼트에 대한 이벤트 처리가 수월하고, 이벤트 핸들러 관리에 수월하고, 메모리 사용량을 줄이고, 메모리 누수 가능성이 줄어든다.

ex) "data-"를 이용한 예제

<html>
	<body>
    	<div id='menu'>
        	<button data-action='save'>저장</button>
            <button data-action='reset'>초기화</button>
            <button data-action='load'>불러오기</button>
        </div>
    </body>
</html>
<script>
	const $Menu = document.getElementById('Menu');
    const ActionFunction = {
    	save: () => alert('저장');
        reset: () => alert('초기화');
        load: () => alert('불러오기');
    }
    $Menu.addEventListener('click', e => {
    	const action = e.target.dataset.action
        if(action){
        	ActionFunction[action]();
        }
    });
</script>

🐶 keydown & keyup

'keypress' event를 최신버전에서는 더이상 지원하지 않는다. 그래서 keydown 이나 keyup을 쓴다.

✏️ keydown : 사용자가 키보드를 눌렀을때 바로 발생하는 이벤트 (누르고나서)
✏️ keyup : 사용자가 키보드를 눌렀다가 손을 뗀 순간 발생하는 이벤트 (떼고나서)

input.addEventListener('keydown', (e) => {
	if(e.isComposing) {
    	return;
    }
    if(e.key === 'Enter') {
    	onAdd();
    }
});

keypress는 이제 더이상 지원하지 않으므로 keydown/keyup을 쓰면되는데 이 둘의 미묘한 차이를 알고 써야한다.
예를 들어, input 칸에 한국어로 오늘의 해야 할 일을 입력한다고 했을때 한국어처럼 받침이 있는 경우에는 만들어지고 있는 와중에 한번 더 출력이되므로 e.isComposing을 써주는게 좋다. 그게 귀찮으면 keyup을 쓰면 된다.

🐶 Web form

브라우저에서 사용자에게 input을 받아오거나 또는 사용자가 데이터를 입력해서 서버에 데이터를 전송해야하는 경우, 우리는 'form'태그를 쓴다. form은 어떤 http 메소드를 이용해서 서버에게 post할건지, put할건지, get할건지 등을 명시할 수 있다. 데이터를 서버에 보내지 않아도 클라이언트측에서 단순히 데이터를 받아오는 경우에도 사용할 수 있다. (이럴땐 action 생략) 그러면 자바스크립트에서 클릭과 enter 이벤트를 등록하지 않아도 form 안의 버튼을 클릭하면 자동으로 submit이라는 이벤트가 발생한다. (그러므로 e.preventDefault를 잊지 말아야 한다)

그렇기 때문에 form을 이용하면 어떤 버튼을 클릭 해야 하거나 enter을 눌렀을 때(keydown or keyup) 이벤트가 발생되도록 따로 지정할 필요없이 form의 submit 이벤트를 이용해서 편하게 기능을 만들 수 있다.


출처
드림코딩아카데미 브라우저 101

profile
프론트엔드개발자가 되고 싶어서 열심히 땅굴 파는 자

0개의 댓글