Skip to main content
Summary

이벤트는 브라우저가 보내는 알림 신호입니다

지난 장에서는 자바스크립트로 HTML 요소를 찾고, 글자를 바꾸고, 새 요소를 추가했습니다. 그런데 그 코드는 우리가 직접 실행해야만 움직였습니다.

실제 웹페이지는 조금 다릅니다. 사용자가 버튼을 누르면 문구가 바뀌고, 입력칸에 글자를 쓰면 즉시 안내문이 나타나고, 할 일 목록의 삭제 버튼을 누르면 해당 항목이 사라집니다.

이때 필요한 개념이 이벤트입니다.

이벤트는 브라우저가 보내는 알림 신호라고 생각하면 됩니다.

  • 사용자가 버튼을 클릭했다

  • 입력칸에 글자를 썼다

  • 폼을 제출했다

  • 마우스를 어떤 영역 위에 올렸다

  • 키보드에서 Enter를 눌렀다

브라우저는 이런 일이 생길 때마다 신호를 보냅니다. 자바스크립트는 그 신호를 기다리고 있다가, 신호가 오면 정해 둔 코드를 실행합니다.

이벤트를 연결할 때 가장 많이 쓰는 방법은 addEventListener()입니다.

요소.addEventListener('이벤트이름', 실행할함수);

조금 자연스러운 말로 바꾸면 이런 뜻입니다.

이 요소에서 어떤 일이 생기는지 기다리다가, 그 일이 생기면 이 함수를 실행해 줘.

이제 콘솔이 아니라 HTML 파일에서 직접 확인해 보겠습니다.

클릭과 입력에 반응하는 이벤트AI 생성 이미지

버튼을 클릭하면 문구 바꾸기

가장 먼저 클릭 이벤트를 다뤄 보겠습니다. 아래 코드를 event-practice.html이라는 이름으로 저장하고 브라우저에서 열어 보세요.

<!DOCTYPE html>
<html lang='ko'>
<head>
  <meta charset='UTF-8'>
  <title>이벤트 연습</title>
</head>
<body>
  <p id='message'>아직 버튼을 누르지 않았습니다.</p>
  <button id='changeButton'>문구 바꾸기</button>

  <script>
    const message = document.querySelector('#message');
    const changeButton = document.querySelector('#changeButton');

    changeButton.addEventListener('click', function () {
      message.textContent = '버튼을 눌렀습니다!';
    });
  </script>
</body>
</html>

흐름을 천천히 보면 이렇습니다.

const message = document.querySelector('#message');

화면의 문구 요소를 찾습니다.

const changeButton = document.querySelector('#changeButton');

버튼 요소를 찾습니다.

changeButton.addEventListener('click', function () {
  message.textContent = '버튼을 눌렀습니다!';
});

changeButton에서 click 이벤트가 일어나기를 기다립니다. 사용자가 버튼을 클릭하면 중괄호 안의 코드가 실행되고, 문구가 바뀝니다.

여기서 중요한 점이 하나 있습니다. 아래처럼 쓰면 안 됩니다.

changeButton.addEventListener('click', message.textContent = '버튼을 눌렀습니다!');

이 코드는 클릭을 기다리지 않고, 페이지가 열릴 때 바로 문구를 바꾸려고 합니다. 그리고 addEventListener()의 두 번째 자리에는 함수가 들어가야 하는데, 위 코드에서는 함수가 아니라 문구를 바꾼 결과가 들어가므로 오류가 날 수 있습니다.

이벤트에는 보통 나중에 실행할 함수를 전달합니다. 그래서 function () { ... }처럼 묶어 줍니다.

마우스를 올리면 카드 강조하기

이벤트는 클릭에만 쓰이지 않습니다. 사용자가 마우스를 어떤 영역 위에 올렸을 때도 반응할 수 있습니다.

아래 예제는 카드 위에 마우스를 올리면 배경색을 바꾸고, 마우스를 빼면 다시 원래대로 돌립니다.

<style>
  .card {
    width: 240px;
    padding: 20px;
    border: 1px solid #ccc;
    border-radius: 12px;
  }

  .highlight {
    background-color: #fff3b0;
    border-color: #f0c000;
  }
</style>

<div id='productCard' class='card'>
  <strong>오늘의 추천 상품</strong>
  <p>마우스를 올려 보세요.</p>
</div>

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

  productCard.addEventListener('mouseenter', function () {
    productCard.classList.add('highlight');
  });

  productCard.addEventListener('mouseleave', function () {
    productCard.classList.remove('highlight');
  });
</script>

mouseenter는 마우스가 요소 안으로 들어왔을 때 발생합니다. mouseleave는 마우스가 요소 밖으로 나갔을 때 발생합니다.

여기서는 자바스크립트가 직접 색을 하나하나 바꾸지 않습니다. 대신 highlight라는 클래스를 붙였다가 제거합니다. 모양은 CSS가 담당하고, 자바스크립트는 상태만 바꾸는 방식입니다. 이 습관은 코드가 길어질수록 도움이 됩니다.

입력할 때마다 글자 수 세기

검색창, 댓글창, 회원가입 화면에서는 사용자가 글자를 입력하는 순간 바로 안내가 바뀌는 경우가 많습니다.

예를 들어 자기소개를 20자 이내로 입력해야 한다고 해 보겠습니다.

<label for='intro'>자기소개</label>
<input id='intro' type='text' placeholder='20자 이내로 입력하세요'>
<p id='countMessage'>0 / 20</p>

<script>
  const intro = document.querySelector('#intro');
  const countMessage = document.querySelector('#countMessage');

  intro.addEventListener('input', function () {
    const length = intro.value.length;
    countMessage.textContent = length + ' / 20';

    if (length > 20) {
      countMessage.textContent = length + ' / 20 - 너무 깁니다.';
    }
  });
</script>

입력칸의 현재 값은 value로 읽습니다.

intro.value

그리고 문자열의 길이는 length로 확인합니다.

intro.value.length

input 이벤트는 입력칸의 값이 바뀔 때마다 발생합니다. 글자를 입력할 때뿐 아니라 지울 때도 실행됩니다. 그래서 글자 수 세기, 검색어 자동 반응, 비밀번호 길이 검사 같은 기능에 자주 쓰입니다.

폼 제출과 preventDefault()

이번에는 폼을 다뤄 보겠습니다. 폼은 로그인, 회원가입, 검색, 주문서처럼 사용자가 입력한 내용을 제출하는 영역입니다.

HTML에서 폼은 기본 동작을 가지고 있습니다. 제출 버튼을 누르면 브라우저가 페이지를 새로고침하거나 다른 주소로 이동하려고 합니다.

그런데 자바스크립트로 입력값을 검사하거나 화면에 결과를 보여 주고 싶을 때는 이 기본 동작이 방해가 됩니다. 결과를 표시하자마자 페이지가 새로고침되면 화면이 다시 처음 상태로 돌아가기 때문입니다.

이때 사용하는 것이 preventDefault()입니다. 이름은 길지만 뜻은 단순합니다.

기본 동작을 막아 줘.

예제로 확인해 보겠습니다.

<form id='loginForm'>
  <label for='userName'>이름</label>
  <input id='userName' type='text'>
  <button type='submit'>확인</button>
</form>

<p id='result'></p>

<script>
  const loginForm = document.querySelector('#loginForm');
  const userName = document.querySelector('#userName');
  const result = document.querySelector('#result');

  loginForm.addEventListener('submit', function (event) {
    event.preventDefault();

    if (userName.value.trim() === '') {
      result.textContent = '이름을 입력해 주세요.';
      return;
    }

    result.textContent = userName.value + '님, 환영합니다.';
  });
</script>

여기서 새로 등장한 event는 이벤트에 대한 자세한 정보가 들어 있는 값입니다. 브라우저가 함수를 실행할 때 자동으로 넣어 줍니다.

function (event) {
  event.preventDefault();
}

폼 제출 이벤트가 발생하면, 먼저 event.preventDefault()로 새로고침을 막습니다. 그다음 입력값을 검사하고 안내 문구를 보여 줍니다.

입력값을 확인할 때 사용한 trim()은 앞뒤의 빈칸을 없앤 뒤 비교하는 도구입니다. 사용자가 스페이스바만 여러 번 누른 경우도 빈 값으로 볼 수 있어서 실습 예제에서 자주 함께 씁니다.

return은 함수 실행을 그 자리에서 끝냅니다. 이름이 비어 있을 때는 아래의 환영 문구까지 실행되면 안 되므로 return으로 멈춥니다.

event.target으로 실제로 눌린 요소 찾기

이벤트가 발생하면 브라우저는 event 안에 여러 정보를 담아 줍니다. 그중 자주 쓰는 것이 event.target입니다.

event.target은 실제로 이벤트가 일어난 요소를 가리킵니다.

버튼이 여러 개 있을 때 특히 유용합니다.

<div id='menu'>
  <button>아메리카노</button>
  <button>라테</button>
  <button>주스</button>
</div>

<p id='selectedMenu'>메뉴를 선택해 주세요.</p>

<script>
  const menu = document.querySelector('#menu');
  const selectedMenu = document.querySelector('#selectedMenu');

  menu.addEventListener('click', function (event) {
    if (event.target.tagName !== 'BUTTON') {
      return;
    }

    selectedMenu.textContent = event.target.textContent + '를 선택했습니다.';
  });
</script>

눈여겨볼 점은 버튼마다 이벤트를 따로 붙이지 않았다는 것입니다. 세 버튼을 감싸는 #menu에 이벤트를 한 번만 붙였습니다.

사용자가 안쪽 버튼을 클릭하면, 그 클릭 신호는 바깥의 #menu에서도 받을 수 있습니다. 그리고 event.target을 보면 실제로 어떤 버튼이 눌렸는지 알 수 있습니다.

중간에 있는 if문은 실제로 눌린 요소가 버튼인지 확인합니다.

if (event.target.tagName !== 'BUTTON') {
  return;
}

#menu 영역 안의 빈 곳을 누를 수도 있기 때문에, 버튼이 아닐 때는 return으로 함수를 끝냅니다.

이 방식을 이벤트 위임이라고 부릅니다. 말은 어렵지만 원리는 간단합니다.

여러 아이에게 각각 전달하지 않고, 반장에게 한 번만 알려 두는 방식입니다.

이벤트 위임은 목록이 많을 때 유용합니다. 예를 들어 할 일 목록의 삭제 버튼이 100개라면 버튼마다 이벤트를 붙이는 대신, 목록 전체에 이벤트를 한 번만 붙일 수 있습니다.

<ul id='todoList'>
  <li>우유 사기 <button class='deleteButton'>삭제</button></li>
  <li>메일 확인 <button class='deleteButton'>삭제</button></li>
  <li>운동하기 <button class='deleteButton'>삭제</button></li>
</ul>

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

  todoList.addEventListener('click', function (event) {
    if (event.target.classList.contains('deleteButton')) {
      event.target.parentElement.remove();
    }
  });
</script>

이 코드는 먼저 실제로 클릭된 요소가 deleteButton 클래스를 가지고 있는지 확인합니다.

if (event.target.classList.contains('deleteButton'))

삭제 버튼이 맞다면, 그 버튼의 부모 요소인 li를 찾아 제거합니다.

event.target.parentElement.remove();

이벤트 위임을 쓸 때는 확인 과정이 중요합니다. 목록 영역 안의 빈 곳이나 글자를 눌렀을 때도 클릭 이벤트가 발생할 수 있기 때문입니다. 그래서 classList.contains()로 삭제 버튼이 맞는지 검사한 뒤에만 삭제합니다.

키보드 사용자도 사용할 수 있게 만들기

이벤트를 배울 때 마우스 클릭만 떠올리기 쉽습니다. 하지만 웹페이지는 마우스를 쓰지 않는 사람도 사용할 수 있어야 합니다. 키보드로 이동하고 Enter를 눌러 버튼을 실행하는 사용자도 있습니다.

좋은 출발점은 HTML 요소를 제 역할에 맞게 쓰는 것입니다.

  • 누르는 기능은 div보다 button을 사용합니다.

  • 입력칸에는 label을 연결합니다.

  • 폼 제출은 버튼 클릭만 보지 말고 submit 이벤트로 처리합니다.

예를 들어 로그인 버튼에만 click 이벤트를 붙이면 마우스로 누르는 상황만 먼저 떠올리게 됩니다. 반면 폼의 submit 이벤트를 처리하면 입력칸에서 Enter를 눌러 제출하는 흐름도 함께 다룰 수 있습니다.

loginForm.addEventListener('submit', function (event) {
  event.preventDefault();
  // 입력값 검사와 처리
});

이렇게 만들면 마우스 사용자와 키보드 사용자 모두 자연스럽게 사용할 수 있습니다.

자주 만나는 오류 읽기

이벤트를 처음 다룰 때 자주 보는 오류가 있습니다.

Cannot read properties of null (reading 'addEventListener')

이 메시지는 보통 이런 뜻입니다.

이벤트를 붙이려는 요소를 찾지 못했다.

예를 들어 HTML에는 id='changeButton'이라고 썼는데, 자바스크립트에서 #changeBtn이라고 찾으면 결과가 null이 됩니다. 없는 요소에는 addEventListener()를 붙일 수 없습니다.

확인할 것은 세 가지입니다.

  1. HTML의 id와 자바스크립트 선택자가 같은가?

  2. #이나 .을 빠뜨리지 않았는가?

  3. <script>가 요소보다 먼저 실행되고 있지는 않은가?

이 책에서는 보통 <script>를 </body> 바로 위에 둡니다. 그러면 브라우저가 HTML 요소를 먼저 만든 뒤 자바스크립트를 실행하므로 초보자가 실수할 가능성이 줄어듭니다.

작은 실습: 입력하면 추가되는 목록

마지막으로 클릭, 입력값, 폼 제출, 화면 변경을 한 번에 묶어 보겠습니다.

<form id='todoForm'>
  <label for='todoInput'>할 일</label>
  <input id='todoInput' type='text'>
  <button type='submit'>추가</button>
</form>

<ul id='todoList'></ul>

<script>
  const todoForm = document.querySelector('#todoForm');
  const todoInput = document.querySelector('#todoInput');
  const todoList = document.querySelector('#todoList');

  todoForm.addEventListener('submit', function (event) {
    event.preventDefault();

    if (todoInput.value.trim() === '') {
      alert('할 일을 입력해 주세요.');
      return;
    }

    const li = document.createElement('li');
    li.textContent = todoInput.value;

    todoList.appendChild(li);
    todoInput.value = '';
  });
</script>

이 실습의 흐름은 다음과 같습니다.

  1. 사용자가 입력칸에 할 일을 씁니다.

  2. 추가 버튼을 누르거나 Enter를 누릅니다.

  3. submit 이벤트가 발생합니다.

  4. preventDefault()로 새로고침을 막습니다.

  5. 입력값이 비어 있으면 안내하고 멈춥니다.

  6. 값이 있으면 새 li를 만들어 목록에 추가합니다.

  7. 입력칸을 비웁니다.

여기서도 trim()을 사용했기 때문에 공백만 입력한 경우는 할 일로 추가되지 않습니다.

이제 웹페이지는 사용자의 행동을 기다렸다가 반응할 수 있습니다. 이것이 이벤트의 핵심입니다. 자바스크립트가 화면을 바꾸는 힘은 DOM에서 나오고, 화면이 살아 움직이는 느낌은 이벤트에서 시작됩니다.

책 목록

비개발자를 위한 JavaScript: 설치 없이 시작해 작은 프로젝트까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기