단축키 관련 라이브러리
최근에 같은 팀원으로부터 많은 단축키를 사용하는 상황에서 어떻게 하는게 좋을 지 질문을 받았었는데, 선뜻 답이 떠오르지 않아 두루뭉술하게 대답을 했었습니다. 이번 기회에 어떤 라이브러리가 있는지 알아보고 어떤 구조가 좋을지 알아보고자 합니다.
단축키 라이브러리
hotkeys-js
- GitHub : https://github.com/jaywcjlove/hotkeys-js
- 단축키 지정을 위한 js 라이브러리.
example code
- hotkeys를 사용하여 윈도우에서 f5 동작을 막고 등록한 콜백함수를 동작시키는 함수.
import hotkeys from "hotkeys-js";
hotkeys("f5", function (event, handler) {
event.preventDefault();
alert("you pressed F5!");
});
- 여러 키를 조합한 경우를 처리하는 예제 코드
hotkeys("ctrl+a,ctrl+b,r,f", function (event, handler) {
switch (handler.key) {
case "ctrl+a":
alert("you pressed ctrl+a!");
break;
case "ctrl+b":
alert("you pressed ctrl+b!");
break;
case "r":
alert("you pressed r!");
break;
case "f":
alert("you pressed f!");
break;
default:
alert(event);
}
});
**react-hot-keys**
- GitHub : https://www.npmjs.com/package/react-hot-keys
- 명시적으로 컴포넌트 내에서 키보드 단축키를 사용하기 위한 react 라이브러리
- https://github.com/jaywcjlove/hotkeys-js의 fork버전에서 시작
example code
- react class component를 사용한 예제
<Hotkeys>
hoc로 wrapping하고keyName
으로 모니터링할 단축키 이름 지정
export default class HotkeysDemo extends React.Component {
constructor(props) {
super(props);
this.state = {
output: "Hello, I am a component that listens to keydown and keyup of a",
};
}
onKeyUp(keyName, e, handle) {
console.log("test:onKeyUp", e, handle);
this.setState({
output: `onKeyUp ${keyName}`,
});
}
onKeyDown(keyName, e, handle) {
console.log("test:onKeyDown", keyName, e, handle);
this.setState({
output: `onKeyDown ${keyName}`,
});
}
render() {
return (
<Hotkeys
keyName="shift+a,alt+s"
onKeyDown={this.onKeyDown.bind(this)}
onKeyUp={this.onKeyUp.bind(this)}
>
<div style=>{this.state.output}</div>
</Hotkeys>
);
}
}
react-hotkeys-hook:
- GitHub: https://github.com/JohannesKlauss/react-hotkeys-hook
- react-hot-keys의 react hook 버전
example code
- 컴포넌트에 ctrl+k 키를 바인딩하는 예제
import { useHotkeys } from "react-hotkeys-hook";
export const ExampleComponent = () => {
const [count, setCount] = useState(0);
useHotkeys("ctrl+k", () => setCount(count + 1), [count]);
return <p>Pressed {count} times.</p>;
};
focus trap
- 특정 DOM에 foucs를 가두는 라이브러리
- 라이브러리 : https://github.com/focus-trap/focus-trap-react
- 특이사항 : FocusTrap hoc의 자식 컴포넌트는 ref를 가질 수 있어야함.
example code
- modal 컴포넌트에 hoc로 wrapping하여 포커스를 가두는 예
<FocusTrap>
<div id="modal-dialog" className="modal">
<button>Ok</button>
<button>Cancel</button>
</div>
</FocusTrap>
동일한 단축키가 컴포넌트별로 다른 동작을 해야할때에 대한 고민
단축키 관련 기능을 개발하다보면 동일한 단축키가 상황에 따라 다르게 사용되어야하는 경우가 종종 있습니다. 🤔
- 기본 화면에서 F를 눌렀을때 캐릭터 F키에 할당된 스킬을 사용해야합니다.
- 아이템 창이 열려 있는 상태에 F키를 눌렀을 때는 아이템 찾기 기능의 필터를 오픈해주어야합니다.
웹 브라우저에서 이를 구현하기에는 고민되는 부분이 있습니다. document.addEventListener
로 키 바인딩을 하고나면 두 key event가 동시에 트리거 됩니다.
실제 코드로 예를 들어보면 아래 두 컴포넌트를 마운트하게 되면 keydown할 때 마다 각 컴포넌트에 추가된 리스너가 각각 동작하게 됩니다.
//BaseComponent.tsx
const BaseComponent = () => {
useEffect(() => {
document.addEventListener("keydown", () => {
console.log("key press in Base");
});
return () => {
console.log("unmounted");
document.removeEventListener("keydown", () => {
console.log("key press in Base");
});
};
}, []);
return <div>BaseComponent</div>;
};
//Modal.tsx
const Modal = ({ title, content, onClose, onConfirm }: ModalProps) => {
useEffect(() => {
document.addEventListener("keydown", (e) => {
console.log("key press in modal");
});
return () => {
document.removeEventListener("keydown", (e) => {
console.log("key press in modal");
});
};
});
return (
<ModalWrapper>
<ModalInner>
<ModalTitle>{title}</ModalTitle>
<ModalContent>{content}</ModalContent>
<ModalButtonWrapper>
<ModalButton onClick={onClose}>취소</ModalButton>
<ModalButton onClick={onConfirm}>확인</ModalButton>
</ModalButtonWrapper>
</ModalInner>
</ModalWrapper>
);
};
바로 이렇게요
13:06:17.276 Modal.tsx:65 key press in modal
13:06:17.276 Base.tsx:6 key press in Base
13:06:17.277 Modal.tsx:65 key press in modal
13:06:17.867 Base.tsx:6 key press in Base
13:06:17.867 Modal.tsx:65 key press in modal
13:06:17.868 Base.tsx:6 key press in Base
13:06:17.868 Modal.tsx:65
이때 react-hotkeys-hook 에서 제공하는 Scoping hotkeys to components 를 사용하여 해당 컴포넌트가 포커스되었을 때만 등록한 키가 동작하도록 할 수 있습니다.
useHotkeys에서 반환되는 ref를 포커스 대상 컴포넌트에 넘겨주면 해당 컴포넌트가 포커스 되었는지를 확인하여 핫키가 동작할 수 있게 만들어 줍니다. 예제 링크
function ScopedHotkey() {
const [count, setCount] = useState(0);
const ref = useHotkeys("shift+a", () =>
setCount((prevCount) => prevCount + 1)
);
return (
<>
<p>
The count is {count}. Click anywhere except for the button to disable
the hotkey.
</p>
<button ref={ref}>Click me to enable the hotkey</button>
</>
);
}
function SecondScopedHotkey() {
const [count, setCount] = useState(0);
const ref = useHotkeys("shift+a", () =>
setCount((prevCount) => prevCount + 1)
);
return (
<>
<p>
The count is {count}. Click anywhere except for the button to disable
the hotkey.
</p>
<button ref={ref}>Click me to enable the hotkey</button>
</>
);
}
render(
<div>
<ScopedHotkey />
<hr />
<SecondScopedHotkey />
</div>
);
경우에 따라 tabIndex에 -1을 부여하여 tab키로 접근은 불가능하지만 포커스는 가능하게 만들어 주어야하는 경우도 있습니다. (ex : 모달)
참고 자료
**React 앱에 단축키 적용하기** https://velog.io/@juno7803/React-hotkeys
React-hotkeys-hook document https://react-hotkeys-hook.vercel.app/
tabIndex 관련 내용 : https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/tabindex
Leave a comment