[Boated] 컴포넌트 더블 클릭시, input으로 변경 및 외부 영역 클릭 시 API 요청 (Click to edit)

SaaaHo·2022년 8월 18일
0
post-thumbnail


Boated 프로젝트의 Kanban 페이지에서 column의 header부분이 더블 클릭시 input으로 이름을 변경하고, 외부 영역을 클릭시 API 요청이 가는게 좋겠다는 기획이 나왔다.

  • 더블 클릭시 edit가 되는걸 click to edit 이라고 한다.
  • 찾아보니 npm에 패키지도 있다. (npm-react-click-to-edit)

React element 이벤트에 onDoubleClick이 있어서 직접 구현해도 되겠다고 생각했다.

  1. 제목이 있는 헤더 부분 더블 클릭시, input으로 바꿔주고, input focus 해주기.
  2. input onChange 이벤트에 바꾸려는 제목 텍스트를 받는 함수 등록해주기.
  3. 헤더 외의 영역을 클릭시, 외부 영역 클릭을 탐지해서 input을 다시 header text 컴포넌트로 바꿔주기.
  4. 바뀐 header의 제목을 API 요청하기

위의 순서대로 진행하기로 했다.

구현

// 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;
  • 기존에 외부영역을 탐지해주는 hook인 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에 담게했다.

  • 그 다음으로 더블클릭시 input에 자동으로 focus를 해주고 싶어서 input에 ref를 달려고 했는데, vscode에서 빨간줄이 떴다.

	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]);
  • 그래서 input의 렌더링을 결정하는 isEditable을 useEffect의 depth에 넣어주니, focus 되었다.



이제 외부 클릭시 바뀐 이름을 들고 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의 이벤트는 초기 렌더링에만 작용하고, 부차적인 리렌더링에는 작용하지 않는다.


References

TypeScript React에서 useRef의 3가지 정의와 각각의 적절한 사용법

Typescript에서 useRef Error

How to focus something on next render with React Hooks

Handle React state inside an event listener callback

profile
프론트엔드 주니어 개발자

0개의 댓글