키보드 데모 테스트
keydown과 keyup은 언제, 몇 번 발생할까요? 아래 데모를 클릭한 뒤 방향키를 눌러보세요. 키를 꾹 누르고 있으면 어떤 일이 생기는지도 확인해보세요.
데모 · 방향키를 눌러보세요
↑←↓→
- 이벤트가 여기에 표시돼요
코드 보기코드 접기KeyboardDemo.svelte
<script lang="ts">
import { SvelteSet } from "svelte/reactivity";
import DemoFrame from "@/demos/DemoFrame.svelte";
import { roughDecor } from "@/lib/rough-action";
import { ROUGH_PRESETS } from "@/lib/rough";
type LogEntry = { id: number; type: "keydown" | "keyup"; key: string; repeat: boolean };
const KEYS = [
{ key: "ArrowUp", label: "↑", area: "up" },
{ key: "ArrowLeft", label: "←", area: "left" },
{ key: "ArrowDown", label: "↓", area: "down" },
{ key: "ArrowRight", label: "→", area: "right" },
];
const pressed = new SvelteSet<string>();
let log = $state<LogEntry[]>([]);
let nextId = 0;
function record(type: LogEntry["type"], event: KeyboardEvent) {
log = [{ id: nextId++, type, key: event.key, repeat: event.repeat }, ...log].slice(0, 4);
}
function onkeydown(event: KeyboardEvent) {
if (!KEYS.some((k) => k.key === event.key)) return;
event.preventDefault();
pressed.add(event.key);
record("keydown", event);
}
function onkeyup(event: KeyboardEvent) {
if (!KEYS.some((k) => k.key === event.key)) return;
pressed.delete(event.key);
record("keyup", event);
}
</script>
<DemoFrame hint="방향키를 눌러보세요" {onkeydown} {onkeyup} ondeactivate={() => pressed.clear()}>
{#snippet children({ active })}
<div class="body">
<div class="keys">
{#each KEYS as k (k.key)}
<span class="key" class:on={pressed.has(k.key)} style:grid-area={k.area}
><span class="key-shape" aria-hidden="true" use:roughDecor={{ kind: "box", seed: `key:${k.key}`, fill: true, radius: 9, ...ROUGH_PRESETS.chip }}></span
>{#if pressed.has(k.key)}
<!-- 눌린 동안은 화살표 대신 웃는 얼굴(^o^)이 된다. -->
<svg class="face" viewBox="0 0 18 16" aria-hidden="true">
<path d="M2 6.5 4.5 3.5 7 6.5M11 6.5 13.5 3.5 16 6.5" />
<path d="M7 9.5h4c0 2.6-.9 4-2 4s-2-1.4-2-4z" />
</svg>
<span class="sr-only">{k.label}</span>
{:else}
{k.label}
{/if}</span
>
{/each}
</div>
<ol class="log" aria-live="polite">
{#each log as entry (entry.id)}
<li><span class="type">{entry.type}</span> {entry.key}{entry.repeat ? " (repeat)" : ""}</li>
{:else}
<li class="empty">{active ? "방향키를 눌러보세요" : "이벤트가 여기에 표시돼요"}</li>
{/each}
</ol>
</div>
{/snippet}
</DemoFrame>
<style>
.body {
position: relative;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px 24px;
}
.keys {
display: grid;
grid-template-areas: ". up ." "left down right";
gap: 5px;
}
.key {
position: relative;
z-index: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 28px;
color: var(--key-text);
font-size: 14px;
}
/* 키 모양은 rough.js로 그린다. 키마다 모양이 다르다. */
.key-shape {
--rough-fill: var(--key-bg);
--rough-stroke: var(--key-border);
--rough-stroke-width: 1;
position: absolute;
inset: 0;
z-index: -1;
isolation: isolate;
}
.key-shape:not([data-rough-ready]) {
border: 1px solid var(--key-border);
border-radius: 9px;
background: var(--key-bg);
}
.key.on {
color: var(--key-on-text);
}
.face {
width: 18px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 1.4;
stroke-linecap: round;
stroke-linejoin: round;
animation: pop 0.18s ease-out;
}
/* 얼굴이 나올 때 살짝 튀어나온다. */
@keyframes pop {
from {
transform: scale(0.6);
}
60% {
transform: scale(1.15);
}
}
@media (prefers-reduced-motion: reduce) {
.face {
animation: none;
}
}
.key.on .key-shape {
--rough-fill: var(--key-on-bg);
--rough-stroke: var(--key-on-border);
}
.log {
flex: 1;
min-width: 150px;
white-space: nowrap;
margin: 0;
padding: 0;
list-style: none;
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.7;
color: var(--demo-label);
}
.type {
color: var(--demo-active);
}
.empty {
font-family: var(--font-sans);
}
</style>keydown은 여러 번, keyup은 한 번
키를 누르고 있으면 운영체제의 키 반복 설정에 따라 keydown이 계속 발생합니다. 이때 두 번째부터는 event.repeat가 true예요. 반대로 keyup은 손을 뗄 때 딱 한 번만 발생합니다.
게임처럼 “누르고 있는 동안” 상태가 필요하다면 keydown에서 키를 집합에 넣고 keyup에서 빼는 방식이 가장 단순합니다.
const pressed = new Set<string>();
element.addEventListener("keydown", (event) => {
pressed.add(event.key);
});
element.addEventListener("keyup", (event) => {
pressed.delete(event.key);
});왜 window가 아니라 요소에 거나
한 페이지에 데모가 여러 개 있으면 window에 건 이벤트는 모든 데모가 동시에 받습니다. 방향키나 스페이스로 페이지를 스크롤하는 것도 막혀버려요. 그래서 이 블로그의 데모는 이렇게 동작합니다.
- 데모를 클릭해서 포커스가 생겨야 키 입력을 받습니다.
- 포커스가 있을 때만
preventDefault()로 스크롤을 막습니다. Esc를 누르거나 바깥을 클릭하면 포커스가 풀립니다.
포커스가 어디 있는지 눈에 보여야 합니다. 데모 위쪽 문구가 “● 입력 중”으로 바뀌는 이유예요.
정리
keydown의 repeat를 확인하고, 이벤트는 포커스를 받을 수 있는 요소에 거는 게 좋습니다.