Skip to content

크롬 & 사파리 Composition

dannysir edited this page Nov 22, 2024 · 1 revision
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 ("산")

문제해결 과정

  1. 우선 가장 먼저 시도 했던 방법은 Debounce에서 현대 input 테그의 value와 매개변수로 들어온 값이 일치하면 debounce를 실행하지 않고 바로 리턴하는 방식이었다. 우선 해당 방식은 실패했으며 스페이스바 혹은 버튼 클릭시 debounce가 실행되었다. 또한 한가지 큰 단점이 있었는데 사용자가 ‘삼성’ 이라는 입력어를 입력했다가 지우고 바로 다시 ‘삼성’을 입력하면 이전에 기억했던 값과 현재 값이 같아서 무한 로딩이 걸리게 되었다.

  2. 그 다음으로 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);
            }}
          />
    );

    결론은 위의 로직도 실패하였다.

  3. 이후 다른 맥북 노트북을 이용해 서비스를 접속해봤다. 그런데 다른 맥북에서는 safari에서도 해당 이슈가 발생하지 않는 것을 발견했고 맥북 로컬 설정과 한글 입력 설정도 모두 확인해본 결과 safari 버전만 다르다는 것을 알게 되었다. 따라서 safari 18버전 이하의 버전에서는 오류가 나는 것인가 생각하게 되었다.

  4. 위와 같은 과정을 거친 이후 safari 18버전 이하의 맥북을 찾았다. 직접 맥북 safari 버전을 낮추는 방법도 찾아봤지만, mac OS를 직접 낮추는 방법 밖에 찾지 못하여 주변 지인들을 수소문하여 17.2.1버전을 찾게 되었다. 그런데 해당 버전에서는 아무 문제 없이 동작하는 것을 발견했고 그 다음으로 내 PC가 문제인지 의심하게 되었다. 따라서 노트북을 종료후에 다시 키게 되었고 문제가 해결된 것을 확인할 수 있었다.

풀리지 않는 의문

  1. 현재는 내 노트북의 safari, chrome 모두 정상 작동하는 것을 확인했다. 그런데 분명 금요일에 동료들과 이야기하는 과정에서 내 PC 뿐만이 아니라 동료들에게서도 해당 현상이 나타나는 것을 확인했다. 따라서 현재 노트북의 설정을 이것저것 바꿔보면서 해당 오류를 다시 일으키기 위해 노력 중이지만 아직까지 다시 발생하지 않고 있다.
  2. 현재 safari에서는 한글 입력시 composition-underline 이라고 불리는 한글 입력 중에 발생하는 밑줄 커서?가 발생하지 않는다. 그런데 오류 상황의 동영상을 기록한 것을 보면 해당 밑줄이 존재하는 것을 볼 수 있다. chrome에서는 해당 밑줄 커서가 있어도 문제 상황이 발생하지 않지만 safari에서는 아래 보이는 파란색의 밑줄 커서가 발생하면 그런 에러가 발생하는 것이 아닐까 추측하고 있다.
스크린샷 2024-11-16 오후 5 53 31

참고 자료

Safari 17.2 Release Notes | Apple Developer Documentation

Element: compositionstart event - Web APIs | MDN

📜 개발 일지

⚠️ 트러블 슈팅

❗ 규칙

🗒️ 기록

기획
회의록
데일리스크럼
그룹 멘토링
그룹 회고

😲 개별 멘토링

고동우
김진
서산
이시은
박진명
Clone this wiki locally