들어가며

Ctrl+C와 Ctrl+V는 가장 유명한 단축키라고 할 수 있습니다. 바로 복사 붙여넣기를 할 때 사용하는 단축키이죠. 하지만 이러한 브라우저 기본 단축키 외에, 사용자가 정의한 커스텀 단축키를 웹 내에서 구현하려면 어떻게 해야 할까요?
이 글에서는 단순히 커스텀 단축키를 '작동'시키는 것을 넘어, React의 Hook을 활용하여 성능 최적화에 초점을 맞춰 구현하는 방법을 설명합니다.
예시 웹 페이지 링크
https://jaeyeong04.github.io/multi-key-implementation/
React App
jaeyeong04.github.io
위 링크에 들어가면 Shift + j라는 커스텀 단축키에 추가된 동작과 Ctrl + s라는 기존 단축키에 다른 동작을 하는 걸 볼 수 있습니다.
이런 경우 키가 눌린 순간에 딱 한 번 동작하는 "단발성" 이벤트입니다 (alert 창 띄우기).
구현 전략
- useEffect를 사용하여 "keydown" event listener 등록 및 해제
- useCallback을 사용하여 메모리 사용 최적화
- event.preventDefault()를 사용하여 브라우저 기본 동작 방지
코드 분석
위 웹에서 단축키 설정을 하는 부분을 가져왔습니다.
const [count, setCount] = useState(0);
const handleKeyDown = useCallback((event: KeyboardEvent) => {
if (event.ctrlKey && event.key === "s") {
event.preventDefault(); //브라우저의 기본 동작 방지
alert("Ctrl + S 단축키가 눌렸습니다!");
}
if (event.shiftKey && event.key === "J") {
event.preventDefault(); //브라우저의 기본 동작 방지
alert("Shift + J 단축키가 눌렸습니다!");
}
if (event.ctrlKey && event.key === "ArrowUp") {
event.preventDefault();
setCount((prevCount) => prevCount + 1);
}
}, []);
//키보드 이벤트 리스너 등록 및 해제
useEffect(() => {
//키보드 keydown 이벤트 리스너 등록 및 해제
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [handleKeyDown]);
1. useCallback을 활용한 handleKeyDown 함수 선언
일단 키를 눌렀을 때 불러올 함수를 선언합니다. 이 함수 내부에 if문을 사용하여 단축키 조합에 따른 동작을 넣을 수 있습니다.
아래 링크에서 KeyboardEvent의 instance(위 코드에 있는 ctrlKey, shiftKey, key 외에 더 있음)들 확인이 가능합니다.
https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#instance_properties
KeyboardEvent - Web APIs | MDN
The KeyboardEvent interface defines the following constants. The following constants identify which part of the keyboard the key event originates from. They are accessed as KeyboardEvent.DOM_KEY_LOCATION_STANDARD and so forth. Keyboard location identifiers
developer.mozilla.org
또 아래 링크에서는 KeyboardEvent.key instance를 사용할 때 key의 값들을 확인할 수 있습니다.
https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
Key values for keyboard events - Web APIs | MDN
Values of key which have special meanings other than identifying a specific key or character. KeyboardEvent.key Value Description Virtual Keycode Windows Mac Linux Android "Unidentified" The user agent wasn't able to map the event's virtual keycode to a sp
developer.mozilla.org
위에 있는 두 링크를 참고해서 다양한 조합의 단축키를 설정할 수 있습니다 (state 업데이트도 가능).
그렇다면 여기서 궁금한 점은 왜 굳이 useCallback을 사용해서 이 함수를 작성해야 하는지입니다.
리액트 공식 문서에 따르면, "useCallback은 리렌더링 간에 함수 정의를 캐싱해 주는 React Hook입니다".
useCallback 없이 함수를 아래와 같이 정의한다면, 리렌더링될 때마다 같은 역할을 수행하는 함수가 의미 없이 재선언됩니다. 이는 리액트 성능적으로 비효율적입니다.
//handleKeyDown이 일반함수로 선언됐을 때
const handleKeyDown = (event: KeyboardEvent) => {
//중략
};
아래 표에 useCallback을 사용하지 않았을 때 생기는 성능 문제를 정리했습니다.
| 메모리 낭비 | 리렌더링마다 함수가 의미없이 재선언됨 -> 리렌더링마다 새로운 메모리 주소를 가진 새로운 함수 객체가 생김 -> 리렌더링에 상관없이 어짜피 같은 역할을 수행하는데 계속해서 메모리 차지 |
| useEffect의 의미없는 재호출 | 만약에 useCallback으로 선언되지않은 함수가 useEffect의 의존성 배열에 들어가 있다면, 리렌더링 시에 함수 재선언 -> useEffect 내부 코드 재실행 (이 예시에선 불필요하게 eventListener의 해제와 등록이 반복됨) |
2. useEffect로 eventListener 등록 및 해제
다음은 useEffect를 사용한 eventListener 등록 및 해제 코드입니다. 여기서 중요한 점은 의존성 배열에 useCallback으로 선언된 handleKeyDown함수를 넣어서 성능 최적화를 한다는 점입니다.
//키보드 이벤트 리스너 등록 및 해제
useEffect(() => {
//키보드 keydown 이벤트 리스너 등록 및 해제
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [handleKeyDown]);
- addEventListener 함수의 첫 번째 인자는 "이벤트 타입"이고, 두 번째 인자는 해당 이벤트 발생 시에 실행할 콜백 함수입니다. 이 예시의 경우, keydown (키보드가 눌렸을 때) 이벤트 시에, handleKeyDown (단축키에 따른 동작) 함수가 실행됩니다.
- useEffect의 기본동작에 의해서 eventListener는 컴포넌트 첫 렌더링 후 및 의존성배열 멤버가 변할 때 등록되고, return 안에 들어가는 콜백 함수(eventListener 해제)는 컴포넌트 언마운트 or 다음 이벤트 발생 전에 실행됩니다. (return하지 않는다면 eventListener가 해제되지 않아서 메모리 누수 발생 가능성 有)
- 이 예시에서는 handleKeyDown은 의존성 배열이 비어 있는 useCallback으로 선언되었습니다. 즉, handleKeyDown은 절대 재선언되지 않기 때문에 이 useEffect의 의존성배열은 변하지 않습니다. 따라서 해당 useEffect에서 eventListener의 등록은 웹 실행 시 한 번, 해제는 웹 종료 시 한 번 실행됩니다 (성능 최적화!)
3. event.preventDefault()
이 코드를 추가함으로써 Ctrl + s 등 브라우저 기본 단축키 event를 무시할 수 있습니다.
깃허브 레포 주소
https://github.com/jaeyeong04/multi-key-implementation
GitHub - jaeyeong04/multi-key-implementation
Contribute to jaeyeong04/multi-key-implementation development by creating an account on GitHub.
github.com
마치며
이렇게 해서 리액트로 웹 커스텀 단축키를 구현해 보았습니다. 코드 하나하나 보며 성능 최적화에 집중해서 설명을 해보았는데요.
저희도 쓸모없는 반복작업을 싫어하는 것처럼, useCallback과 useEffect를 적절히 사용하여 불필요한 함수 재선언 및 코드 실행을 방지하여 최적화했다고 생각하면 될 것 같습니다.

사실 이 글은 계획에 없었습니다. 원래는 개인 토이 프로젝트로 만들고 있는 웹 게임에서 캐릭터 이동(상하좌우 + 대각선) 구현에 대한 글을 작성할 예정이었습니다. 하지만, 이런 캐릭터 이동은 "지속적인 동작"이 필요한 경우로 단축키 구현에서의 "단발성 동작"과 구현이 조금 다릅니다. 따라서 계획에도 없던 이 글로 키보드를 눌렀을 때 "단발성 동작" 구현에 대해서 먼저 정리하고, 다음 글에서 "지속적인 동작" 구현에 관해 설명하도록 하겠습니다.
(링크는 글 작성 후에 남길게요,,,)