단축키 관련 라이브러리
최근에 같은 팀원으로부터 많은 단축키를 사용하는 상황에서 어떻게 하는게 좋을 지 질문을 받았었는데, 선뜻 답이 떠오르지 않아 두루뭉술하게 대답을 했었습니다. 이번 기회에 어떤 라이브러리가 있는지 알아보고 어떤 구조가 좋을지 알아보고자 합니다.
단축키 라이브러리
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