엔터로 투두 등록을 하고 싶은데, 아래와 같이 코드를 작성했을 때 한글의 끝 글자만 전송되는 문제가 있었다.
import React, { useContext, useRef } from "react";
import { dispatchContext } from "../../context/todoContexts";
const TodoCreate = () => {
const todos = useContext(dispatchContext);
const inputRef = useRef<HTMLInputElement>(null);
const handleButtonClick = () => {
const inputValue = inputRef.current?.value;
if (inputValue) {
todos.addTodo(inputValue);
inputRef.current.value = "";
} else alert("할 일을 입력해주세요!");
};
const submitOnEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
handleButtonClick();
}
};
return (
<div className="flex w-full mb-8">
<>
<input
className="flex-grow mr-2"
data-testid="new-todo-input"
onKeyDown={submitOnEnter}
ref={inputRef}
/>
<button
className="w-1/5 base_button"
data-testid="new-todo-add-button"
onClick={handleButtonClick}
>
추가
</button>
</>
</div>
);
};
export default TodoCreate;
해당 문제를 해결하기 위해 시도한 내용들을 기록하고자 한다.
inputRef로 input에 작성되는 글을 가져온 뒤 엔터를 누르면
input의 onKeyDown으로 submitOnEnter이 실행되고,
handleButtonClick()을 실행시키는 것을 의도했었는데
submitOnEnter이 두 번 실행되어서 handleButtonClick도 두 번 실행되는 문제가 있었다.
handleButtonClick이 두 번 실행되는 게, onKeyDown의 문제일까 싶어서 onKeyUp으로 바꿨더니
여전히 두 번 실행되면서
이번에는 첫 실행에서 TODO가 추가되고, 두번째 실행에서 (빈 내용이므로 else문의) alert가 실행 되거나
반대로 첫 실행에서 alert가 실행되고, 두번째 실행에서 TODO가 추가되었다
useRef로 값을 가져오는 방법에 대해서도 좀 더 알아봤더니,
검색했을 때 내가 작성한 것과 같은 Ref의 사용 사례가 많지 않았고
Ref는 남용해서는 안된다는 공식 문서의 내용이 있었다
구글링 했을 때, 저장된 값을 가져오는 방법으로는 보편적으로 useState를 사용하는 것으로 보였다
https://velog.io/@imzzuu/React-Input-control-과-useRef의-적절한-사용-Input-유효성-검사
그래서 useRef로 가져오는 것은 적합한 방법이 아닌 것 같아 onChange와 useState를 사용, onKeyDown으로 전송하는 방법으로 수정했더니 정상 작동할 것으로 예상 했지만…
다시 처음처럼 마지막 글자만 전송되는 문제가 있었다.
https://velog.io/@dosomething/React-한글-입력시-keydown-이벤트-중복-발생-현상
그러다 검색 끝에 위 블로그 글을 발견했고,
https://legacy.reactjs.org/docs/events.html#composition-events
위 링크의 공식문서도 접하게 되었다.
한글이나 다른 IME (Input Method Editor)를 사용하는 경우, 한 글자씩 입력할 때마다 onChange
이벤트가 발생하지 않을 수 있으며, 이는 IME의 동작 방식으로 인한 것
IME 과정에서 keydown 이벤트가 발생하면 keydown 이벤트가 중복으로 발생한다
따라서 실시간으로 입력된 값을 반영하기 위해서는 onCompositionEnd
이벤트를 함께 처리해야 한다는 내용이었다
코드를 아래처럼 const [isComposing, setComposing] = useState(false)를 추가한 뒤,
onCompositionStart={() => setComposing(true)}
onCompositionEnd={() => setComposing(false)} 속성을 넣고
submitOnEnter에서 isComposing이 true인 경우 리턴을 하고
TODO를 작성한 뒤 엔터를 입력했더니
의도한 대로 submitOnEnter는 한 번만 작동되어서
두 번 alert가 뜨는 에러와 끝 글자만 잘려서 전송되는 에러를 해결할 수 있었다!
해당 방법은 TODO 추가하기 기능에 구현했다.
TODO 수정하기 기능에도 적용하기 위해서, 이번에는 위 블로그 글에 소개된 다른 방법인, 리액트의 키보드 이벤트 타입을 확장해서 아래처럼 사용하는 방법을 시도해보았다.
처음에는 KeyboardEvent에서 확장한 인터페이스를 만들어서, if (event.isComposing) return을 넣어주려고 했으나
interface CustomKeyboardEvent extends KeyboardEvent {
isComposing: boolean;
}
> 70 | onKeyDown={(event: CustomKeyboardEvent) => {
| ^^^^^^^^^
71 | if (event.isComposing) return;
72 | }}
// 오류 메시지
TS2322: Type '(event: CustomKeyboardEvent) => void' is not assignable to type 'KeyboardEventHandler<HTMLInputElement>'.
Types of parameters 'event' and 'event' are incompatible.
Property 'isComposing' is missing in type 'KeyboardEvent<HTMLInputElement>' but required in type 'CustomKeyboardEvent'.
위처럼 이벤트 핸들러 함수의 타입이 일치하지 않기 때문에 발생하는 오류 때문에
해결책을 찾아 보았더니 event.nativeEvent.isComposing 을 사용하는 방법이 있었다.
https://velog.io/@o1_choi/isComposing
https://ko.legacy.reactjs.org/docs/events.html#composition-events
이전 방법에서는 state를 만들고, input에 onCompositionStart와onCompositionEnd 옵션들을 넣어줘야 했다면
// 이전 방법 3. Composition Events 사용하기
const [isComposing, setComposing] = useState(false);
...
<input
..
onCompositionStart={() => setComposing(true)}
onCompositionEnd={() => setComposing(false)}
/>
event.nativeEvent.isComposing을 사용하는 방법으로는
간단하게 if (e.nativeEvent.isComposing) return 만 사용하면 해결할 수 있었다
// 4. nativeEvent 어트리뷰트 사용하기
const submitOnEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.nativeEvent.isComposing) return;
if (e.key === "Enter") {
handleEditSubmit();
console.log("submitOnEnter 작동");
}
};
완성된 ‘nativeEvent 어트리뷰트 방법’은 ‘수정하기’ 기능에 추가했더니 잘 작동하는 것을 볼 수 있었다.
// 첫번째 해결 방법
import React, { useContext, useRef, useState, ChangeEvent } from "react";
import { dispatchContext } from "../../context/todoContexts";
const TodoCreate = () => {
const [inputTodo, setInput] = useState("");
const [isComposing, setComposing] = useState(false);
const todos = useContext(dispatchContext);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setInput(e.target.value);
console.log(inputTodo);
};
const handleButtonClick = () => {
if (inputTodo) {
todos.addTodo(inputTodo);
setInput("");
} else {
alert("할 일을 입력해주세요!");
}
};
const submitOnEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (isComposing) return;
if (e.key === "Enter") {
handleButtonClick();
console.log("submitOnEnter 작동");
}
};
return (
<div className="flex w-full mb-8">
<>
<input
type="text"
className="flex-grow mr-2"
data-testid="new-todo-input"
onKeyDown={submitOnEnter}
onCompositionStart={() => setComposing(true)}
onCompositionEnd={() => setComposing(false)}
value={inputTodo}
onChange={(e) => handleChange(e)}
/>
<button
type="button"
className="w-1/5 base_button"
data-testid="new-todo-add-button"
onClick={handleButtonClick}
>
추가
</button>
</>
</div>
);
};
export default TodoCreate;
// 두번째 해결 방법
import React, { useContext, useState, ChangeEvent } from "react";
import { dispatchContext } from "../../context/todoContexts";
interface Props {
id: number;
isCompleted: boolean;
todo: string;
}
const TodoItem = ({ id, isCompleted, todo }: Props): React.ReactElement => {
const todos = useContext(dispatchContext);
const [editMode, setEditMode] = useState(false);
const [editTodo, setEditTodo] = useState<string>("");
const [isCompletedState, setCompleted] = useState(isCompleted);
const handleEditButtonClick = () => {
setEditMode(!editMode);
};
const handleEditSubmit = () => {
if (editTodo) {
todos.updateTodo(editTodo, isCompleted, id);
setEditMode(false);
setEditTodo("");
} else alert("수정할 내용을 입력해 주세요.");
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setEditTodo(e.target.value);
console.log("editTodo: ", editTodo);
};
const handleDeleteSubmit = (id: number) => {
todos.deleteTodo(id);
};
const handleCheckSubmit = (e: ChangeEvent<HTMLInputElement>) => {
setCompleted(e.target.checked);
todos.updateTodo(todo, e.target.checked, id);
};
const submitOnEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.nativeEvent.isComposing) return;
if (e.key === "Enter") {
handleEditSubmit();
console.log("submitOnEnter 작동");
}
};
return (
<li className="mb-2 list-none text-lg" key={id}>
<label className="flex w-full items-center">
<div className="flex items-center flex-grow mr-2">
<div className="checkContainer mr-3 w-5 h-5 flex">
<input
className="appearance-none mr-2 text-lg w-4 h-4 flex-none
cursor-pointer rounded-md"
type="checkbox"
checked={isCompletedState}
onChange={(e) => handleCheckSubmit(e)}
></input>
<div className="checkIcon cursor-pointer">
{isCompletedState && "✔︎"}
</div>
</div>
{editMode ? (
<input
className="edit_input "
defaultValue={todo}
data-testid="modify-input"
onKeyDown={submitOnEnter}
onChange={(e) => handleChange(e)}
></input>
) : (
<span className={isCompleted ? "completed" : ""}>{todo}</span>
)}
</div>
<div className="flex items-center">
{editMode ? (
<button
className="base_button w-20 mr-3"
onClick={() => handleEditSubmit()}
data-testid="submit-button"
>
제출
</button>
) : (
<button
className="base_button w-20 mr-3"
data-testid="modify-button"
onClick={() => handleEditButtonClick()}
>
수정
</button>
)}
{editMode ? (
<button
className="gray_button w-20"
data-testid="cancel-button"
onClick={() => handleEditButtonClick()}
>
취소
</button>
) : (
<button
className="gray_button w-20"
data-testid="delete-button"
onClick={() => handleDeleteSubmit(id)}
>
삭제
</button>
)}
</div>
</label>
</li>
);
};
export default TodoItem;