๐ŸŒ
๊ฐœ๋ฐœโ€ขํ”„๋ก ํŠธ์—”๋“œ

safari IME ์ด์Šˆ ์ •๋ฆฌ

2024.08.29

์„œ๋น„์Šค ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ ์ค‘, ๋ชจ๋‹ฌ ๋‚ด ๋งํฌ์— ํด๋ฆญ ์ด๋ฒคํŠธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š” ๋ฒ„๊ทธ๋ฅผ ๋ฐœ๊ฒฌํ–ˆ๋‹ค. ๊ตฌ์ฒด์ ์œผ๋กœ๋Š” ๋‘ ๋ฒˆ ํด๋ฆญํ•ด์•ผ๋งŒ ์ด๋ฒคํŠธ๊ฐ€ ์ œ๋Œ€๋กœ ์‹คํ–‰๋˜๋Š” ์ƒํ™ฉ์ด์—ˆ๋‹ค.

๊ฒฐ๋ก ๋ถ€ํ„ฐ ๋งํ•˜๋ฉด ํ•ด๊ฒฐํ•˜์ง€๋Š” ๋ชปํ–ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋น„์Šทํ•œ ๋ฌธ์ œ๋ฅผ ๊ฒช๊ณ  ์žˆ๋Š” ๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ ์ฐธ๊ณ ๊ฐ€ ๋  ์ˆ˜ ์žˆ๋„๋ก, ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๊ณผ์ •์„ ๊ธฐ๋ก์œผ๋กœ ๋‚จ๊ธด๋‹ค.

์ฒซ๋ฒˆ์งธ ์‹œ๋„

์ฒ˜์Œ์—๋Š” ์‚ฌํŒŒ๋ฆฌ์—์„œ ํด๋ฆญ์ด ๋™์ž‘ํ•˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๋ผ๊ณ  ์ƒ๊ฐํ•˜๊ณ , ๊ด€๋ จ ๋‚ด์šฉ์„ ๊ฒ€์ƒ‰ํ•ด๋ณด๋‹ค๊ฐ€ mousedown ์ด๋ฒคํŠธ์—์„œ ๋””๋ฐ”์šด์‹ฑ์„ ๊ฑธ์–ด click ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ๋ฐฉ๋ฒ•์„ ์‹œ๋„ํ–ˆ๋‹ค.

const handleClick = debounce(()=>{
    // ์‹คํ–‰ํ•  ์ด๋ฒคํŠธ
})

const handleMouseDown =(e)=> {
    e.currentTarget.click()
}

<Link onMouseDown={handleMouseDown} onClick={handleClick} />

ํ•ด๊ฒฐํ•œ ์ค„ ์•Œ์•˜์œผ๋‚˜ ์ •ํ™•ํ•˜๊ฒŒ ์–ด๋А ์‚ฌํŒŒ๋ฆฌ ๋ฒ„์ „์—์„œ ํ•ด๋‹น ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š”์ง€ ํŒŒ์•…ํ•˜์ง€ ๋ชปํ–ˆ๊ณ  ์—ฌ์ „ํžˆ ์ด์Šˆ๋Š” ์žฌํ˜„๋˜๊ณ  ์žˆ์—ˆ๋‹ค.

๋‘๋ฒˆ์งธ ์‹œ๋„

๊ทธ๋ž˜์„œ ๋‹ค๋ฅธ๋ฐฉ๋ฒ•์œผ๋กœ ๋””๋ฒ„๊น…์„ ์‹œ๋„ํ•ด๋ณด๊ธฐ๋กœ ํ–ˆ๋‹ค. ์šฐ์„  input ์ž…๋ ฅ๊ด€๋ จ ๋ฌธ์ œ๊ฐ€ ์žˆ์„๊ฑฐ๋ผ ์ถ”์ธกํ•ด์„œ ๊ด€๋ จ๋‚ด์šฉ์œผ๋กœ ๋‹ค์‹œ ์ฐพ์•„๋ณด๋‹ˆ composing ๊ด€๋ จ ๋ฌธ์ œ๋ผ๊ณ  ์ƒ๊ฐํ•ด์„œ isComposing์„ ์ฒดํฌํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•ด๋ณด๊ธฐ๋กœ ํ–ˆ๋‹ค.

isComposing์ด๋ž€? input ์ด๋ฒคํŠธ์—์„œ compositionStart, compositionEnd ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ถˆ๋ฆฐ๊ฐ’์ด๋‹ค.

const handleKeyDown = (e) => {
  if (e.nativeEvent.isComposing) return;
};

ํ•ด๋‹น ๋ฐฉ๋ฒ•๋„ ํšจ๊ณผ๊ฐ€ ์—†์—ˆ๋‹ค.

์„ธ๋ฒˆ์งธ ์‹œ๋„

composition ์ด๋ฒคํŠธ์™€ ๊ด€๋ จ์ด ์žˆ์„๊ฒƒ์ด๋ผ ์ถ”์ธก๋˜์–ด ๊ด€๋ จ ๋‚ด์šฉ์œผ๋กœ ๊ฒ€์ƒ‰ํ•ด๋ณด๋‹ˆ IME์— ๋Œ€ํ•ด ์ฐพ์•„๋ณด๊ฒŒ ๋˜์—ˆ๋‹ค.

์ž…๋ ฅ๊ธฐ ๋˜๋Š” ์ž…๋ ฅ ๋ฐฉ์‹ ํŽธ์ง‘๊ธฐ(input method editor, IME)๋Š” ํ•œ๊ธ€, ํ•œ์ž์ฒ˜๋Ÿผ ์ปดํ“จํ„ฐ ์žํŒ์— ์žˆ๋Š” ๊ธ€์ž๋ณด๋‹ค ์ˆ˜๊ฐ€ ๋” ๋งŽ์€ ๋ฌธ์ž๋ฅผ ๊ณ„์‚ฐํ•˜๊ฑฐ๋‚˜ ์กฐํ•ฉํ•˜์—ฌ ์ž…๋ ฅํ•ด ์ฃผ๋Š” ์‹œ์Šคํ…œ ์†Œํ”„ํŠธ์›จ์–ด์ด๋‹ค.

ํ•ด๋‹น ๋‚ด์šฉ์„ ๋ณด๊ณ  ๋‹ค์‹œ ๋””๋ฒ„๊น…์„ ํ•ด๋ณด๋‹ˆ ์˜๋ฌธ์ž…๋ ฅ์‹œ์—๋Š” ํ•ด๋‹น ํ˜„์ƒ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š”๊ฑธ ์•Œ๊ฒŒ๋๋‹ค. ๊ทธ๋ฆฌ๊ณ  ํฌ๋กฌ๊ณผ ๋‹ค๋ฅด๊ฒŒ ์‚ฌํŒŒ๋ฆฌ์—์„  ํ•œ๊ธ€๋กœ input์„ ์ž…๋ ฅํ•˜๊ณ  link๋ฅผ ๋ˆ„๋ฅด๋ฉด compositionEnd ์ด๋ฒคํŠธ๊ฐ€ ํŠธ๋ฆฌ๊ฑฐ๋˜๊ณ  ์ดํ›„ ํด๋ฆญ ์ด๋ฒคํŠธ ์ „ํŒŒ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์•˜๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ๋ฏธ๋ฆฌ compositionEnd๋ฅผ ์‹คํ–‰์‹œ์ผœ์ฃผ๋ฉด ๋˜์ง€ ์•Š์„๊นŒ? ํ•˜๋Š” ์•„์ด๋””์–ด๊ฐ€ ๋– ์˜ฌ๋ž๋‹ค. ๐Ÿง

๊ทธ๋ž˜์„œ input์„ ์ž…๋ ฅํ•˜๋ฉด ๋””๋ฐ”์šด์‹ฑ์œผ๋กœ ๋งˆ์ง€๋ง‰์— blur๋ฅผ ์‹คํ–‰์‹œ์ผœ์ฃผ๋ฉด ๋ ๊ฑฐ๋ผ ์ƒ๊ฐํ•ด์„œ hiddenInput์„ ๋งŒ๋“ค๊ณ  focus๋ฅผ hiddenInput์œผ๋กœ ์ด๋™์‹œ์ผœ blur๋ฅผ ์‹คํ–‰์‹œํ‚ค๊ณ  ๋‹ค์‹œ ์›๋ž˜ input์œผ๋กœ focus๋ฅผ ์‹œ์ผœ์ฃผ๋Š” ๋ฐฉ๋ฒ•์„ ์ ์šฉํ•ด๋ดค๋‹ค. ์—ฌ๊ธฐ์— ์ž…๋ ฅ์†๋„๋ณด๋‹ค ๊ธด ํƒ€์ด๋ฐ์ธ 500ms ์ •๋„์˜ ๋””๋ฐ”์šด์Šค๋ฅผ ๊ฑธ์–ด์„œ ํ•ด๊ฒฐํ•œ ๋“ฏ ๋ณด์˜€๋‹ค..

๊ทธ๋Ÿฌ๋‚˜ 500ms๋ผ๊ณ  ์ •ํ•œ ๊ทธ ํƒ€์ด๋ฐ์„ ํŠน์ • ๊ธฐ์ค€์œผ๋กœ ์„ค์ •ํ•˜๊ธฐ๋„ ์–ด๋ ค์šธ ๋ฟ๋”๋Ÿฌ ์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ ์†๋„๊ฐ€ ๋””๋ฐ”์šด์Šค ํƒ€์ด๋ฐ๋ณด๋‹ค ๊ธด ๊ฒฝ์šฐ์—” ํ•œ๊ธ€์ด ์ž˜๋ฆฌ๋Š” ํ˜„์ƒ์ด ์žˆ์—ˆ๋‹ค.

ex) ๊ฐ•์•„์ง€ => ใ„ฑใ…ใ…‡ใ…‡ใ…ใ…ˆใ…ฃ

๋„ค๋ฒˆ์งธ ์‹œ๋„

๊ทธ๋ž˜์„œ ๋งˆ์ง€๋ง‰์œผ๋กœ compositionStart, compositionUpdate, compositionEnd ์ด๋ฒคํŠธ์— ๋กœ๊ทธ๋ฅผ ์ฐ์–ด๋ณด๋‹ˆ compositionEnd ์ด๋ฒคํŠธ๋Š” ๊ธ€์ž ์กฐํ•ฉ์ด ์™„์„ฑ๋˜์–ด 1. ๋‹ค์Œ ๊ธ€์ž๋กœ ๋„˜์–ด๊ฐ€๊ฑฐ๋‚˜, 2. ์ข…๊ฒฐ๋  ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ๊ทœ์น™์„ ์ฐพ์•˜๊ณ  end๊ฐ€ ์‹คํ–‰๋˜๊ณ  start๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š๋Š”๊ฒฝ์šฐ์— ํƒ€์ดํ•‘์„ ์ข…๊ฒฐํ–ˆ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค.

๊ฒฐ๊ตญ end๊ฐ€ ์‹คํ–‰๋˜๊ณ  ๊ณง๋ฐ”๋กœ start๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š๋Š”๊ฒฝ์šฐ์—” blur ์ด๋ฒคํŠธ๋ฅผ ์‹คํ–‰์‹œํ‚ค๋Š” ๋ฐฉ๋ฒ•์„ ์ ์šฉํ•ด๋ณด๊ธฐ๋กœ ํ–ˆ๋‹ค.


  const [compositionEndEventData, setCompositionEndEventData] = useState<CompositionEvent['data'] | null>(null);

  const { register } = useFormContext<FormValues>();
  const [blurTimeout, setBlurTimeout] = useState<NodeJS.Timeout | null>(null);

  const callbackRef = useCallback(
    (node: null | HTMLInputElement) => {
      const handleBlur = (node: HTMLInputElement) => {
        return new Promise((resolve) => {
          // blur ์ด๋ฒคํŠธ๋ฅผ ์ผ์ • ์‹œ๊ฐ„ ์ง€์—ฐ ํ›„ ์‹คํ–‰
          const blurTimeout = setTimeout(() => {
            node.blur();
            resolve(true);
          }, 0);

          setBlurTimeout(blurTimeout);
        });
      };

      if (node) {
        ...
        node.addEventListener('blur', () => {
          if (compositionEndEventData !== null) {
            // ๊ธฐ์กด compositionend ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜๋™์œผ๋กœ ํŠธ๋ฆฌ๊ฑฐ
            const compositionEndEvent = new CustomEvent('compositionend', {
              bubbles: true,
              cancelable: true,
              detail: compositionEndEventData, // ์›๋ž˜์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จ
            });

            node.dispatchEvent(compositionEndEvent);

            // ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”
            setCompositionEndEventData(null);
          }
          node.focus();
        });

        node.addEventListener('compositionend', async (event) => {
          event.stopImmediatePropagation();
          await handleBlur(node);
          setCompositionEndEventData(event.data);
        });
      }
    },
    [register, getValues]
  );


  return (
        ...
        <input
            ref={callbackRef}
            onCompositionStart={(e) => {
                if (blurTimeout) clearTimeout(blurTimeout);
            }}
        ...
        />

์œ„ ๋ฐฉ๋ฒ•๋„ ์ฒ˜์Œ์— ์–ธ๊ธ‰ํ–ˆ๋“ฏ์ด ์ด์Šˆ๋ฅผ ํ•ด๊ฒฐํ•˜์ง€๋Š” ๋ชปํ–ˆ๋‹ค.

์ •๋ฆฌ

๊ทธ๋ž˜๋„ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…์„ ํ•ด๋ณด๋ฉด์„œ IME์— ๋Œ€ํ•ด ์•Œ๊ฒŒ๋˜์—ˆ๊ณ , composition ์ด๋ฒคํŠธ์˜ ๋™์ž‘ ๊ณผ์ •๊ณผ ๋ฐฉ์‹ / ๋‹ค์–‘ํ•œ ์‹œ๊ฐ์œผ๋กœ ์ ‘๊ทผํ•ด๋ณผ ๊ฒฝํ—˜์ด ๋˜์—ˆ๋‹ค. ์ถ”ํ›„ ๋”์šฑ ๊ฐœ๋ฐœ์ง€์‹์„ ์Œ“์•„์„œ ํ•ด๋‹น ์ด์Šˆ๋„ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐํšŒ๊ฐ€ ๋˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค.