한글 input value를 입력 받은 후 키보드 이벤트가 발생했을 때, 왜 api가 2번 호출될까?
프로젝트를 진행하며 콘솔로 받아온 데이터를 확인하는데 계속 두번 씩 데이터를 받아오는 것을 발견했다.
현재 프로젝트의 규모가 작아 당장은 상관 없으나,
나중에 서버와 연결했을 경우 정확한 원인을 알 수 없으니 메모리 누수가 생길 수도 있는 점을 고려하지 않을 수 없었다.
그래서 오늘은 그에 대한 트러블 슈팅 기록을 하고자 한다. 😇
// SearchInput.tsx
const enterKeyHandler: KeyboardEventHandler<HTMLInputElement> = (e) => {
const locationInfo = filteredTitle(inputSearch);
if (e.key === "Enter" && locationInfo.length !== 0) {
api(setData, locationInfo); // 유저가 엔터키 이벤트를 발생시켰을 때, data state를 업데이트(데이터를 받아옴)하고, location에 담긴 title을 필터된 변수를 api 함수로 호출한다.
setIsSearchToggle(false);
} else if (e.key === "Enter" && locationInfo.length === 0) {
alert("일치하는 도시를 찾을 수 없습니다! 😢");
setInputSearch("");
}
}
사용자가 검색창(input)에 값을 입력하고 엔터키로 이벤트가 발생하였을 때 api가 호출될 수 있도록 하였다.
리액트를 사용하고 있기 때문에 onKeyDown(js는 keydown) 이벤트를 연결해주었다.
그런데 계속 데이터가 두번씩 받아와지는게 아닌가..? (당황)
그 말인 즉슨, 똑같은 이벤트가 2번 발생된다는 얘기이다.
처음에는 핸들링 함수 내에서 잘못 조작하고 있거나 api 함수 내에서 중복으로 호출하고 있는 것이라고 생각했다.
// api.ts
export default function api(
setFn: React.Dispatch<React.SetStateAction<data[]>>,
locationInfo: locationInfo[]
) {
const API_KEY = process.env.REACT_APP_API_KEY;
const API_URL = process.env.REACT_APP_API_URL;
const BASE_DATE = year + month + day;
const BASE_TIME = hours + String(Number(minutes) - 30);
const LOCATION = locationInfo;
const url = `${API_URL}?serviceKey=${API_KEY}&numOfRows=60&pageNo=1&base_date=${BASE_DATE}&base_time=${BASE_TIME}&nx=${LOCATION?.[0]?.nx}&ny=${LOCATION?.[0]?.ny}&dataType=json`;
fetch(url)
.then((res) => res.json())
.then((res) => {
setFn(res.response.body.items.item);
})
.catch((err) => console.log("error:", err));
}
문제는 없어보였다..
이때부터 왜이러는지 이유를 몰라 멘붕 그잡채..살려줘..
구글링을 해보니 React Strict mode 때문에 이중으로 호출될 수 있다고 한다.
Strict 모드가 자동으로 부작용을 찾아주는 것은 불가능합니다. 하지만, 조금 더 예측할 수 있게끔 만들어서 문제가 되는 부분을 발견할 수 있게 도와줍니다. 이는 아래의 함수를 의도적으로 이중으로 호출하여 찾을 수 있습니다.
- 클래스 컴포넌트의 constructor, render 그리고 shouldComponentUpdate 메서드
- 클래스 컴포넌트의 getDerivedStateFromProps static 메서드
- 함수 컴포넌트 바디
- State updater 함수 (setState의 첫 번째 인자)
- useState, useMemo 그리고 useReducer에 전달되는 함수
⚠️ 주의
- 개발 모드에서만 적용됩니다. 생명주기 메서드들은 프로덕션 모드에서 이중으로 호출되지 않습니다.
하지만 프로젝트를 시작하면서 <React.StrictMode>는 진작에 해제했었기 때문에 이 문제가 아니었다.
혹시 이벤트만 중복으로 발생되는 것인지,
아니면 api도 2번 호출되는 것인지 테스트를 위해 크롬 개발자도구의 network 탭에서 확인을 해보았다.
역시나 api가 2번 호출되고 있었다.
그 후에도 문제가 무엇일지 이것저것 시도해보던 중..
우연히 자동완성에 영문으로 "seoul" 떠있는 걸 입력해보니 api가 1번만 호출됐다..??
자동완성 값일 땐 api가 1번 호출되는건가? 캐시랑 상관이 있는건가?
다시 한글 "서울"로 입력해보고, 영문 "seoul"로 입력해보았더니,
한글일 땐 api가 2번 호출되고 영문일 땐 api가 1번 호출되는 것으로 확인되었다.
아.. 이건 분명 한글과 영문에 문제가 있는거구나!!
구글링을 해보니 이미 나와 같은 문제를 겪고있는 블로그 글들이 꽤 있었다. (하단 Ref 링크 참고)
영문은 알파벳 한글자씩 표현되지만,
한글의 경우 자음+모음의 조합으로 만들어지는 문자여서 value 입력값인 글자가 현재 조합중인건지, 조합이 끝난 상태인지 알 수 없다고 한다.
이 과정을 OS와 브라우저가 동시에 처리하기 때문에 한글로 입력 시 이벤트가 중복으로 발생하게 된다.
IME
IME는 영어 외 언어를 다양한 브라우저에서 지원하도록 언어를 변환시켜주기 위한 OS 단계의 어플리케이션이다.
IME를 통해 한글, 일본어, 중국어 등을 변환하는 과정(composition)에서 keydown이나 keyup 이벤트는 OS뿐만 아니라 브라우저에서도 처리된다.
Web API 스펙 중 event target에 KeyboardEvent.isComposing 라는 프로퍼티를 제공하고 있다.
The KeyboardEvent.isComposing read-only property returns a boolean value indicating if the event is fired within a composition session
composition 중에 이벤트가 발생하는지 여부를 boolean 값으로 반환한다고 한다.
이는 영어 외의 문자를 표현하는 과정(composition)에서 isComposing 값을 참조하면 true로 값이 반환된다는 의미이다.
따라서 조건으로 composition 단계가 true일 경우(한글 등의 문자) return을 해주도록 코드를 추가해준다.
// SearchInput.tsx
const enterKeyHandler: KeyboardEventHandler<HTMLInputElement> = (e) => {
const locationInfo = filteredTitle(inputSearch);
if (e.nativeEvent.isComposing) return; // composition 단계일 때(한글일 때) return하도록 코드 추가
if (e.key === "Enter" && locationInfo.length !== 0) {
api(setData, locationInfo);
setIsSearchToggle(false);
} else if (e.key === "Enter" && locationInfo.length === 0) {
alert("일치하는 도시를 찾을 수 없습니다! 😢");
setInputSearch("");
}
}
⭐️ TL;DR
트러블 슈팅
- input value 입력값으로 키보드 이벤트(keydown) 발생 시 api가 이중 호출되는 문제
해결방법
- React Strict Mode로 설정되어 있는지 확인 후 해제
keyup, keydown 이벤트를 keypress 이벤트로 변경MDN Deprecated- event.isComposing === true 일 때 return; (React에서는 event.nativeEvent.isComposing 값을 참조)
<Ref>
https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/isComposing
https://developer.mozilla.org/en-US/docs/Web/API/Element/keypress_event
https://ko.legacy.reactjs.org/docs/strict-mode.html
https://levelup.gitconnected.com/javascript-events-handlers-keyboard-and-load-events-1b3e46a6b0c3