# 1. 내가 겪은 문제
아래 영상에서 처럼 아래 방향키를 한 번 눌렀을 때 뇌암-> 뇌경색 순서대로가 아닌,
뇌암 -> 뇌출혈로 이동되는 현상이다.
마치 2번 누른 것 처럼 중복된 이벤트가 발생되는 것 같다.
구글링 엄청 했는데 보통 한글 입력이 2번 발생하는 현상이라는 글이 많았다.
나는 한글입력은 올바르게 동작되고 있었기에 이 문제와 다른 문제라고 생각했다.
# 2. 문제 파악
사용한 코드는 아래와 같다.
상황 파악을 위해 콘솔을 찍어보는 방법밖에 없었다..
// SearchBox.jsx
import React, { useEffect, useState } from "react";
import searchApi from "../api/seachAPI";
import ResultList from "./ResultList";
import { debounce } from "../utils/debounce";
import { maxResult } from "../constant/maxResult";
export default function SearchBox() {
const [searchText, setSearchText] = useState("");
const [result, setResult] = useState([]);
const [selectedIndex, setSelectedIndex] = useState(0);
console.log("selectedIndex", selectedIndex); // selectedIndex 콘솔을 통해 확인
const handleChange = (e) => {
setSearchText(e.target.value);
debounce(() => {
handleSearch(e.target.value);
}, 400)();
};
const handleSearch = async (searchValue) => {
try {
if (searchValue.length !== 0) {
const res = await searchApi.searchKeyword(searchValue);
setResult(res.data.splice(0, maxResult));
}
} catch (e) {
console.error("error", e);
}
};
const handleKeyArrow = (e) => {
console.log("keyCode", e.keyCode); // 콘솔로 키코드 확인
if (e.key === "ArrowDown") {
const lastIndex = result.length - 1;
if (selectedIndex === lastIndex) {
return setSelectedIndex(0);
}
if (selectedIndex < lastIndex) {
setSelectedIndex((prev) => prev + 1);
}
}
if (e.key === "ArrowUp") {
const lastIndex = result.length - 1;
if (selectedIndex === 0) {
return setSelectedIndex(lastIndex);
} else {
setSelectedIndex((prev) => prev - 1);
}
}
};
return (
<div className="flex flex-col w-72 m-auto mt-10">
<div className="flex rounded-md overflow-hidden">
<input
className="p-3 flex-grow"
type="text"
value={searchText}
onChange={handleChange}
autoFocus
onKeyDown={handleKeyArrow}
/>
<button className="bg-blue-600 text-white p-3">검색</button>
</div>
{searchText.length > 0 ? (
<ResultList result={result} selectedIndex={selectedIndex} />
) : null}
</div>
);
}
selectedIndex와 handleKeyArrow 함수 안에서 e.keycode를 콘솔을 통해 확인해봤다.
selectedIndex는 0,1,2,3,4~ 이런식으로 모두 출력은 되나 육안으로 확인했을 때는 방향키 2번이 눌린 것 처럼 동작한다.
그러나!! e.keycode를 콘솔로 찍어보고 문제를 정확하게 파악하고 해결방법을 도출할 수 있었다.
아래 이미지에서 범위 [1]은 '뇌'를 입력할 때까지 찍힌 콘솔이며,
[2]는 방향키를 누르고 찍힌 콘솔이다.
콘솔 결과를 보면 [1]까지 keyCode 229가 3번 찍힌다. 한글 "ㄴ", "ㅗ", "ㅣ" 니까 3번 찍히는게 맞다.
그리고 아래 방향키를 눌렀을 때 [2]에서 keyCode 229가 다시 찍히는 것을 볼 수 있다.
결국 한글을 입력할 때 키보드 이벤트핸들러가 두 번 호출되는 현상을 똑같이 겪고 있던 것이였다.
# 3. 해결 방법
이런 문제는 크롬 브라우저에서 한글을 사용하는 경우에만 문제가 발생한다.
따라서 영어로 입력하면 키 이벤트가 중복으로 발생하지 않는다는 것도 알게되었다.
한글의 경우 자음과 모음의 조합으로 만들어지는 문자여서,
글자가 조합중인지 조힙이 끝난 상태인지를 알 수 없어 생기는 문제라고 한다.
유독 한글에서만 이러한 문제가 발생되는건 IME 때문이다.
IME는 영어가 아닌 한글, 일본어, 중국어와 같은 언어를 다양한 브라우저에서 지원하도록 언어를 변환시켜주기 위한 OS 단계의 어플리케이션을 말한다. 그러나 IME 과정에서 keydown 이벤트가 발생하면, OS와 브라우저에서 해당 이벤트를 모두 처리하기 때문에 keydown 이벤트가 중복으로 발생하게 되는 것이다.
즉, IME를 통해 한글, 일본어, 중국어 등을 변환하는 과정(composition)에서 keydown 이벤트는 OS 뿐만 아니라 브라우저에서도 처리되기 때문에 중복 발생된다.
이럴 때 사용할 수 있는 프로티가 있는데, isComposing이다.
Web API 스펙에서 키보드 이벤트 중 isComposing은 설명은 다음과 같다.
https://w3c.github.io/uievents/#dom-keyboardevent-iscomposing
isComposing, of type boolean, readonlytrue if the key event occurs as part of a composition session, i.e., after a compositionstart event and before the corresponding compositionend event.
( 번역: composition Session 중에 event가 발생하는지 여부를 불리언 값으로 반환한다.)
즉, 한글 등 비영어권 언어를 표현하는 과정에서 이 값을 참조하면 true값을 반환한다.
React에서 해결 방법
React에서는 isComposing 프로퍼티를 제공하지 않는다.
따라서 naitveEvent를 사용하거나 React에서 제공하는 composition event를 사용해야 한다.
React에서 제공하는 composition event은 리액트 문서에서 확인 가능하다.
https://react.dev/reference/react-dom/components/common#common-props
방법1 - naitveEvent 사용
e.native.isComposing이 true이면 함수를 리턴하게 함으로써 중복을 막을 수 있었다.
const handleKeyArrow = (e) => {
console.log("keyCode", e.keyCode);
if (e.key === "ArrowDown") {
if (e.nativeEvent.isComposing) return;
if (isComposing) return;
const lastIndex = result.length - 1;
if (selectedIndex === lastIndex) {
return setSelectedIndex(0);
}
if (selectedIndex < lastIndex) {
setSelectedIndex((prev) => prev + 1);
}
}
if (e.key === "ArrowUp") {
if (e.nativeEvent.isComposing) return;
const lastIndex = result.length - 1;
if (selectedIndex === 0) {
return setSelectedIndex(lastIndex);
} else {
setSelectedIndex((prev) => prev - 1);
}
}
};
방법2 - SyntheticEvent 사용 (isCompositionStart, isCompositionEnd)
React에서 제공하는 composition event를 사용하는 것이다.
isComposing 상태를 별도로 관리하고, isCompositionStart, isCompositionEnd 과 같은 컴포지션 이벤트 핸들러로 isComposing 상태를 true, false로 변경시켜주었다.
이렇게 하면 각각의 composition 이벤트가 발생할 때 상태의 값을 변경하면 한글 입력에 따른 keydown 이벤트의 중복을 막을 수 있다.
import React, { useEffect, useState } from "react";
import searchApi from "../api/seachAPI";
import ResultList from "./ResultList";
import { debounce } from "../utils/debounce";
import { maxResult } from "../constant/maxResult";
export default function SearchBox() {
const [isComposing, setIsComposing] = useState(false); // 컴포징을 확인하는 state 만들기
const handleKeyArrow = (e) => {
console.log("keyCode", e.keyCode);
if (e.key === "ArrowDown") {
if (isComposing) return;
const lastIndex = result.length - 1;
if (selectedIndex === lastIndex) {
return setSelectedIndex(0);
}
if (selectedIndex < lastIndex) {
setSelectedIndex((prev) => prev + 1);
}
}
if (e.key === "ArrowUp") {
const lastIndex = result.length - 1;
if (selectedIndex === 0) {
return setSelectedIndex(lastIndex);
} else {
setSelectedIndex((prev) => prev - 1);
}
}
};
return (
<div className="flex flex-col w-72 m-auto mt-10">
<div className="flex rounded-md overflow-hidden">
<input
className="p-3 flex-grow"
type="text"
value={searchText}
onChange={handleChange}
autoFocus
onKeyDown={handleKeyArrow}
onCompositionStart={() => setIsComposing(true)} //
onCompositionEnd={() => setIsComposing(false)} //
/>
<button className="bg-blue-600 text-white p-3">검색</button>
</div>
{searchText.length > 0 ? (
<ResultList result={result} selectedIndex={selectedIndex} />
) : null}
</div>
);
}
# 마무리
이번 문제를 통해 SyntheticEvent와 nativeEvent 서도 알게되었는데,
리액트 애플리케이션에서는 SyntheticEvent와 nativeEvent를 혼용해서 사용하지 않아야 한다는 것도 알게 되었다.
https://ryankubik.com/blog/dont-mix-react-synthetic-and-native-events
문제를 겪고 해결하는 과정이 엄청 오래걸리고 힘들었지만, 이런 과정이 나름 즐겁고 뿌듯하고 성취감이 엄청나다,,ㅎ
그래서 개발하나보다~^_^
'React' 카테고리의 다른 글
[React] Netlify를 통해 서버리스 만들기 및 netlify: command not found 해결방법(엄청난 에러의 연속..) (0) | 2023.05.17 |
---|---|
캐시 스토리지(cache storage) 설정, 에러 해결(clone is not a function), 디바운싱을 활용한 검색 기능 (0) | 2023.05.08 |