Boated 프로젝트의 Kanban 페이지에서 column의 header부분이 더블 클릭시 input으로 이름을 변경하고, 외부 영역을 클릭시 API 요청이 가는게 좋겠다는 기획이 나왔다.
React element 이벤트에 onDoubleClick이 있어서 직접 구현해도 되겠다고 생각했다.
위의 순서대로 진행하기로 했다.
// useDetectOutsideClick.ts
import React, { useEffect, useState } from 'react';
const useDetectOutsideClick: any = (el: React.RefObject<HTMLDivElement>, initialState: boolean) => {
const [isActive, setIsActive] = useState(initialState);
useEffect(() => {
const pageClickEvent = (e: Event) => {
if (el.current && !el.current.contains(e.target as Node)) {
setIsActive(!isActive);
}
};
if (isActive) {
window.addEventListener('click', pageClickEvent);
}
return () => {
window.removeEventListener('click', pageClickEvent);
};
}, [isActive, el]);
return [isActive, setIsActive];
};
export default useDetectOutsideClick;
const kanbanHeaderRef = useRef<HTMLDivElement>(null);
const [isEditable, setIsEditable] = useDetectOutsideClick(kanbanHeaderRef, false);
...
...
<Styled.KanbanHeader
ref={kanbanHeaderRef}
{...provided.dragHandleProps}
onMouseEnter={() => isEditable || setIsIconVisible(true)}
onMouseLeave={() => setIsIconVisible(false)}
onDoubleClick={onDoubleClickHeaderName}
>
영역부분을 설정하기 위해서 useRef를 이용해서 kanbanHeaderRef를 선언하고, 설정하고 싶은 영역 HTML 태그에 ref를 달아주면 된다.
해당영역의 외 부분이 클릭되면, isEditable 반대값인 !isEditable 로 state가 변경된다.
onDoubleClick 이벤트에는 setIsEditable(true)를 통해 isEditable을 true 로 변경시키게 했다.
{isEditable ? (
<Styled.HeaderChangeInput
type="text"
id="header-name"
name="header-name"
width={200}
height={40}
maxLength={15}
value={changedHeaderName}
onChange={onChangeHeaderName}
ref={kanbanHeaderInputRef}
/>
) : (
<Text fontSize={14} fontFamily={'Gmarket Sans'} color={Theme.S_0}>
{name}
</Text>
)}
Styled.KanbanHeader 컴포넌트 안의 부분을 삼항연산자로 isEditable의 상태에 따라 렌더링을 다르게 해주었다.
true의 경우는 input을 렌더링하고, false인 경우는 text로 보여주기.
onChangeEvent에는 input의 e.target.value를 state인 changedHeaderName에 담게했다.
if (kanbanHeaderInputRef.current) {
console.log(kanbanHeaderInputRef.current)
kanbanHeaderInputRef.current.focus();
}
// 밑에와 같이 쓰면 조건을 안달아줘도 됨.
// const kanbanHeaderInputRef = useRef() as React.MutableRefObject<HTMLInputElement>;
찾아보니, 조건을 달아줘야 했다. current가 null인지 조건을 통해 확인을 해줘야한다고 한다.
조건을 쓰고싶지 않으면, useRef를 선언할때 위의 주석처럼 해주면 조건을 걸어주지 않아도 된다고 한다.
이제 focus가 문제없이 될 줄 알았는데, 작동을 하지 않았다.
계속 inputRef의 current가 undefined로 되어서 조건문에 들어가지지 않은것이다.
(console.log로 조건문 위에서 찍어보니 undefined로 나왔다)
내 생각에, 삼한연산자로 input이 렌더링 되거나 안되게 해놨으니, input이 렌더링 될때 바로 current를 못불러와서 undefined로 뜬거 같다. 맨처음 더블클릭은 안되는데, 다시하면 되었기 때문에 그렇게 생각했다.
// current가 있을때 focus
useEffect(() => {
if (isEditable && kanbanHeaderInputRef.current) {
kanbanHeaderInputRef.current.focus();
}
}, [isEditable]);
이제 외부 클릭시 바뀐 이름을 들고 API 요청을 해줘야 하는데, 쓰고 있던 useDetectOutside의 return state를 반대로 해주는 로직 부분에서 API 요청을 해줘야하기 때문에, 기존거를 사용하지 않고 살짝 수정해서 사용하기로 했다.
const useDetectHeaderOutsideClick: any = (el: React.RefObject<HTMLDivElement>, initialState: boolean) => {
const [isActive, setIsActive] = useState(initialState);
useEffect(() => {
const pageClickEvent = async (e: Event) => {
if (el.current && !el.current.contains(e.target as Node)) {
setIsActive(!isActive);
// 기존 이름과 바뀐 이름이 다르면 API 요청
if (name !== changedHeaderName) {
await putProjectsKanbanLaneName({ projectId, kanbanLaneId: id, name: changedHeaderName });
}
}
};
if (isActive) {
window.addEventListener('click', pageClickEvent);
}
return () => {
window.removeEventListener('click', pageClickEvent);
};
}, [isActive, el, changedHeaderName]);
return [isActive, setIsActive];
};
기존 이름과 같으면 API 요청을 해 줄 필요가 없기 때문에, 다를때만 API 요청을 하게 해주었다.
처음에는 이 부분의 useEffect depth에 changedHeaderName이 없었는데,
없으면 처음 이름 변경시 API 요청이 안되었다. (두번째로 시도하면 됨)
이유를 생각해봤는데, addEventListener로 외부영역 탐지 이벤트가 등록될때, changedHeaderName이 기존 처음 state값인 name으로 똑같이 등록된 상태로 고정이 되어서 if문 안으로 들어가지 못했을거라고 생각한다. (실제로 console.log로 찍어봤을때도 값을 변경했지만 기존 초기값으로 되어있었다)
찾아보니, addEventListener는 초기 state만 접근한다고 한다. 왜냐하면 eventListener는 초기 렌더링에만 작용하고, 부차적인 리렌더링에는 작용하지 않는다고 한다. (React useState hook event handler using initial state)
그래서 depth로 changedHeaderName을 넣어서 해당 값이 바뀔때마다 탐지시키게 변경시켜주니, 정상적으로 잘 작동했다.
근데 이러면 changedHeaderName이 바뀔때마다 계속 새로 함수를 만들어서 성능면에서 안좋을거 같은 생각이 든다.
typescript에서 useRef를 사용할때, 타입에 맞게 설정해서 사용해야 한다.
addEventListener의 이벤트는 초기 렌더링에만 작용하고, 부차적인 리렌더링에는 작용하지 않는다.
TypeScript React에서 useRef의 3가지 정의와 각각의 적절한 사용법