-
Notifications
You must be signed in to change notification settings - Fork 1
크롬 & 사파리 Composition
KakaoTalk_Video_2024-11-16-16-49-17.mp4
현재 서비스를 실행하는 몇몇 브라우저에서 위의 동영상과 같이 한글 입력시 발생하는 오류가 발생했다.
영어를 입력할 때는 발생하지 않고 한글을 입력할 때만 composition-underline이 생기며 엔터, 스페이스바, 방향키, 클릭 시 debounce가 한번 더 발생했다.
크롬에서는 해당 문제가 발생하지 않고 사파리에서만 해당 문제가 발생했다. 따라서 우선 두 브라우저의 IME의 동작 방식에 차이가 있다는 것을 알게 되었고 어떤 차이가 있는지 아래 코드를 통해 확인했다.
HMTL 파일
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>JuGa</title>
</head>
<body>
<input type="text" id="testInput" />
<div id="log"></div>
</body>
<script src='src/main.tsx'></script>
</html>
JS파일
const input = document.getElementById('testInput');
const log = document.getElementById('log');
function logEvent(event) {
const detail = {
type: event.type,
data: event.data,
target: {
value: event.target.value,
selectionStart: event.target.selectionStart,
selectionEnd: event.target.selectionEnd,
},
};
if (event.type === 'compositionstart' || event.type === 'compositionupdate') {
detail.compositionData = event.data;
}
console.log(JSON.stringify(detail, null, 2));
}
// 이벤트 리스너 등록
input.addEventListener('compositionstart', logEvent);
input.addEventListener('compositionupdate', logEvent);
input.addEventListener('compositionend', logEvent);
input.addEventListener('input', logEvent);
사파리
{
"type": "input",
"data": "ㅅ",
"target": {
"value": "ㅅ",
"selectionStart": 1,
"selectionEnd": 1
}
}
{
"type": "input",
"data": "서",
"target": {
"value": "서",
"selectionStart": 1,
"selectionEnd": 1
}
}
{
"type": "input",
"data": "섯",
"target": {
"value": "섯",
"selectionStart": 1,
"selectionEnd": 1
}
}
{
"type": "input",
"data": "서",
"target": {
"value": "서",
"selectionStart": 1,
"selectionEnd": 1
}
}
{
"type": "input",
"data": "사",
"target": {
"value": "서사",
"selectionStart": 2,
"selectionEnd": 2
}
}
{
"type": "input",
"data": "산",
"target": {
"value": "서산",
"selectionStart": 2,
"selectionEnd": 2
}
}
크롬
{
"type": "compositionstart",
"data": "",
"target": {
"value": "",
"selectionStart": 0,
"selectionEnd": 0
},
"compositionData": ""
}
main.tsx:42 {
"type": "compositionupdate",
"data": "ㅅ",
"target": {
"value": "",
"selectionStart": 0,
"selectionEnd": 0
},
"compositionData": "ㅅ"
}
main.tsx:42 {
"type": "input",
"data": "ㅅ",
"target": {
"value": "ㅅ",
"selectionStart": 1,
"selectionEnd": 1
}
}
main.tsx:42 {
"type": "compositionupdate",
"data": "서",
"target": {
"value": "ㅅ",
"selectionStart": 0,
"selectionEnd": 1
},
"compositionData": "서"
}
main.tsx:42 {
"type": "input",
"data": "서",
"target": {
"value": "서",
"selectionStart": 1,
"selectionEnd": 1
}
}
main.tsx:42 {
"type": "compositionupdate",
"data": "섯",
"target": {
"value": "서",
"selectionStart": 0,
"selectionEnd": 1
},
"compositionData": "섯"
}
main.tsx:42 {
"type": "input",
"data": "섯",
"target": {
"value": "섯",
"selectionStart": 1,
"selectionEnd": 1
}
}
main.tsx:42 {
"type": "compositionupdate",
"data": "서",
"target": {
"value": "섯",
"selectionStart": 0,
"selectionEnd": 1
},
"compositionData": "서"
}
main.tsx:42 {
"type": "input",
"data": "서",
"target": {
"value": "서",
"selectionStart": 1,
"selectionEnd": 1
}
}
main.tsx:42 {
"type": "compositionend",
"data": "서",
"target": {
"value": "서",
"selectionStart": 1,
"selectionEnd": 1
}
}
main.tsx:42 {
"type": "compositionstart",
"data": "",
"target": {
"value": "서",
"selectionStart": 1,
"selectionEnd": 1
},
"compositionData": ""
}
main.tsx:42 {
"type": "compositionupdate",
"data": "사",
"target": {
"value": "서",
"selectionStart": 1,
"selectionEnd": 1
},
"compositionData": "사"
}
main.tsx:42 {
"type": "input",
"data": "사",
"target": {
"value": "서사",
"selectionStart": 2,
"selectionEnd": 2
}
}
main.tsx:42 {
"type": "compositionupdate",
"data": "산",
"target": {
"value": "서사",
"selectionStart": 1,
"selectionEnd": 2
},
"compositionData": "산"
}
main.tsx:42 {
"type": "input",
"data": "산",
"target": {
"value": "서산",
"selectionStart": 2,
"selectionEnd": 2
}
}
main.tsx:42 {
"type": "compositionend",
"data": "산",
"target": {
"value": "서산",
"selectionStart": 2,
"selectionEnd": 2
}
}
위의 결과를 보면 input에 한글을 입력할 때 각각의 브라우저의 한글입력 처리 방식이 명확히 보인다.
두 경우 모두 input은 계속 일어나서 바로바로 input 테그의 value에 적용이 된다. 하지만 사파리의 경우 composition이 발생하지 않는데 크롬은 하나의 조합이 완성이 될 때마다 end와 start가 발생하는 것을 볼 수 있다. 따라서 “섯→서사” 에서 end가 발생하고 다시 start가 발생하는 것을 볼 수 있다.
// Safari의 경우 ("서산" 입력 시)
//input 이벤트
"ㅅ" → "서" → "섯" → "서" → "서사" → "서산"
// Chrome의 경우
// "ㅅ"
compositionstart ("")
compositionupdate ("ㅅ") + input ("ㅅ")
// "서"
compositionupdate ("서") + input ("서")
// "섯"
compositionupdate ("섯") + input ("섯")
// "서사"
compositionupdate ("서") + input ("서")
compositionend ("서")
compositionstart ("")
compositionupdate ("사") + input ("서사")
// "서산"
compositionupdate ("산") + input ("서산")
// 엔터 혹은 방햐잌 혹은 클릭 등을 할 경우
compositionend ("산")
-
우선 가장 먼저 시도 했던 방법은 Debounce에서 현대 input 테그의 value와 매개변수로 들어온 값이 일치하면 debounce를 실행하지 않고 바로 리턴하는 방식이었다. 우선 해당 방식은 실패했으며 스페이스바 혹은 버튼 클릭시 debounce가 실행되었다. 또한 한가지 큰 단점이 있었는데 사용자가 ‘삼성’ 이라는 입력어를 입력했다가 지우고 바로 다시 ‘삼성’을 입력하면 이전에 기억했던 값과 현재 값이 같아서 무한 로딩이 걸리게 되었다.
-
그 다음으로 input 테그에
onCompositionEnd
,onCompositionStart
,onCompositionUpdate
옵션을 이용해 직접 composition 이벤트를 제어하면 해결될 것이라고 생각했다. 아래와 같이 composing이 발생하는지 확인한 이후 발생했을 경우에만 debounce 내부 로직이 돌아가도록 만들었다.const [isComposing, setIsComposing] = useState(false); return ( <input onChange={(e) => { if (!isComposing) { onChange(e.target.value); } }} onCompositionStart={() => setIsComposing(true)} onCompositionEnd={(e) => { setIsComposing(false); onChange(e.target.value); }} /> );
결론은 위의 로직도 실패하였다.
-
이후 다른 맥북 노트북을 이용해 서비스를 접속해봤다. 그런데 다른 맥북에서는 safari에서도 해당 이슈가 발생하지 않는 것을 발견했고 맥북 로컬 설정과 한글 입력 설정도 모두 확인해본 결과 safari 버전만 다르다는 것을 알게 되었다. 따라서 safari 18버전 이하의 버전에서는 오류가 나는 것인가 생각하게 되었다.
-
위와 같은 과정을 거친 이후 safari 18버전 이하의 맥북을 찾았다. 직접 맥북 safari 버전을 낮추는 방법도 찾아봤지만, mac OS를 직접 낮추는 방법 밖에 찾지 못하여 주변 지인들을 수소문하여 17.2.1버전을 찾게 되었다. 그런데 해당 버전에서는 아무 문제 없이 동작하는 것을 발견했고 그 다음으로 내 PC가 문제인지 의심하게 되었다. 따라서 노트북을 종료후에 다시 키게 되었고 문제가 해결된 것을 확인할 수 있었다.
- 현재는 내 노트북의 safari, chrome 모두 정상 작동하는 것을 확인했다. 그런데 분명 금요일에 동료들과 이야기하는 과정에서 내 PC 뿐만이 아니라 동료들에게서도 해당 현상이 나타나는 것을 확인했다. 따라서 현재 노트북의 설정을 이것저것 바꿔보면서 해당 오류를 다시 일으키기 위해 노력 중이지만 아직까지 다시 발생하지 않고 있다.
- 현재 safari에서는 한글 입력시 composition-underline 이라고 불리는 한글 입력 중에 발생하는 밑줄 커서?가 발생하지 않는다. 그런데 오류 상황의 동영상을 기록한 것을 보면 해당 밑줄이 존재하는 것을 볼 수 있다. chrome에서는 해당 밑줄 커서가 있어도 문제 상황이 발생하지 않지만 safari에서는 아래 보이는 파란색의 밑줄 커서가 발생하면 그런 에러가 발생하는 것이 아닐까 추측하고 있다.

- [FE] 프론트엔드 기술스택
- [FE] 라이브러리 없이 차트 구현 이유
- [FE] Canvas API 사용방법
- [FE] 네비게이션 바 애니메이션 구현
- [FE] Socket.io 사용방법
- [FE] Tanstack Router에 대하여...
- [FE] Intl(Internationalization) API
- [FE] React Suspense 적용
- [FE] 한글 입력 방식의 유연성을 높인 검색 시스템 구현하기
- [BE] 백엔드 기술 스택
- [BE] SSE vs Socket.io
- [BE] Redis를 도입하게 된 계기
- [BE] ACG Rule을 활용한 Secure CI CD 파이프라인 구현
- [BE] Nginx 로드밸런싱을 통해 한국 투자 API 소켓 제한 극복
- [BE] 주가 지수 기능 개발 과정
- [BE] 매수 및 매도 기능 개발 과정
- [BE] 실시간 자산 조회 기능 개발 과정
- [BE] 단위 테스트
- [BE] redis를 이용한 한국투자 Open API 세션 관리
- [BE] 데이터베이스 인덱싱
- [FE] React에서의 DOM 요소 접근 (useRef vs getElementById)
- [FE] Outlet을 활용한 공통 레이아웃 관리
- [FE] react hooks가 특정 조건에서 실행되면 안되는 이유 & useQuery에 query function 매개변수가 undefined일 수도 있을 때 어떻게 해결할까
- [FE] cross‐domain 로컬 환경에서 cookie로 인증 처리하기 with vite proxy
- [FE] 크롬&사파리 Composition 차이
- [FE] useEffect 의존성 배열
- [BE] Naver Cloud Platform HTTPS 무응답 현상
- [BE] 한국투자 Open API에서 access token을 발급받지 못하는 문제
- [BE] 한국투자 Open API와 웹소켓 연결이 되지 않던 문제
- [BE] 한국투자 Open API 웹소켓 연결이 중단되는 문제
- [BE] 같은 주식 주문이 동시에 여러 번 체결되는 문제
- [BE] 한국투자 Open API Websocket 세션을 두 개에서 한 개로 변경하기
- [BE] Nginx 로드 밸런싱 중 Socket bad Request 발생하는 현상
- [BE] 매수/매도 체결 로직에 의해 redis pub/sub이 정상적으로 동작하지 않는 문제