웹 페이지가 사용자의 클릭이나 키보드 입력에 반응하려면, 요소에 **"이 일이 일어나면 이 함수를 불러 달라"**고 미리 맡겨 두어야 합니다. 브라우저에서 일어나는 사건을 **이벤트(event)**라 부르고, 이벤트를 기다렸다가 실행되는 콜백 함수를 **이벤트 리스너(event listener)**라고 부릅니다.
1클릭 리스너 등록하기: addEventListener
요소에 리스너를 등록할 때는 요소.addEventListener에 '이벤트이름'과 콜백함수를 넘깁니다.
<p id="count">0</p>
<button id="plus" type="button">
+1 올리기
</button>단추(#plus)를 누를 때마다 숫자(count)를 1씩 올려 화면(#count)에 보여 주는 코드입니다.
'use strict';
let count = 0;
const label = document
.querySelector('#count');
const btn = document
.querySelector('#plus');
이어서 단추에 클릭 리스너를 등록합니다.
btn.addEventListener(
'click',
() => {
// 1. 상태 바꾸기
count = count + 1;
// 2. 화면 고치기
label.textContent = count;
},
);
처음 화면: 0
단추를 한 번 누르면: 1
단추를 또 한 번 누르면: 2addEventListener('click', ...): 지금 당장 화살표 함수를 실행하는 것이 아니라, 나중에'click'이 일어났을 때 **할 일 줄(task queue)**에 올려 실행해 달라고 브라우저에 맡겨 둡니다(규칙 8).- 콜백 안에서는 1. 상태 바꾸기(
count = count + 1;)와 2. 화면 고치기(label.textContent = count;) 두 단계를 나란히 적습니다.count만 올리고label.textContent를 갱신하지 않으면 단추를 아무리 눌러도 화면은0에 멈춰 있습니다.
2함수를 넘길 때 ()를 붙이지 않는다
이름 붙은 함수 handle을 만들어 addEventListener의 두 번째 인자로 넘길 때는 괄호 없이 함수 이름 handle만 적어야 합니다.
만약 'click' 뒤에 handle()처럼 ()를 붙이면 무슨 일이 일어나는지 Node에서 똑같이 확인해 봅니다.
function fakeAdd(type, fn) {
console.log('등록된 값:', fn);
}
function handle() {
console.log('실행됨!');
}
fakeAdd('click', handle());
실행됨!
등록된 값: undefinedhandle()처럼 괄호를 붙이면 리스너를 등록하는 그 줄에서 함수가 딱 한 번 바로 실행되어 버리고, 정작 addEventListener에는 handle()의 반환값인 undefined가 넘어갑니다. 그래서 정작 사용자가 단추를 누를 때는 아무 일도 일어나지 않습니다.
올바르게 함수를 맡기려면 괄호를 뺀 fakeAdd('click', handle)처럼 적어야 합니다.
function fakeAdd(type, fn) {
console.log(
'등록된 종류:',
typeof fn,
);
}
function handle() {
console.log('실행됨!');
}
fakeAdd('click', handle);
등록된 종류: function3리스너 안에서 오류가 나도 페이지는 살아 있다
2레슨 표에서 봤듯이, 브라우저에서는 클릭 리스너가 돌다가 오류가 나더라도 그 한 번의 클릭 실행만 멈추고 웹 페이지는 죽지 않습니다.
'use strict';
let step = 0;
btn.addEventListener(
'click',
() => {
step = step + 1;
if (step === 1) {
throw new Error('첫 오류');
}
label.textContent = step;
},
);
첫 클릭: 콘솔에 Error: 첫 오류
둘째 클릭: 화면이 2로 바뀜첫 번째 클릭에서 오류가 나도 리스너 등록 자체는 사라지지 않으므로, 두 번째 클릭을 하면 콜백이 다시 할 일 줄에 올라가 step이 2가 되고 화면이 바뀝니다.
예제
결과를 먼저 생각한 뒤 펼쳐 보세요. 맞혀 보기는 점수에 들지 않습니다.
다음 코드를 처음 불러왔을 때(아직 단추를 한 번도 누르기 전)와, 단추를 한 번 눌렀을 때 콘솔에 무엇이 찍힐까요?
'use strict';
function greet() {
console.log('반갑습니다');
}
console.log('준비 시작');
btn.addEventListener(
'click',
greet,
);
console.log('준비 끝');
결과 보기
[처음 불러왔을 때]
준비 시작
준비 끝
[단추를 한 번 눌렀을 때]
반갑습니다addEventListener는greet함수를 등록만 하고 바로 다음 줄로 넘어가므로 처음에는'준비 시작'과'준비 끝'만 찍힙니다.- 사용자가 단추를 누르면 그때 할 일 줄에서
greet가 실행되어'반갑습니다'가 찍힙니다.
자주 틀리는 곳
addEventListener에handle()처럼 괄호를 붙여 넘긴다: 괄호를 붙이면 페이지가 열릴 때handle이 한 번 실행되고 끝납니다. 인자 없이 넘길 때는handle만 적고, 인자를 넘겨야 할 때는() => handle(1)처럼 화살표 함수로 감쌉니다.- 콜백 안에서 변수만 바꾸고 화면 요소를 안 고친다:
count += 1;만 적으면 메모리 속 숫자만 올라갑니다. 바뀐 값을label.textContent = count;로 화면 요소에 다시 대입해야 눈에 보입니다. - 클릭할 때마다
addEventListener안에서 또addEventListener를 부른다: 리스너 등록은 처음에 한 번만 해 두면 클릭할 때마다 계속 불립니다. 리스너 안에서 같은 리스너를 중첩해서 또 등록하면 한 번 클릭에 함수가 두 번, 네 번씩 중복 실행됩니다.
확인 문제
addEventListener의 둘째 인자로onSave를 넘길 때와onSave()를 넘길 때의 차이를 설명하세요.
정답
onSave는 함수 자체를 넘겨 단추를 클릭할 때마다 실행되게 하지만, onSave()는 등록하는 줄에서 함수를 즉시 한 번 실행해 버리고 그 반환값(undefined)을 넘기므로 클릭해도 실행되지 않습니다.
- 클릭할 때마다
removeItem(3)처럼 인자3을 넣어 부르고 싶습니다.addEventListener의 두 번째 인자 자리를 어떻게 적어야 하나요?
정답
() => removeItem(3)처럼 화살표 함수로 감싸서 넘깁니다.
- 브라우저의 클릭 리스너 첫 번째 실행 도중
TypeError가 났습니다. 사용자가 그 단추를 다시 한 번 누르면 리스너가 실행되나요, 아니면 페이지가 완전히 멈춰 있나요?
정답
다시 실행됩니다. 브라우저에서는 오류가 난 그 한 번의 리스너 실행만 멈추고 페이지와 등록된 리스너는 그대로 살아 있습니다.