safari IME ์ด์ ์ ๋ฆฌ
์๋น์ค ๊ธฐ๋ฅ ๊ฐ๋ฐ ์ค, ๋ชจ๋ฌ ๋ด ๋งํฌ์ ํด๋ฆญ ์ด๋ฒคํธ๊ฐ ์ ์์ ์ผ๋ก ๋์ํ์ง ์๋ ๋ฒ๊ทธ๋ฅผ ๋ฐ๊ฒฌํ๋ค. ๊ตฌ์ฒด์ ์ผ๋ก๋ ๋ ๋ฒ ํด๋ฆญํด์ผ๋ง ์ด๋ฒคํธ๊ฐ ์ ๋๋ก ์คํ๋๋ ์ํฉ์ด์๋ค.
๊ฒฐ๋ก ๋ถํฐ ๋งํ๋ฉด ํด๊ฒฐํ์ง๋ ๋ชปํ๋ค. ํ์ง๋ง ๋น์ทํ ๋ฌธ์ ๋ฅผ ๊ฒช๊ณ ์๋ ๊ฐ๋ฐ์๋ค์๊ฒ ์ฐธ๊ณ ๊ฐ ๋ ์ ์๋๋ก, ํธ๋ฌ๋ธ์ํ ๊ณผ์ ์ ๊ธฐ๋ก์ผ๋ก ๋จ๊ธด๋ค.
์ฒซ๋ฒ์งธ ์๋
์ฒ์์๋ ์ฌํ๋ฆฌ์์ ํด๋ฆญ์ด ๋์ํ์ง ์๋ ๋ฌธ์ ๋ผ๊ณ ์๊ฐํ๊ณ , ๊ด๋ จ ๋ด์ฉ์ ๊ฒ์ํด๋ณด๋ค๊ฐ 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 ์ด๋ฒคํธ์ ๋์ ๊ณผ์ ๊ณผ ๋ฐฉ์ / ๋ค์ํ ์๊ฐ์ผ๋ก ์ ๊ทผํด๋ณผ ๊ฒฝํ์ด ๋์๋ค. ์ถํ ๋์ฑ ๊ฐ๋ฐ์ง์์ ์์์ ํด๋น ์ด์๋ ํด๊ฒฐํ ์ ์๋ ๊ธฐํ๊ฐ ๋๋ฉด ์ข์ ๊ฒ ๊ฐ๋ค.