이제 각 키를 실제 키보드로 입력했을 때 지정한 오디오가 출력되도록 만들어 보자.
초기 아이디어
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>
);
}
'React.ts' 카테고리의 다른 글
리액트 가상 키보드 만들기 - 키보드 매핑하기 2, 키보드 eventHandler, 클래스 동적으로 추가하기, HTML data attribute 활용 (0) | 2024.10.31 |
---|---|
리액트 가상 키보드 만들기 - Audio 객체 (추가) (0) | 2024.10.28 |
리액트 가상 키보드 만들기 - 오디오 재생하기, Audio 객체 (0) | 2024.10.25 |
리액트 가상 키보드 만들기 - 레이아웃 및 디자인, 배열과 map을 활용한 키 매핑 (3) | 2024.10.25 |
Firebase 정보 새로고침 시 초기화 되는 오류 (1) | 2024.09.19 |