
JavaScript 키보드 이벤트 완벽 가이드: key, code, keyCode의 차이와 디버깅 방법
📷 Life Of Pix / PexelsJavaScript 키보드 이벤트 완벽 가이드: key, code, keyCode의 차이와 디버깅 방법
JavaScript 키보드 이벤트의 핵심 개념 — keyCode가 왜 deprecated됐는지, key와 code의 차이는 무엇인지, 모든 브라우저와 키보드 레이아웃에서 동작하는 단축키를 만드는 방법을 실전 예제로 설명합니다.
비슷한 버그를 여러 번 겪어봤습니다. Chrome에서 키보드 단축키 핸들러를 작성하고, 잘 동작하는 걸 확인한 뒤 배포했는데, 얼마 지나지 않아 Firefox에서 동작하지 않는다는 버그 리포트가 들어왔습니다. 같은 코드인데 동작이 달랐습니다. 한 시간쯤 파고들어 보니 원인은 단순했습니다 — 키를 감지할 때 e.keyCode를 사용했는데, 브라우저와 키보드 레이아웃에 따라 다른 값을 반환하고 있었습니다.
이 버그는 완전히 예방 가능합니다. 그래서 JavaScript 키보드 이벤트를 제대로 이해하는 게 중요합니다. 이 글에서는 전체 키보드 이벤트 모델을 살펴봅니다 — 어떤 이벤트가 발생하는지, 어떤 속성을 사용해야 하는지, 어떤 건 피해야 하는지, 그리고 어디서나 동작하는 단축키를 만드는 방법까지 다룹니다.
세 가지 이벤트: keydown, keypress, keyup
사용자가 키를 누르면 브라우저는 순서대로 최대 세 가지 이벤트를 발생시킵니다:
- keydown — 키를 누르는 즉시 발생, 문자가 삽입되기 전
- keypress — keydown 이후 발생, 문자를 생성하는 키에만 해당 (문자, 숫자, 구두점)
- keyup — 키를 떼면 발생
짧게 말하면: 대부분의 경우 keydown을 사용하세요. 이유는 이렇습니다:
keypress는 deprecated됐습니다. Escape, Delete, F1~F12, 방향키 같은 출력되지 않는 키에서는 발생하지 않았습니다. 단축키 핸들러가 문자 키에만 동작했다면, keypress가 원인이었을 가능성이 높습니다. 새 코드에서는 사용하지 마세요.
keyup은 키를 뗀 뒤에 반응해야 할 때 유용합니다 — 예를 들어 사용자가 문자 입력을 마친 후 미리보기를 갱신하는 경우. 하지만 단축키나 게임 컨트롤에는 keydown이 더 빠르고 예측 가능한 동작을 제공합니다.
keydown은 키를 계속 누르고 있을 때 OS 키 반복 속도로 반복 발생한다는 장점도 있습니다. 스크롤이나 게임 캐릭터 이동에 필요한 동작 방식입니다.
document.addEventListener('keydown', (e) => {
// 여기가 맞는 위치입니다
console.log(e.key, e.code);
});
실제로 중요한 속성들
KeyboardEvent가 발생하면 이벤트 객체에는 여러 속성이 있습니다. 꼭 알아야 할 것들을 설명합니다.
key — 키가 생성하는 값
key는 현재 컨텍스트에서 키가 나타내는 문자열 값을 반환합니다. 일반 문자 키라면 문자 자체입니다: "a", "A" (Shift와 함께), "é" (프랑스어 키보드). 특수 키라면 설명 이름입니다: "Enter", "Escape", "ArrowLeft", "F5", "Backspace".
가장 먼저 사용해야 할 속성입니다. 레이아웃과 수정자를 인식하며, 키가 사용자에게 무엇을 의미하는지 알려줍니다.
document.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
submitForm();
}
if (e.key === 'Escape') {
closeModal();
}
if (e.key === 'ArrowLeft') {
goToPreviousSlide();
}
});
한 가지 주의점: key는 대소문자와 수정자를 구분합니다. Shift 없이 a 키를 누르면 "a", Shift와 함께 누르면 "A"입니다. 대소문자에 무관한 단축키를 만들려면 정규화하세요:
if (e.key.toLowerCase() === 'k') {
openCommandPalette();
}
code — 어떤 물리적 키가 눌렸는지
code는 수정자 키나 키보드 레이아웃에 상관없이 물리적 키 위치의 식별자를 반환합니다. 글자 영역 왼쪽 상단의 키는 항상 "KeyQ"입니다 — 사용자의 레이아웃이 거기에 "A"를 배치해도 마찬가지입니다 (AZERTY 레이아웃처럼).
형식은 일관됩니다: 문자 키는 "KeyA" ~ "KeyZ", 숫자 키는 "Digit0" ~ "Digit9", 기능 키는 "F1" ~ "F12".
키의 위치가 생산하는 문자보다 중요할 때 code를 사용합니다. 대표적인 예가 게임 컨트롤입니다:
document.addEventListener('keydown', (e) => {
switch (e.code) {
case 'KeyW':
case 'ArrowUp':
player.moveUp();
break;
case 'KeyS':
case 'ArrowDown':
player.moveDown();
break;
case 'KeyA':
case 'ArrowLeft':
player.moveLeft();
break;
case 'KeyD':
case 'ArrowRight':
player.moveRight();
break;
}
});
code를 사용하면 QWERTY, AZERTY, Dvorak 어떤 레이아웃이든 WASD 컨트롤이 동일하게 동작합니다. AZERTY에서는 W 키가 다른 위치에 있어 e.key는 "z"를 반환하지만, e.code는 여전히 물리적 위치가 같기 때문에 "KeyW"를 반환합니다.
keyCode와 which — 레거시, deprecated, 피하세요
keyCode와 which는 과거 방식입니다. 키에 대한 숫자 코드를 반환합니다 — A는 65, Enter는 13, Escape는 27. 문제는 특히 구두점과 특수 키에서 이 값들이 일관성이 없었다는 것입니다. 브라우저마다 다른 숫자를 반환했고, 값들은 직관성이 전혀 없는 Windows 가상 키 코드 기반이었습니다.
MDN 문서는 keyCode와 which 모두 deprecated로 표시합니다. 하위 호환성을 위해 아직 존재하지만, 새 코드에서는 사용하지 마세요. 이 속성을 사용하는 기존 코드를 유지보수 중이라면 마이그레이션을 계획하세요.
흔히 보는 과거 코드:
// 하지 마세요
if (e.keyCode === 13) { /* Enter */ }
if (e.which === 27) { /* Escape */ }
현대적인 대체:
// 이렇게 하세요
if (e.key === 'Enter') { /* Enter */ }
if (e.key === 'Escape') { /* Escape */ }
charCode — keypress에서만, 역시 deprecated
charCode는 문자의 Unicode 코드 포인트를 반환했지만, keypress 이벤트에서만, 그것도 출력 가능한 문자에서만 동작했습니다. keypress 자체가 deprecated됐으므로 charCode도 이중으로 deprecated입니다. e.key를 사용하고 Unicode 값이 필요하면 .codePointAt(0)을 호출하세요.
location — 여러 위치에 있는 키 구분
location은 키보드의 여러 위치에 존재하는 키가 어디서 눌렸는지 알려줍니다. 숫자값입니다: 0은 표준 위치, 1은 왼쪽 수정자, 2는 오른쪽 수정자, 3은 넘패드.
e.location === 1에 e.key === "Shift"면 왼쪽 Shift 키가 눌린 것입니다. e.location === 3에 e.key === "5"면 넘패드의 5가 눌린 것입니다. 잘 필요하지 않지만 알아두면 좋습니다.
수정자 키: Ctrl, Shift, Alt, Meta
키보드 단축키는 보통 일반 키와 하나 이상의 수정자 키를 조합합니다. 이벤트 객체에는 각 수정자에 대한 불리언 속성이 있습니다:
e.ctrlKey— Ctrl 키가 눌린 상태e.shiftKey— Shift 키가 눌린 상태e.altKey— Alt(Mac에서는 Option) 키가 눌린 상태e.metaKey— Meta 키가 눌린 상태 (Mac에서는 Command, Windows에서는 Windows 키)
이 속성들은 어떤 속성으로 키를 식별하든 상관없이 현재 이벤트에 대해 항상 정확하게 설정됩니다:
document.addEventListener('keydown', (e) => {
// Ctrl+S (Mac에서는 Cmd+S)
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault(); // 브라우저의 저장 대화상자 방지
saveDocument();
}
// Ctrl+Shift+P — 명령 팔레트
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'p') {
e.preventDefault();
openCommandPalette();
}
// Alt+Left — 뒤로 가기
if (e.altKey && e.key === 'ArrowLeft') {
goBack();
}
});
e.ctrlKey || e.metaKey 패턴은 크로스플랫폼 단축키의 표준 접근 방식입니다. macOS에서는 대부분 Command(metaKey)를 사용하고, Windows/Linux에서는 Ctrl을 사용합니다. 이 패턴이 둘 다 처리합니다.
e.preventDefault() 호출도 중요합니다. 단축키가 브라우저 단축키와 충돌할 경우 브라우저가 해당 동작을 실행하는 것을 막으려면 이 메서드를 호출해야 합니다. 없으면 Ctrl+S가 브라우저의 저장 대화상자를 열어버립니다.
국제 키보드 문제
개발자를 자주 당황시키는 시나리오가 있습니다: 앱에 키보드 단축키(예: 주석 토글용 Ctrl+/)를 추가합니다. en-US 키보드에서 테스트하면 잘 동작합니다. 그런데 독일의 사용자가 단축키가 동작하지 않는다는 버그 리포트를 올립니다.
독일어 키보드 레이아웃에는 / 문자를 위한 전용 키가 없습니다. Shift+7 같은 다른 조합으로 입력합니다. 그래서 해당 키 위치를 눌렀을 때 e.key는 예상과 전혀 다른 값을 반환합니다.
두 가지 접근 방식이 있습니다:
방법 1: 위치 기반 단축키에는 e.code 사용
// US 키보드의 / 키는 특정 물리적 위치에 있습니다
// code는 항상 해당 위치에 대해 'Slash'를 반환합니다
if (e.ctrlKey && e.code === 'Slash') {
toggleComment();
}
단축키가 키 위치 기반으로 의미가 있다면 잘 동작합니다. 하지만 다른 레이아웃 사용자에게는 키에 표시된 내용이 예상과 다르게 느껴질 수 있습니다.
방법 2: 사용자가 단축키를 직접 설정하도록 허용
VS Code 같은 앱이 잘 구현한 방식입니다 — 모든 단축키를 원하는 조합으로 다시 지정할 수 있게 합니다. 구현하기 복잡하지만, 국제 사용자를 위한 전문 도구에서는 올바른 답입니다.
방법 3: 여러 키 값 허용
// US 레이아웃 값과 일반적인 대안 모두 허용
const toggleKeys = new Set(['/', '?', '-']);
if (e.ctrlKey && toggleKeys.has(e.key)) {
toggleComment();
}
이상적이진 않지만 단순한 경우에는 실용적입니다.
접근성: 키보드 탐색은 중요합니다
앱에 커스텀 키보드 인터랙션을 추가할 때 키보드 탐색이 핵심 접근성 요구사항임을 잊지 마세요. 스크린 리더에 의존하거나 마우스를 사용할 수 없는 사용자는 키보드 접근에 의존합니다.
몇 가지 사항을 기억하세요:
포커스 관리. 인터랙티브 요소가 포커스 가능한지 확인하고(tabindex="0" 필요 시), UI 변경에 따라 포커스가 논리적으로 이동하는지 확인하세요. 모달을 열면 포커스를 모달 안으로 이동하고, 닫으면 모달을 열었던 요소로 돌아와야 합니다.
포커스를 가두지 마세요 (모달 제외). 사용자가 Tab 키로 인터페이스를 탐색할 때 막히지 않아야 합니다.
키보드 단축키에만 의존하지 마세요. 사용자가 발견하지 못할 수 있습니다. 모든 액션에 대해 보이는 UI 대안을 제공하세요.
Keycode Viewer로 디버깅하기
키보드 인터랙션을 구현할 때 가장 어려운 부분 중 하나는 특정 키 입력에 대해 브라우저가 어떤 값을 반환하는지 파악하는 것입니다. 문서가 도움이 되지만, 직접 키를 눌러 결과를 확인하는 게 필요할 때가 있습니다.
Keycode Viewer 도구가 바로 그 역할을 합니다. 도구를 열고 아무 키나 누르면 즉시 확인할 수 있습니다:
key— 논리적 값code— 물리적 위치 식별자keyCode— 레거시 숫자 값 (기존 코드 작업 시 참고용)which— 다른 레거시 속성charCode— keypress 이벤트의 문자 코드location— 표준/왼쪽/오른쪽/넘패드ctrlKey,shiftKey,altKey,metaKey— 수정자 상태
국제 키보드나 특수 문자를 다루거나, 기존 코드가 특정 키를 인식하지 못하는 이유를 찾을 때 특히 유용합니다. console.log를 추가하고 새로고침하는 대신 도구를 열고 키를 누르기만 하면 됩니다.
빠른 참고표
| 속성 | 사용 시기 | Deprecated? |
|---|---|---|
key | 문자 또는 동작 이름 | 아니요 |
code | 물리적 키 위치 | 아니요 |
keyCode | 없음 — 레거시 전용 | 예 |
which | 없음 — 레거시 전용 | 예 |
charCode | 없음 — 레거시 전용 | 예 |
ctrlKey | Ctrl 눌림 확인 | 아니요 |
shiftKey | Shift 눌림 확인 | 아니요 |
altKey | Alt/Option 눌림 확인 | 아니요 |
metaKey | Cmd/Win 키 눌림 확인 | 아니요 |
핵심 사고방식: 먼저 e.key를 사용하세요. 키의 의미를 알려줍니다. 물리적 위치가 중요할 때만(게임 컨트롤, 레이아웃 독립 단축키) e.code를 사용하세요. 새 코드에서는 keyCode, which, charCode를 절대 사용하지 마세요.
이것만 제대로 이해하면 브라우저, 키보드 레이아웃, 운영체제에 걸쳐 일관된 키보드 인터랙션을 만들 수 있습니다. 그리고 Firefox 사용자로부터 버그 리포트를 받는 일도 사라질 겁니다.