[React] textarea tab 커서 위치 문제

Wooki·2023년 1월 30일
1

트러블슈팅

목록 보기
2/3
post-thumbnail

블로그 서비스 만들기 프로젝트를 진행하던 도중
textarea태그로 블로그 포스트의 내용을 작성하는 부분을 구현하는 중에 막히는 부분이 발생했다.

기존 코드

const [contents, setContents] = useState("");

// textarea 태그 내에서 tab 키 처리
const textAreaInputTab = (e) => {
    if (e.key === "Tab") {
      e.preventDefault();
      setContents(contents + "\t");
      return false;
    }
  };
 <textarea
	onChange={(e) => {
      setContents(e.target.value);
    }}
    onKeyDown={(e) => {
      textAreaInputTab(e);
    }}
    value={contents}
></textarea>

html에서 textarea태그에서 tab키를 사용하면 다음 input태그로 포커스가 이동된다.

텍스트 영역 내에 tab을 넣기 위해서는 textarea태그의 onkeyDown속성을 이용할 수 있다고 해서 위처럼 작성을 했다.

문제점

위 코드처럼 작성을 하니 문자 작성중에 탭 키가 정상적으로 반응 하지만 탭 키를 누르면 항상 textarea 의 마지막에 tab 키가 들어간다

setContents(contents + "\t")

탭 키를 textarea의 value값으로 사용하고 있는 contents의 뒤에 탭을 넣겠다고 코드를 작성했기 때문에 textarea의 커서를 기준으로 tab을 넣기 위해 코드를 변경했다.

 const textAreaInputTab = (e) => {
    if (e.key === "Tab") {
      let textarea = e.target;
      let value = textarea.value;
      let start = textarea.selectionStart;
      let end = textarea.selectionEnd;
      
      e.preventDefault();
      setContents(value.substring(0, start) + "\t" + value.substring(end));
    }

커서의 위치를 찾기 위해서 HTMLTextAreaElement에 내장되어 있는 selectionStart,selectionEnd를 이용해서 사용자의 커서를 기반으로 탭이 입력되게 수정하였다.

꼬리를 무는 문제점

이제 커서를 기반으로 탭이 입력되는데는 문제가 없는데 다른 문제가 생겼다.
textAreaInputTab 함수가 처리되고 나면 커서가 textarea의 가장 마지막으로 이동되는 문제가 발생했다.

예시


시도 1

이를 해결하기 위해서 textAreaInputTab 함수에 setSelectionRange()를 이용해 봤다.

  const textAreaInputTab = (e) => {
    if (e.key === "Tab") {
      e.preventDefault();
      let textarea = e.target;
      let value = textarea.value,
        start = textarea.selectionStart,
        end = textarea.selectionEnd;
      setContents(value.substring(0, start) + "\t" + value.substring(end));
      textarea.setSelectionRange(start + 1, start + 1);
    }
  };

하지만 이 함수가 끝나고 re-render가 진행되고 나면 다시 커서가 textarea의 마지막에 이동해있다.

해결

아무래도 state인 contents가 변경되면서 textAreaInputTab 함수에서 setSelectionRange함수를 적용하더라도 리렌더링 되며 textarea의 value가 다시 쓰여지며 커서의 위치가 변경되는것 같았다.

setSelectionRange를 렌더링이 된 다음 실행되도록 하면 괜찮지 않을까 하는 생각에
cursor state를 하나 만들어서 tab이 눌리면 cursor의 위치를 저장하고,
useEffect Hook을 이용해서 cursor가 re-render 되면 커서의 위치를 다시 설정하도록 했다.

  const [cursor, setCursor] = useState(0); //탭 입력 후 커서의 위치
  const taRef = useRef(); // textarea에 ref 연결


// Tab 키가 입력되었을 때 실행되는 함수
  const textAreaInputTab = (e) => {
    if (e.key === "Tab") {
      e.preventDefault();
      let value = e.target.value,
        start = e.target.selectionStart,
        end = e.target.selectionEnd;

      setContents(value.substring(0, start) + "\t" + value.substring(end));
      setCursor(start + 1);
      // "\t"가 입력되었으므로 기존 커서 위치에서 +1
    }
  };


// cursor가 변경될 때만 커서를 다시 잡도록 한다.
// tab key입력이 있을때만 cursor의 값이 변경되므로 tab키 입력시에만 커서를 다시
  useEffect(() => {
    taRef.current.setSelectionRange(cursor, cursor);
  }, [cursor]);
 잡는다.

기존 코드가 setSelectionRange -> re-render -> textArea value 변경 의 순으로 동작한다면
수정 코드에서는 re-render -> textArea value 변경 -> setSelectionRange
의 순서로 동작하도록 변경

💬 TIL

문제 원인을 잘 모르겠어서 차근차근 코드의 동작을 생각해보고 state의 동작 순서에 대해서 찾아볼 수 있었고,
HTMLTextareaElement Docs를 찾아보며 textarea의 다양한 속성과 인스턴스 메소드들에 대해서 공부할 수 있었다.


잡담

포스트를 작성하고 다시 읽어보면서
if(e.key === "Tab")를 탭이 눌리면 실행되는 함수 안에 넣어버렸다는 바보같은 실수를 했다는 것을 깨달았다.. 어서 수정하러 가야겠다..ㅎ

  const textAreaInputTab = ({ value, selectionStart, selectionEnd }) => {
    setContents(
      value.substring(0, selectionStart) + "\t" + value.substring(selectionEnd)
    );
    setCursor(selectionStart + 1);
  };

수정하는 김에 구조분해 할당으로 함수를 수정했다.

profile
웹 개발자

0개의 댓글