최근 본 글

    자바스크립트 버튼 클릭 이벤트 만들기

    자바스크립트 버튼 클릭 이벤트 만들기 (addEventListener)

    지난 글에서는 코드를 묶어 재사용하는 함수를 배우고, 쇼핑몰 결제 금액 계산기까지 만들어 보았습니다. 그런데 지금까지 만든 자바스크립트 코드에는 한 가지 아쉬운 점이 있었습니다. 결과를 확인하려면 매번 F12를 눌러 콘솔을 열어야 했고, 웹페이지 화면 자체는 아무런 변화가 없었습니다. 이제 드디어 그 벽을 넘을 차례입니다. 오늘은 버튼을 클릭하면 화면이 바뀌는, 진짜 살아 움직이는 웹페이지를 만들어 보겠습니다. 자바스크립트로 HTML 요소를 찾는 방법부터 addEventListener로 이벤트를 연결하는 방법, 그리고 다크 모드 버튼과 글자 수 세기 같은 실용적인 기능까지 차근차근 알아보겠습니다.

    1. 이벤트란 무엇인가요?

    이벤트(Event)는 웹페이지에서 일어나는 사건을 말합니다. 사용자가 버튼을 클릭하는 것, 입력창에 글자를 입력하는 것, 마우스를 어떤 요소 위에 올리는 것, 키보드를 누르는 것, 폼을 제출하는 것이 모두 이벤트입니다.

    자바스크립트는 이런 사건이 일어나기를 기다리고 있다가, 사건이 발생하면 미리 정해 둔 함수를 실행할 수 있습니다. 초인종에 비유하면 이해가 쉽습니다. 누군가 초인종을 누르면(이벤트 발생) 집 안에서 “딩동” 소리가 나고 집주인이 문을 열러 갑니다(함수 실행). 초인종이 눌리기 전까지는 아무 일도 일어나지 않고, 눌리는 순간에만 정해진 반응이 일어나는 것입니다.

    ① 요소 찾기
    어떤 버튼?
    →
    ② 이벤트 연결
    클릭하면
    →
    ③ 함수 실행
    이 일을 해라

    오늘 배울 내용은 결국 이 세 단계로 정리됩니다. “어떤 요소에서, 어떤 사건이 일어나면, 무슨 일을 할 것인가?”를 코드로 적는 것입니다.

    2. DOM – 자바스크립트가 보는 HTML

    자바스크립트로 버튼이나 글자를 다루려면 먼저 DOM이라는 개념을 알아야 합니다. DOM은 Document Object Model의 약자로, 브라우저가 HTML 문서를 읽어서 자바스크립트가 다룰 수 있는 형태로 만든 나무 모양의 구조입니다.

    document
    └── html
        ├── head
        │   └── title
        └── body
            ├── h1
            ├── p
            └── button

    HTML 기초 편에서 태그 안에 태그가 들어가는 구조를 배웠는데, 브라우저는 이것을 가계도처럼 부모와 자식 관계로 정리해 둡니다. 자바스크립트는 이 나무 구조에서 원하는 요소를 찾아내 글자를 바꾸거나, 스타일을 바꾸거나, 이벤트를 연결할 수 있습니다. 맨 꼭대기의 document는 웹페이지 문서 전체를 뜻하며, 앞으로 요소를 찾을 때 항상 이 document에서 출발합니다.

    3. HTML 요소 찾기 – querySelector

    자바스크립트로 HTML 요소를 찾을 때 가장 많이 쓰는 방법은 document.querySelector()입니다. 괄호 안에 CSS 선택자를 그대로 넣으면 됩니다. CSS 선택자 편에서 배운 내용이 여기서 그대로 쓰입니다.

    <h1 id="title">안녕하세요</h1>
    <p class="message">첫 번째 메시지</p>
    <p class="message">두 번째 메시지</p>
    <button>클릭</button>
    
    <script>
      const title = document.querySelector("#title");      // 아이디로 찾기
      const message = document.querySelector(".message");  // 클래스로 찾기
      const button = document.querySelector("button");     // 태그로 찾기
    </script>

    아이디는 #, 클래스는 .을 붙인다는 CSS 규칙이 그대로 적용됩니다. 주의할 점은 querySelector는 조건에 맞는 요소 중 첫 번째 하나만 찾는다는 것입니다. 위 코드에서 message 변수에는 “첫 번째 메시지” 문단만 담깁니다. 조건에 맞는 요소를 모두 찾고 싶다면 document.querySelectorAll()을 사용하고, 지난 글에서 배운 for…of문으로 하나씩 꺼내 처리하면 됩니다.

    예전 코드에서는 document.getElementById("title")처럼 아이디 전용 방법도 자주 보입니다. 이때는 # 없이 아이디 이름만 적습니다. 두 방법 모두 동작하지만, querySelector 하나로 아이디·클래스·태그를 모두 찾을 수 있어 요즘은 querySelector를 더 많이 사용합니다.

    4. 요소의 내용과 스타일 바꾸기

    요소를 찾았다면 이제 그 요소를 바꿀 수 있습니다. 가장 기본적인 세 가지를 알아보겠습니다.

    const title = document.querySelector("#title");
    
    // ① 글자 내용 바꾸기
    title.textContent = "반갑습니다!";
    
    // ② 스타일 직접 바꾸기
    title.style.color = "#264de4";
    title.style.fontSize = "40px";
    
    // ③ 클래스 추가/제거하기
    title.classList.add("highlight");
    title.classList.remove("highlight");
    title.classList.toggle("highlight");  // 있으면 제거, 없으면 추가
    • textContent : 요소 안의 글자를 읽거나 바꿉니다.
    • style : CSS 속성을 직접 바꿉니다. 자바스크립트에서는 하이픈을 쓸 수 없기 때문에 font-size는 fontSize처럼 카멜 케이스로 적습니다. 변수 이름 규칙에서 배운 내용이 여기서도 쓰입니다.
    • classList : 요소에 CSS 클래스를 붙이거나 뗍니다. 특히 toggle은 스위치처럼 누를 때마다 켜고 끄기를 반복해, 다크 모드나 메뉴 열기·닫기 버튼에 매우 유용합니다.

    실무에서는 style로 하나하나 바꾸기보다 CSS에 미리 클래스를 만들어 두고 classList로 붙였다 뗐다 하는 방식을 더 권장합니다. 디자인은 CSS에, 동작은 자바스크립트에 맡기는 것이 내용과 디자인을 분리하는 원칙에도 맞고 관리하기도 쉽습니다.

    5. addEventListener – 이벤트 연결하기

    드디어 오늘의 핵심인 addEventListener입니다. 이름 그대로 “이벤트를 듣는 귀를 추가한다”는 뜻으로, 요소에서 특정 이벤트가 일어나면 함수를 실행하도록 연결해 줍니다.

    요소.addEventListener("이벤트 종류", 실행할 함수);

    버튼을 클릭하면 제목이 바뀌는 가장 간단한 예제를 만들어 보겠습니다.

    <h1 id="title">버튼을 눌러 보세요</h1>
    <button id="btn">클릭!</button>
    
    <script>
      const title = document.querySelector("#title");
      const btn = document.querySelector("#btn");
    
      function changeTitle() {
        title.textContent = "버튼이 클릭되었습니다! 🎉";
      }
    
      btn.addEventListener("click", changeTitle);
    </script>

    클릭 전

    버튼을 눌러 보세요

    클릭!

    클릭 후

    버튼이 클릭되었습니다! 🎉

    클릭!

    여기서 가장 중요한 주의점이 있습니다. addEventListener에 함수를 넣을 때는 changeTitle처럼 괄호 없이 이름만 적어야 합니다. 만약 changeTitle()처럼 괄호를 붙이면, 클릭할 때가 아니라 코드가 읽히는 순간 바로 실행되어 버립니다. 괄호 없이 넘기는 것은 “클릭하면 이 함수를 실행해 줘”라고 함수를 맡겨 두는 것이고, 괄호를 붙이는 것은 “지금 당장 실행해”라는 뜻이기 때문입니다. 지난 글에서 배운 함수의 선언과 호출의 차이를 떠올려 보세요.

    함수를 따로 만들지 않고, 지난 글에서 배운 화살표 함수를 그 자리에 바로 적는 방식도 매우 많이 사용합니다.

    btn.addEventListener("click", () => {
      title.textContent = "버튼이 클릭되었습니다! 🎉";
    });

    한 번만 쓰이는 간단한 동작이라면 이렇게 바로 적는 것이 편하고, 여러 곳에서 같은 동작을 쓴다면 함수를 따로 만들어 이름으로 넘기는 것이 좋습니다.

    6. 자주 쓰는 이벤트 종류

    click 외에도 다양한 이벤트가 있습니다. 실무에서 자주 사용하는 것들을 정리해 보겠습니다.

    이벤트 발생 시점 활용 예
    click요소를 클릭했을 때버튼, 메뉴 열기
    input입력창 내용이 바뀔 때마다글자 수 세기, 실시간 검색
    change값 변경이 완료됐을 때드롭다운, 체크박스 선택
    submit폼을 제출할 때로그인, 회원가입 검사
    keydown키보드를 눌렀을 때Enter로 전송, 단축키
    mouseenter / mouseleave마우스가 들어오거나 나갈 때말풍선 안내, 미리보기
    DOMContentLoadedHTML을 모두 읽었을 때페이지 준비 후 코드 실행

    사용법은 모두 같습니다. addEventListener의 첫 번째 자리에 이벤트 이름만 바꿔 넣으면 됩니다. 이 일관성 덕분에 click 하나만 제대로 익혀 두면 나머지 이벤트도 쉽게 사용할 수 있습니다.

    7. 이벤트 객체 – 무슨 일이 일어났는지 알아내기

    이벤트가 발생하면 브라우저는 그 사건에 대한 자세한 정보를 담은 이벤트 객체를 함수에 자동으로 넘겨줍니다. 함수의 매개변수로 받아서 사용하면 되며, 보통 event나 줄여서 e라는 이름을 씁니다.

    // 어떤 요소에서 이벤트가 일어났는지
    btn.addEventListener("click", (e) => {
      console.log(e.target);          // 클릭된 요소
    });
    
    // 입력창에 입력된 값
    const input = document.querySelector("#nickname");
    input.addEventListener("input", (e) => {
      console.log(e.target.value);    // 현재 입력된 글자
    });
    
    // 어떤 키를 눌렀는지
    input.addEventListener("keydown", (e) => {
      if (e.key === "Enter") {
        console.log("Enter 키를 눌렀습니다!");
      }
    });

    e.target은 이벤트가 일어난 바로 그 요소를 가리킵니다. 입력창이라면 e.target.value로 사용자가 입력한 값을 꺼낼 수 있습니다. 변수와 자료형 편에서 강조했듯이, 이렇게 꺼낸 값은 숫자를 입력했더라도 항상 문자열이라는 점을 잊지 마세요. 계산에 쓰려면 Number()로 바꿔야 합니다. 또한 e.key로 어떤 키가 눌렸는지 알 수 있어, 지난 글에서 배운 if문과 함께 쓰면 “Enter를 누르면 전송” 같은 기능을 쉽게 만들 수 있습니다.

    8. 폼 제출 막기 – preventDefault

    폼 편에서 로그인 화면을 만들었을 때, 로그인 버튼을 누르면 페이지가 새로고침되거나 다른 페이지로 이동하는 것을 보셨을 겁니다. 이것은 폼이 데이터를 서버로 보내는 브라우저의 기본 동작입니다. 자바스크립트로 입력값을 먼저 검사하고 싶다면 이 기본 동작을 막아야 합니다. 이때 사용하는 것이 e.preventDefault()입니다.

    <form id="login-form">
      <input type="text" id="userid" placeholder="아이디">
      <button type="submit">로그인</button>
      <p id="error"></p>
    </form>
    
    <script>
      const form = document.querySelector("#login-form");
      const userid = document.querySelector("#userid");
      const error = document.querySelector("#error");
    
      form.addEventListener("submit", (e) => {
        e.preventDefault();   // 새로고침(기본 동작) 막기
    
        if (userid.value.length < 4) {
          error.textContent = "아이디는 4글자 이상 입력해 주세요.";
          error.style.color = "#d62828";
        } else {
          error.textContent = `${userid.value}님, 환영합니다!`;
          error.style.color = "#2e8b57";
        }
      });
    </script>

    한 가지 주목할 점은 이벤트를 버튼이 아니라 form에 연결했다는 것입니다. submit 이벤트는 로그인 버튼을 클릭할 때뿐 아니라 입력창에서 Enter 키를 눌렀을 때도 발생하므로, form에 연결해야 두 경우를 모두 처리할 수 있습니다. 문자열 뒤에 .length를 붙이면 글자 수를 알 수 있는데, 지난 글에서 배열의 개수를 구할 때 썼던 것과 같은 방식입니다.

    9. onclick 속성과 addEventListener의 차이

    인터넷의 예제 코드를 보다 보면 HTML 태그에 직접 이벤트를 적는 방식도 볼 수 있습니다.

    <!-- HTML 속성 방식 -->
    <button onclick="changeTitle()">클릭</button>

    이 방식도 동작은 하지만 권장하지 않습니다. CSS 편에서 인라인 스타일보다 외부 스타일 시트를 권장했던 것과 같은 이유입니다. HTML과 자바스크립트가 뒤섞여 코드가 지저분해지고, 나중에 수정할 때 HTML 곳곳을 찾아다녀야 합니다. 또한 onclick 방식은 한 요소에 동작을 하나만 연결할 수 있지만, addEventListener는 같은 요소에 여러 개의 함수를 연결할 수 있습니다. 따라서 새로 작성하는 코드에서는 addEventListener를 사용하는 것이 좋습니다.

    10. 실습: 카운터, 다크 모드, 글자 수 세기 만들기

    오늘 배운 내용을 모두 활용해 세 가지 기능이 있는 페이지를 만들어 보겠습니다. 아래 코드를 event.html로 저장해 브라우저에서 열고 직접 버튼을 눌러 보세요. 이번에는 콘솔을 열 필요 없이 화면에서 바로 결과를 확인할 수 있습니다.

    <!DOCTYPE html>
    <html lang="ko">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>이벤트 실습</title>
      <style>
        * { box-sizing: border-box; }
        body {
          margin: 0;
          padding: 24px 16px;
          font-family: sans-serif;
          background: #f5f7fb;
          color: #333;
          transition: background 0.3s, color 0.3s;
        }
        /* 다크 모드 클래스 */
        body.dark {
          background: #1a1a2e;
          color: #eee;
        }
        .box {
          max-width: 480px;
          margin: 0 auto 20px;
          padding: 24px;
          background: white;
          border-radius: 12px;
          text-align: center;
        }
        body.dark .box { background: #2a2a40; }
    
        #count {
          font-size: 48px;
          font-weight: bold;
          margin: 10px 0;
        }
        button {
          padding: 10px 18px;
          margin: 4px;
          border: none;
          border-radius: 6px;
          background: #e0b000;
          color: white;
          font-size: 16px;
          cursor: pointer;
        }
        textarea {
          width: 100%;
          height: 100px;
          padding: 10px;
          border: 1px solid #ccc;
          border-radius: 6px;
          font-size: 15px;
        }
        .over { color: #d62828; font-weight: bold; }
      </style>
    </head>
    <body>
    
      <!-- ① 다크 모드 -->
      <div class="box">
        <h2>다크 모드</h2>
        <button id="theme-btn">🌙 다크 모드 켜기</button>
      </div>
    
      <!-- ② 카운터 -->
      <div class="box">
        <h2>카운터</h2>
        <p id="count">0</p>
        <button id="minus">- 1</button>
        <button id="reset">초기화</button>
        <button id="plus">+ 1</button>
      </div>
    
      <!-- ③ 글자 수 세기 -->
      <div class="box">
        <h2>한 줄 소개 (최대 50자)</h2>
        <textarea id="intro" placeholder="자기소개를 입력하세요"></textarea>
        <p><span id="length">0</span> / 50자</p>
      </div>
    
      <script>
        // ===== ① 다크 모드 =====
        const themeBtn = document.querySelector("#theme-btn");
    
        themeBtn.addEventListener("click", () => {
          document.body.classList.toggle("dark");
    
          if (document.body.classList.contains("dark")) {
            themeBtn.textContent = "☀️ 라이트 모드 켜기";
          } else {
            themeBtn.textContent = "🌙 다크 모드 켜기";
          }
        });
    
        // ===== ② 카운터 =====
        const countText = document.querySelector("#count");
        let count = 0;
    
        function updateCount() {
          countText.textContent = count;
          countText.style.color = count < 0 ? "#d62828" : "";
        }
    
        document.querySelector("#plus").addEventListener("click", () => {
          count++;
          updateCount();
        });
    
        document.querySelector("#minus").addEventListener("click", () => {
          count--;
          updateCount();
        });
    
        document.querySelector("#reset").addEventListener("click", () => {
          count = 0;
          updateCount();
        });
    
        // ===== ③ 글자 수 세기 =====
        const intro = document.querySelector("#intro");
        const lengthText = document.querySelector("#length");
    
        intro.addEventListener("input", (e) => {
          const length = e.target.value.length;
          lengthText.textContent = length;
    
          if (length > 50) {
            lengthText.classList.add("over");
          } else {
            lengthText.classList.remove("over");
          }
        });
      </script>
    </body>
    </html>

    완성된 화면에서 각 기능은 다음과 같이 동작합니다.

    카운터

    3

    – 1 초기화 + 1

    음수가 되면 숫자가 빨간색으로

    다크 모드 적용 후

    ☀️ 라이트 모드 켜기

    누를 때마다 켜짐·꺼짐 전환

    글자 수 세기

    코딩을 배우고 있는 입문자입니다

    17 / 50자

    50자를 넘으면 빨간색 경고

    세 기능에 오늘 배운 내용이 골고루 담겨 있습니다. 다크 모드는 CSS에 미리 만들어 둔 dark 클래스를 classList.toggle로 켜고 끄며, classList.contains로 현재 상태를 확인해 버튼 글자를 바꿉니다. CSS 선택자 편에서 배운 body.dark .box 같은 조합 선택자 덕분에 클래스 하나로 페이지 전체 색이 바뀝니다. 카운터는 값이 바뀌어야 하므로 let으로 만든 count 변수를 버튼마다 늘리거나 줄이고, 화면을 갱신하는 코드는 updateCount 함수 하나로 묶어 세 버튼이 함께 사용합니다. 지난 글에서 배운 “반복되는 코드는 함수로”의 실제 활용입니다. 글자 수 세기는 input 이벤트로 글자를 입력할 때마다 실시간으로 길이를 세고, 50자를 넘으면 경고 클래스를 붙입니다. SNS나 회원가입 화면에서 흔히 보는 바로 그 기능입니다.

    11. 자주 하는 실수와 해결 방법

    • Cannot read properties of null 오류 : 가장 흔한 오류로, querySelector가 요소를 찾지 못해 null을 돌려준 경우입니다. 선택자의 #이나 .을 빠뜨렸는지, 아이디 철자가 맞는지 확인하세요. 또는 script 태그가 HTML 요소보다 위에 있어서 아직 요소가 만들어지기 전에 찾으려 한 경우일 수 있습니다. script는 body 끝에 두세요.
    • 클릭도 안 했는데 함수가 실행돼요 : addEventListener("click", myFunc())처럼 함수에 괄호를 붙인 경우입니다. 괄호를 빼고 이름만 넘기세요.
    • 이벤트 이름 오타 : "Click", "onclick"처럼 쓰면 동작하지 않습니다. 이벤트 이름은 on 없이 소문자로 "click"이라고 적어야 합니다.
    • 폼 버튼을 누르면 페이지가 새로고침돼요 : submit 이벤트에서 e.preventDefault()를 빠뜨린 경우입니다.
    • querySelectorAll로 찾은 요소에 이벤트가 안 걸려요 : querySelectorAll은 여러 요소의 목록을 돌려주므로 목록 전체에 addEventListener를 쓸 수 없습니다. for…of문으로 하나씩 꺼내서 각각 연결하세요.
    • 입력값 계산이 이상해요 : e.target.value는 항상 문자열입니다. 숫자 계산이 필요하면 Number()로 바꾸세요.

    마무리하며

    오늘은 자바스크립트로 웹페이지를 살아 움직이게 만드는 이벤트에 대해 알아보았습니다. 핵심은 세 단계입니다. querySelector로 요소를 찾고, addEventListener로 이벤트를 연결하고, 함수 안에서 textContent나 classList로 화면을 바꾸는 것입니다. 함수를 넘길 때 괄호를 붙이지 않는다는 점, 이벤트 객체의 e.target으로 입력값을 꺼낸다는 점, 그리고 폼 제출 시 preventDefault로 새로고침을 막는다는 점도 꼭 기억해 두세요. 오늘을 기점으로 HTML, CSS, 자바스크립트 세 가지가 하나로 연결되었습니다. HTML로 만든 버튼을, CSS로 꾸미고, 자바스크립트로 동작하게 만든 것입니다.

    다음 글에서는 지금까지 배운 변수, 조건문, 반복문, 함수, 이벤트를 모두 활용해 실제로 쓸 수 있는 할 일 목록(To-Do List) 앱을 만들어 보겠습니다. 할 일을 추가하고, 완료 표시를 하고, 삭제하는 기능까지 직접 구현해 볼 예정입니다. 궁금한 점은 댓글로 남겨 주세요!

    댓글 0

    댓글 남기기