지금까지 데이터 상의 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 라는 새로운 키를 생성하고, 여기에 실제 입력되는 값을 부여했다.
이제 할 일은
- 눌린 키의 정보를 받고,
- 이 정보를 데이터의 code와 같은지 비교하고,
- 이 code에 부여된 오디오를 연결하여
- 해당되는 <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]); // 타이핑모드 변경사항이 있을 때마다 동작
이제 클래스가 각 키에 동적으로 추가되는 것을 확인할 수 있다.
'React.ts' 카테고리의 다른 글
리액트 식물 검색 서비스 - 다중 API 호출과 매핑, Promise.all (1) | 2024.11.14 |
---|---|
리액트 가상 키보드 만들기 - 키보드 매핑 (오류 해결) (1) | 2024.11.02 |
리액트 가상 키보드 만들기 - Audio 객체 (추가) (0) | 2024.10.28 |
리액트 가상 키보드 만들기 - 키보드 매핑하기 1 (0) | 2024.10.28 |
리액트 가상 키보드 만들기 - 오디오 재생하기, Audio 객체 (0) | 2024.10.25 |