지금까지 데이터 상의 label과 실제 키보드 입력값이 다른 경우를 새로운 specialKeyMap 객체에 저장하고, reduce( ) 를 통해 합쳐 처리했다.

 

하지만 코드 가독성이 떨어지고, 실제 label이 매겨진 버튼을 추적하는 것을 더욱 편하게 하기 위해 데이터를 조금 다듬었다.

 

interface Key {
  code: string;
  label: string;
  audio: AudioType;
  extraClass?: string;
}

const mainKeys: Key[][] = [
  [
    { code: "`", label: "~", audio: "A" },
    { code: "1", label: "1", audio: "A" },
    { code: "2", label: "2", audio: "A" },
    
    ...
  ]
]

 

code 라는 새로운 키를 생성하고, 여기에 실제 입력되는 값을 부여했다.

 

이제 할 일은

  1. 눌린 키의 정보를 받고,
  2. 이 정보를 데이터의 code와 같은지 비교하고,
  3. 이 code에 부여된 오디오를 연결하여
  4. 해당되는 <button>에 eventHandler를 추가한다.

 

 

이전에 작성해둔 오디오 재생 함수는 아래와 같다.

  // 오디오 객체 배열 생성 함수
  const createAudioArray = (audioSource: string, count: number) =>
    Array.from({ length: count }, () => new Audio(audioSource));

  // 오디오 맵 초기화
  const audioMap = useRef<{ [audioFileName in AudioType]: HTMLAudioElement[] }>(
    {
      A: createAudioArray(AAudio, 10),
      B: createAudioArray(BAudio, 10),
      C: createAudioArray(CAudio, 10),
      D: createAudioArray(DAudio, 10),
      E: createAudioArray(EAudio, 10),
    }
  );

  // 컴포넌트 마운트 시 오디오 파일 로드
  useEffect(() => {
    Object.values(audioMap.current).forEach((audioArray) =>
      audioArray.forEach((audio) => audio.load())
    );
  }, []);

  // 현재 재생할 복제본의 인덱스 저장
  const audioIndex = useRef<{ [audioFileName in AudioType]: number }>({
    A: 0,
    B: 0,
    C: 0,
    D: 0,
    E: 0,
  });

  // 오디오 재생
  const playHandler = (audio: AudioType) => {
    const audioArray = audioMap.current[audio];
    const index = audioIndex.current[audio];

    // 현재 인덱스의 오디오 객체 재생
    const audioElement = audioArray[index];
    audioElement.currentTime = 0;
    audioElement.play();

    // 인덱스 순환
    audioIndex.current[audio] = (index + 1) % audioArray.length;
  };

 

 

 

이제 새로운 useEffect문을 통해 작업해보자.

먼저 code와 audioFile을 나란히 찾을 수 있도록 mainKeys 데이터를 새로운 객체로 변환한다.

useEffect(() => {
    // code, audio가 매핑된 객체 생성
    const keyMap = new Map<string, AudioType>(
      mainKeys.flat().map((key) => [key.code, key.audio])
    );

	// code와 일치하는 오디오 파일 찾기
    function findAudioFile(audioFile: string) {
      return keyMap.get(audioFile);
    }
})

 

이제 keyDown과 keyUp에 대한 핸들러 함수를 작성한다.

	//눌렀을 때
    const handleKeyDown = (event: KeyboardEvent) => {
      const pressedKey = event.key.toLowerCase();
      const audioFile = findAudioFile(pressedKey);

      if (audioFile) { // 매칭되는 오디오 파일이 있다면
        event.preventDefault();	// 원래 키의 기능을 막고
        playHandler(audioFile); // 오디오를 재생한다

        // 해당하는 버튼 요소에 test 클래스 추가
        const buttonElement = document.querySelector(
          `button[data-code="${pressedKey}"]`
        );

        if (buttonElement) {
          buttonElement.classList.add("test");
          console.log(buttonElement, "추가");
        }
      }
    };

    //뗄 때
    const handleKeyUp = (event: KeyboardEvent) => {
      const pressedKey = event.key.toLowerCase();
      
      //추가했던 클래스 제거
      const buttonElement = document.querySelector(
        `button[data-code="${pressedKey}"]`
      );
      
      if (buttonElement) {
        buttonElement.classList.remove("test");
        console.log(buttonElement, "제거");
      }
    };

 


Data Attribute 활용

위에서 클래스를 동적으로 추가하면서 나온 data- 문법은 HTML에서 데이터 속성 (data attribute)을 사용하는 방식이다.

HTML요소에 추가적인 정보를 저장할 때 사용할 수 있다.

쉽게 말해 연결될 지점에 링크를 거는 것이다.

button 태그에 붙어있는 data-code의 값이 눌린 키와 같은지 확인하는 것이다.

활용법은 아래와 같다.

data- 로 작성하는 것이 일반적이고, 뒤의 키워드는 자유롭게 지정할 수 있다.

    const buttonElement = document.querySelector(
        `button[data-code="${pressedKey}"]`	// 이 값과
    );

	//후략

return (
    <button
        data-code={label.code.toLowerCase()} // 이 값이 같다면 querySelector에 선택되는 방식
    >
        <span>내용</span>
    </button>
)}

 

 

 

이제 핸들러 함수가 트리거 되는 경우를 작성한다.

    document.addEventListener("keydown", handleKeyDown); //키를 누른 경우 handleKeyDown 트리거
    document.addEventListener("keyup", handleKeyUp); //키를 뗀 경우 handleKeyUp 트리거

 

마지막으로 클린업 함수를 추가한다.

    return () => { // 클린업 함수
      document.removeEventListener("keydown", handleKeyDown);
      document.removeEventListener("keyup", handleKeyUp);
    };

 

아래는 전체코드이다.

  useEffect(() => {
    // code, audio가 매핑된 객체
    const keyMap = new Map<string, AudioType>(
      mainKeys.flat().map((key) => [key.code, key.audio])
    );

	// 오디오 파일 찾는 함수
    function findAudioFile(audioFile: string) {
      return keyMap.get(audioFile);
    }

    //눌렀을 때
    const handleKeyDown = (event: KeyboardEvent) => {
      const pressedKey = event.key.toLowerCase(); // 누른 키 받기
      if (pressedKey === "capslock" && !event.getModifierState("CapsLock")) return;
      const audioFile = findAudioFile(pressedKey);

      if (audioFile) {
        event.preventDefault();
        playHandler(audioFile);

        // data-로 연결된 버튼 요소에 test 클래스 추가
        const buttonElement = document.querySelector(
          `button[data-code="${pressedKey}"]`
        );
		
        // 존재한다면 클래스 추가
        if (buttonElement) {
          buttonElement.classList.add("test");
          console.log(buttonElement, "추가"); //디버깅용 콘솔
        }
      }
    };

    //뗄 때
    const handleKeyUp = (event: KeyboardEvent) => {
      const pressedKey = event.key.toLowerCase(); // 누른 키 받기
      //클래스 제거
      const buttonElement = document.querySelector(
        `button[data-code="${pressedKey}"]`
      );
      if (buttonElement) {
        buttonElement.classList.remove("test");
        console.log(buttonElement, "제거"); //디버깅용 콘솔
      }
    };

    document.addEventListener("keydown", handleKeyDown);
    document.addEventListener("keyup", handleKeyUp);

    return () => { // 클린업 함수
      document.removeEventListener("keydown", handleKeyDown);
      document.removeEventListener("keyup", handleKeyUp);
    };
  }, []);

 

 

나의 경우, 위 코드에 상위 컴포넌트에서 타이핑 모드를 설정한 경우(typingOn = true)에만 키보드 이벤트가 활성화 되도록 했다.

useEffect문 시작부분에 예외처리와 typingOn 상태 변화가 있을 때마다 작동하도록 의존성 배열에 추가했다.

  useEffect(() => {
    if (!typingOn) return; // 타이핑모드가 아닌 경우 실행하지 않음

	...
    
   }, [typingOn]); // 타이핑모드 변경사항이 있을 때마다 동작

 

 

이제 클래스가 각 키에 동적으로 추가되는 것을 확인할 수 있다.

 

 

 

 

 

+ Recent posts