엔터 키로 전송 시 끝 글자만 전송되는 문제 해결하기

chichi·2023년 7월 11일
1
post-thumbnail

완성된 모습


엔터 키 전송 시 끝 글자만 전송되는 모습

엔터로 투두 등록을 하고 싶은데, 아래와 같이 코드를 작성했을 때 한글의 끝 글자만 전송되는 문제가 있었다.

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;

해당 문제를 해결하기 위해 시도한 내용들을 기록하고자 한다.

1. onKeyUp으로 바꾸면 어떨까 ?

inputRef로 input에 작성되는 글을 가져온 뒤 엔터를 누르면

input의 onKeyDown으로 submitOnEnter이 실행되고,

handleButtonClick()을 실행시키는 것을 의도했었는데

submitOnEnter이 두 번 실행되어서 handleButtonClick도 두 번 실행되는 문제가 있었다.

handleButtonClick이 두 번 실행되는 게, onKeyDown의 문제일까 싶어서 onKeyUp으로 바꿨더니

여전히 두 번 실행되면서

이번에는 첫 실행에서 TODO가 추가되고, 두번째 실행에서 (빈 내용이므로 else문의) alert가 실행 되거나

반대로 첫 실행에서 alert가 실행되고, 두번째 실행에서 TODO가 추가되었다

2. onKeyUp을 onKeyDown으로 바꾸고, useRef가 아닌 useState와 onChange를 사용하면 ?

useRef로 값을 가져오는 방법에 대해서도 좀 더 알아봤더니,

검색했을 때 내가 작성한 것과 같은 Ref의 사용 사례가 많지 않았고

Ref는 남용해서는 안된다는 공식 문서의 내용이 있었다

구글링 했을 때, 저장된 값을 가져오는 방법으로는 보편적으로 useState를 사용하는 것으로 보였다

https://velog.io/@imzzuu/React-Input-control-과-useRef의-적절한-사용-Input-유효성-검사

그래서 useRef로 가져오는 것은 적합한 방법이 아닌 것 같아 onChange와 useState를 사용, onKeyDown으로 전송하는 방법으로 수정했더니 정상 작동할 것으로 예상 했지만…

다시 처음처럼 마지막 글자만 전송되는 문제가 있었다.

💡 3. Composition Events 사용하기!

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 추가하기 기능에 구현했다.

💡 4. 다른 방법, nativeEvent 어트리뷰트 사용하기

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;

0개의 댓글