이제 각 키를 실제 키보드로 입력했을 때 지정한 오디오가 출력되도록 만들어 보자.

 

 

 

초기 아이디어

type AudioType = "A" | "B" | "C" | "D" | "E";

useEffect(() => {
  const handleKeyDown = (event: KeyboardEvent) => {
    const keyMap: { [key: string]: AudioType } = {	//문자열인 key: 값은 AudioType 중 하나
      a: "A",	// 우선 5개 키만 대충 지정한다
      b: "B",
      c: "C",
      d: "D",
      e: "E"
    };

    const audioType = keyMap[event.key];
    if (audioType) {	// keyMap에 누른 키가 존재하는지 확인
      playHandler(audioType);	// 있다면 해당 오디오 재생
    }
  };

  // window 객체에 이벤트 리스너 추가.
  // 각 button 요소를 추적하는 것이 아니라, 눌린 키가 keyMap객체에 있는지 확인하는 방식.
  window.addEventListener("keydown", handleKeyDown);

  return () => { // 클린업
    window.removeEventListener("keydown", handleKeyDown);
  };
}, []);

 

모든 키의 원래 이름(입력 시 브라우저가 해당 키라는 것을 인식하는 원본 네이밍)은 lowerCase로 작성되어야 한다.

따라서 event.key에 .toLowerCase() 를 추가해주고, 각 키를 다시 매핑해준다.

 

 

수정할 점 : mainKeys 배열 불러오기, 소문자로 변환하기

type AudioType = "A" | "B" | "C" | "D" | "E";

useEffect(() => {
  // mainKeys 배열에서 키보드 레이블과 오디오 타입을 짝지어주는 객체 생성하기
  const keyMap = mainKeys.flat().reduce((accumulator, key) => {
    accumulator[key.label.toLowerCase()] = key.audio;
    return accumulator;
  }, {} as { [key: string]: AudioType });

  const handleKeyDown = (event: KeyboardEvent) => {	// 키보드 이벤트 핸들러 생성
    const audioType = keyMap[event.key.toLowerCase()]; // 눌린 키의 레이블과 매칭되는 오디오 타입 찾기
    if (audioType) { // 방금 누른 키가 매칭되는 오디오의 키와 같다면
      playHandler(audioType); // 오디오 재생
    }
  };

  window.addEventListener("keydown", handleKeyDown); //handleKeyDown이라는 함수를 키보드 입력 시 실행

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

 

 

여기서 문제가 또 발생한다.

데이터 배열의 label에 실제 이름과 다른 키에 대해서는 매핑이 되지 않아 소리가 재생되지 않는다.

또한 caps lock 이나 ctrl 등 실제 브라우저에서 역할이 있는(?) 키들은 오디오를 재생하지 않고 자신의 기능을 우선 수행한다.

이 부분을 event.preventDefault()로 방지하는 기능까지 추가해보자.

 

수정할 점 : 이름 다른 키 수정하기, 각 키의 원기능 방지하기

useEffect(() => {

    const specialKeyMap: { [key: string]: AudioType } = {
      // 데이터의 label과 실제 값이 다른 키를 새로운 객체에 정리
      " ": "E", // 스페이스바
      escape: "B", // esc
      backspace: "A",
      enter: "B",
      tab: "D",
      capslock: "D",
      arrowup: "A",
      arrowdown: "A",
      arrowleft: "A",
      arrowright: "A",
      control: "C", // ctrl
      meta: "C", // Windows 키 (macOS에서는 Command 키)
      alt: "C",
      fn: "C", 
      "`": "A", // 백틱
      "=": "A",
    };

    // 실제 키 이름과 맞추기 ---> mainKeys에 specialKeyMap를 더하기
    const keyMap = mainKeys.flat().reduce((accumulator, currentValue) => {
      const keyLabel = currentValue.label.toLowerCase(); // 각 키의 실제 이름
      accumulator[keyLabel] = currentValue.audio;
      return accumulator;
    }, specialKeyMap); // 초기값으로 지정했기 때문에 accumulator는 specialKeyMap를 갖고 출발

    const handleKeyDown = (event: KeyboardEvent) => {
      const keyName = event.key.toLowerCase();  // 현재 눌린 키
      const audioType = keyMap[keyName];

      if (audioType) {
        event.preventDefault(); // 원래 이 키의 기능 방지
        playHandler(audioType);
      }
    };

    document.addEventListener("keydown", handleKeyDown);

    return () => {
      document.removeEventListener("keydown", handleKeyDown);
    };
  }, []);

 

이번에야말로 모든 키에 대해 정상적으로 오디오가 출력되는 것을 확인할 수 있다.

 

 

 

전체 코드

import { useRef, useEffect, useState } from "react";
import "../styles/styles.css";
import mainKeys from "../assets/data/keyArray";
import { functionKeys } from "../assets/data/keyArray";
import { AudioType } from "../assets/data/keyArray";
import AAudio from "../assets/audio/A.mp3";
import BAudio from "../assets/audio/B.mp3";
import CAudio from "../assets/audio/C.mp3";
import DAudio from "../assets/audio/D.mp3";
import EAudio from "../assets/audio/E.mp3";

interface KeysProps {
  onLightToggle: () => void;
  typingOn: boolean;
  onClickOn: boolean;
  mouseEnterOn: boolean;
  onKnobChange: (value: number) => void;
  onLightOn: boolean;
}

export default function Keys({ onLightToggle, typingOn, onClickOn, mouseEnterOn, onKnobChange, onLightOn }: KeysProps) {
  const [knobValue, setKnobValue] = useState<number>(0);

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

  // 각 오디오 타입별로 미리 10개의 오디오 객체 생성
  const audioMap = useRef<{ [key in AudioType]: HTMLAudioElement[] }>({
    A: createAudioArray(AAudio, 10),
    B: createAudioArray(BAudio, 10),
    C: createAudioArray(CAudio, 10),
    D: createAudioArray(DAudio, 10),
    E: createAudioArray(EAudio, 10),
  });

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

  // 미리 오디오 파일 로드
  useEffect(() => {
    Object.values(audioMap.current).forEach((audioArray) =>
      audioArray.forEach((audio) => audio.load())
    );
  }, []);

  // 오디오 재생
  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(() => {
    if (!typingOn) return; // 타이핑 모드에서만 실행

    const specialKeyMap: { [key: string]: AudioType } = {
      //데이터의 label과 실제 값이 다른 키
      " ": "E",
      escape: "B",
      backspace: "A",
      enter: "B",
	  ...
    };

    //실제 키 이름과 맞추기 ---> mainKeys에 specialKeyMap를 더하기
    const keyMap = mainKeys.flat().reduce((accumulator, currentValue) => {
      const keyLabel = currentValue.label.toLowerCase(); //소문자만 인식됨. 각 키의 실제 이름
      accumulator[keyLabel] = currentValue.audio;
      return accumulator;
    }, specialKeyMap); // 초기값으로 지정했기 때문에 accumulator는 specialKeyMap를 갖고 출발

    const handleKeyDown = (event: KeyboardEvent) => {
      const keyName = event.key.toLowerCase();  //현재 눌린 키
      const audioType = keyMap[keyName];

      if (audioType) {
        event.preventDefault(); //원래 이 키의 기능 방지
        playHandler(audioType);
      }
    };

    document.addEventListener("keydown", handleKeyDown);

    return () => {
      document.removeEventListener("keydown", handleKeyDown);
    };
  }, [typingOn]); //상위 컴포넌트에서 타이핑모드로 설정한 경우

  const handleKnobChange = (e: { value: number }) => {
    setKnobValue(e.value);
    onKnobChange(e.value);
  };

  return (
    <div className="key-container">
      <div className="quad-key-row">
        <button
          className={`esc ${!typingOn ? "hoverEffect" : ""}`}
        > // 타이핑 모드에서는 hover 스타일 제거
          <span className="key-span">ESC</span>
        </button>

        <div className="quad-container ">
          {functionKeys.map((row, rowIndex) => (
            <div className="quad-row" key={rowIndex}>
              {row.map((key) => (
                <button
                  className={`quad-keys ${!typingOn ? "hoverEffect" : ""}`}
                  key={key}
                >
                  <span className="key-span">{key}</span>
                </button>
              ))}
            </div>
          ))}
        </div>

        <button className="wheel">
          <span className="key-span">LED</span>
        </button>
      </div>

      <div className="key-main-container">
        {mainKeys.map((row, rowIndex) => (
          <div className="key-main-row" key={rowIndex}>
            {row.map((label, labelIndex) => (
                className={`main-keys ${label.extraClass || ""} ${ !typingOn ? "hoverEffect" : ""}`}
                key={labelIndex}								
              >
                <span className="key-span">{label.label}</span>
              </button>
            ))}
          </div>
        ))}
      </div>
    </div>
  );
}

 

 

 

 

+ Recent posts