블로그 서비스 만들기 프로젝트를 진행하던 도중
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의 가장 마지막으로 이동되는 문제가 발생했다.
이를 해결하기 위해서 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
의 순서로 동작하도록 변경
문제 원인을 잘 모르겠어서 차근차근 코드의 동작을 생각해보고 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);
};
수정하는 김에 구조분해 할당으로 함수를 수정했다.