다다의 개발일지 6v6

[JS] Day 1 - Javascript Drum Kit ( 키보드로 드럼치기!! 바닐라 js 프로젝트 1 시작 ) 본문

Frontend/JavaScript

[JS] Day 1 - Javascript Drum Kit ( 키보드로 드럼치기!! 바닐라 js 프로젝트 1 시작 )

dev6v6 2025. 2. 19. 01:16

 

https://javascript30.com/

 

 

JavaScript 30

Build 30 things with vanilla JS in 30 days with 30 tutorials

javascript30.com

우선 DOM조작, 이벤트 핸들링 부터 알아보자

DOM 조작을 위한 주요 document 메서드

 

특정 요소를 선택하는 방법

document.getElementById("id이름");  // ID로 요소 선택
document.getElementsByClassName("클래스이름");  // 클래스 이름으로 여러 개 선택 (배열 비슷한 형태)
document.getElementsByTagName("태그이름");  // 태그 이름으로 여러 개 선택 (예: "div", "p")

document.querySelector("선택자");  // CSS 선택자 방식 (가장 처음 나오는 요소 선택)
document.querySelectorAll("선택자");  // CSS 선택자로 모든 요소 선택 (NodeList 반환)
더보기

둘 다 id="item"인 요소를 선택

document.getElementById("item");      // id가 "item"인 요소 선택
document.querySelector("#item");      // CSS 선택자로 id가 "item"인 요소 선택

차이점 정리

getElementById("id") ID로 요소 선택 단일 요소 (HTMLElement) 빠름 🚀
querySelector("#id") CSS 선택자로 요소 선택 단일 요소 (Element) 약간 느림
  • ID 선택만 할 거면 getElementById()가 직관적이고 빠름.
  • CSS 선택자(클래스, 태그 등)를 같이 쓰고 싶다면 querySelector()가 유용.

큰 프로젝트라면 성능 때문에  getElementById 이걸 쓰는게 낫다고 함

 

새로운 요소 만들기

document.createElement("태그이름");  // 새로운 요소 생성

 

  사용 예시

let newDiv = document.createElement("div"); 
newDiv.textContent = "새로운 div 요소!";
document.body.appendChild(newDiv);  // body 태그 안에 추가

DOM에서 요소를 제거하는 방법

요소.remove();  // 직접 삭제
부모요소.removeChild(자식요소);  // 부모 기준으로 삭제

 

사용 예시

let box = document.getElementById("box");
box.remove();  // box 요소 삭제

 

 

속성 및 내용 변경

요소.textContent = "텍스트 변경";  // 순수 텍스트 변경
요소.innerHTML = "<b>HTML 변경</b>";  // HTML 포함 변경
요소.setAttribute("속성이름", "값");  // 속성 추가/변경
요소.getAttribute("속성이름");  // 속성 값 가져오기
요소.removeAttribute("속성이름");  // 속성 제거

 

사용 예시

let title = document.getElementById("myTitle");
title.textContent = "새로운 제목";
title.setAttribute("class", "highlight");

 

 

이벤트 핸들링 (사용자 입력 감지)

이벤트 추가하는 방법

요소.addEventListener("이벤트명", 함수);

 

사용 예시

let btn = document.getElementById("myButton");

btn.addEventListener("click", function () {
  alert("버튼 클릭됨!");
});

자주 쓰이는 이벤트 종류

"click" 클릭 시 실행
"mouseover" 마우스를 요소 위에 올릴 때
"mouseout" 마우스가 요소 밖으로 나갈 때
"keydown" 키보드를 누를 때
"keyup" 키보드를 뗄 때
"change" <input> 값이 변경될 때
"submit" 폼이 제출될 때

 

키보드 이벤트 예제

document.addEventListener("keydown", function (event) {
  console.log("눌린 키: " + event.key);
});

 

1. 먼저 키를 눌렀을 때 소리 나게 만들어 보자! + 애니메이션

 

keydown 이벤트에서 event 객체에 들어있는 정보

더보기

이 객체에는 어떤 키가 눌렸는지, 어떤 요소에서 발생했는지 같은 정보를 포함하고 있다.

이벤트 객체(e)에서 자주 쓰는 속성

A 키 입력 시

 

e.key 사용자가 입력한 키 값 "a" (소문자)
e.code 물리적 키 이름 "KeyA"
e.keyCode 키 코드(숫자, 옛날 방식) 65
e.shiftKey Shift 키가 눌렸는지 true 또는 false
e.ctrlKey Ctrl 키가 눌렸는지 true 또는 false
e.altKey Alt 키가 눌렸는지 true 또는 false
e.repeat 키가 계속 눌려있는지 true 또는 false

 

 

일단 키를 눌렀을 때 소리 나게 만들어 보자! + 애니메이션

document.addEventListener("keydown", function (e) {
    // key를 눌렀을 때 그 키의 data-key와 같은 .key요소 찾기
    let key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
    let audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);

    if (!key || !audio) return; // 해당하는 키가 없으면 종료 (우리가 설정한 키 이외의 키면 종료)

    key.classList.add("playing"); // 키에 애니메이션 효과 추가
    audio.currentTime = 0; // 소리를 처음부터 재생 (연타 가능)
    audio.play();
});

 

이거를 좀 더 정리해서 따로 함수로 만들어 주면

function playSound(e) {
    // key를 눌렀을 때 그 키의 data-key와 같은 .key요소 찾기
    let key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
    let audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);

    if (!key || !audio) return; // 해당하는 키가 없으면 종료 (우리가 설정한 키 이외의 키면 종료)

    key.classList.add("playing"); // 키에 애니메이션 효과 추가
    audio.currentTime = 0; // 소리를 처음부터 재생 (연타 가능)
    audio.play();
}

window.addEventListener("keydown", playSound);

 

여기서 document가 아닌 window를 이용해서 이벤트 핸들링한 이유

더보기
window.addEventListener("keydown", playSound);
  • window → 브라우저 창 전체를 의미.
  • .addEventListener("keydown", playSound) → 키보드를 누르면(keydown) playSound 함수를 실행.
  • 즉, 사용자가 어디에 포커스가 있든 키를 누르면 작동함.

window와 document 두 객체의 차이점

  window document
대상 브라우저 창 웹 페이지(HTML)
역할 브라우저 기능 조작 HTML 조작
주요 기능 창 크기, 스크롤, 알림창 요소 선택, 내용 변경

즉, document는 window 안에 있는 HTML 문서를 다루는 객체

document.addEventListener("keydown", playSound); 해도 되지 않을까?

document에서도 이벤트를 감지할 수 있지만 HTML 문서 내에서만 감지하고
window에서 감지하는 이유는 "웹 페이지의 어디에서든 키 입력을 받을 수 있도록 하기 위해서"이다.

 

차이점                                    window.addEventListener             document.addEventListener

 

감지 범위 브라우저 창 전체 HTML 문서 내에서만
포커스 필요 여부 없음 (어디서든 가능) 웹 페이지에 포커스가 있어야 가능

정리하자면

  • window.addEventListener("keydown", playSound);
    → 브라우저 창에서 키보드 입력을 감지해서 playSound 실행!
  • window를 사용하면 어디에서든(심지어 입력창에 포커스가 있어도) 키 입력을 놓치지 않음.
  • document.addEventListener("keydown", playSound);도 가능하지만, window가 더 범용적으로 작동함.

 

여기까지 하면 눌렀을 때 소리는 나지만 소리가 끝난 후에도 애니메이션이 적용되어 있다.

누르고 돌아오지 않음!

2. 이제 애니메이션이 끝났을 때 효과가 사라지도록 해보자

애니메이션이 끝났음을 감지하는 transitionend 이벤트 이용할 거임!

 

transitionend 이벤트란?

CSS transition(변화)이 끝났을 때 실행되는 이벤트

예를 들어, 버튼이 커지는 애니메이션이 끝났을 때 실행하고 싶은 코드가 있다면 transitionend를 사용할 수 있다.

element.addEventListener("transitionend", function (event) {
    console.log("애니메이션 끝남!", event.propertyName);
});

 

특정 요소(element)에서 CSS transition이 끝나면 이벤트 실행!
✅ event.propertyName → 어떤 속성의 변화가 끝났는지 확인 가능. = 변화가 끝난 속성의 이름을 띄운다

 

 

이 프로젝트에서는 크기 변화라 속성 이름이 transform이다

function removeTransition(e) {
    if (e.propertyName !== 'transform') return; // transform이 아닌 다른 속성의 변화가 끝나면 패스!!
    e.target.classList.remove('playing'); // 애니메이션 끝나면 원래 상태로 복구
}

 

  1. 키를 누르면 (keydown) playing 클래스 추가 → CSS에서 애니메이션 효과 적용.
  2. 애니메이션(transform)이 끝나면 "transitionend" 이벤트 발생!
  3. removeTransition()이 실행돼서 propertyName 확인 후 transform이 맞으면
  4.  playing 클래스 제거 → 원래 상태로 복구.

 

transitionend가 필요한 이유

더보기

만약 transitionend를 사용하지 않고 직접 setTimeout을 써서 클래스를 제거하면?

// 비효율적인 코드
document.addEventListener("keydown", function (e) {
    const key = document.querySelector(`div[data-key="${e.keyCode}"]`);
    if (!key) return;

    key.classList.add("playing");

    setTimeout(() => {
        key.classList.remove("playing"); // 일정 시간 후 제거
    }, 200); // transition 시간과 맞춰야 함
});
  • setTimeout(200)처럼 CSS transition 시간과 정확히 맞춰야 해서 유지보수가 어려움.
  • 애니메이션 속도를 변경하면 setTimeout 값도 수정해야 함.
  • transitionend를 사용하면 자동으로 애니메이션이 끝나는 타이밍에 실행되므로 더 효율적이다 ^^

 

const keys = document.querySelectorAll('.key');
keys.forEach(key => key.addEventListener('transitionend', removeTransition));

 

  1. document.querySelectorAll('.key')로 모든 .key 요소를 선택해 keys에 저장.
  2. keys.forEach()로 각 key 요소에 대해 transitionend 이벤트를 감지하고, 애니메이션이 끝났을 때마다 removeTransition 함수가 실행되도록 설정.
  3. removeTransition 함수에서는 애니메이션이 끝난 속성이 transform일 때만 'playing' 클래스를 제거.

 

키가 여러번 눌려도 transitionend 이벤트가 계속 발생하는 이유

더보기

keys.forEach(key => key.addEventListener('transitionend', removeTransition));
이렇게 이벤트 리스너를 추가하면, transitionend 이벤트가 발생할 때마다 removeTransition 함수가 계속 호출된다. 즉, 재사용이 가능! 

 

왜 계속 재사용이 가능할까?

이벤트 리스너는 "이벤트가 발생할 때마다" 그에 해당하는 콜백 함수를 실행하도록 설정되기 때문에, 한 번의 이벤트 처리 후에도 리스너는 계속 활성화된 상태에 있음.

 

이벤트 리스너의 동작

  1. transitionend 이벤트가 한 번 발생하면 removeTransition 함수가 실행.
  2. transitionend 이벤트가 다시 발생하면, 그때마다 removeTransition 함수가 새롭게 실행.
    • 이벤트 리스너는 한번 추가되면 계속 동작하므로, 매번 새로운 이벤트 발생마다 자동으로 실행된다!

 

결과물!!! ㅜㅜ

ㅎㅂㅎ

 

 

script

function playSound(e) {
	// key를 눌렀을 때 그 키의 data-key와 같은 .key요소 찾기
	let key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
	let audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);

	if (!key || !audio) return; // 해당하는 키가 없으면 종료 (우리가 설정한 키 이외의 키면 종료)

	key.classList.add("playing"); // 키에 애니메이션 효과 추가
	audio.currentTime = 0; // 소리를 처음부터 재생 (연타 가능)
	audio.play();
}
function removeTransition(e) {
	if (e.propertyName !== "transform") return; // transform이 아닌 다른 속성의 변화가 끝나면 패스!!
	e.target.classList.remove("playing"); // 애니메이션 끝나면 원래 상태로 복구
}

window.addEventListener("keydown", playSound);

const keys = document.querySelectorAll(".key");
keys.forEach((key) =>
	key.addEventListener("transitionend", removeTransition)
);