ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React / 에러 핸들링] useClickAway훅 만들기
    React 2022. 2. 4. 23:03

    원티드에서 진행하는 프리온보딩 코스 과제를 하면서 컴포넌트 외부에 있는 클릭 이벤트를 감지하는 작업이 많아졌습니다.

    따라서 내가 원하는 컴포넌트 외의 영역에 클릭 이벤트가 발생한다면 원하는 동작을 하는 useClickAway라는 훅을 만들게 되었습니다.

    사실 동작 자체는 굉장히 간단했습니다.

     

     

    초기 코드

    아래는 제가 처음에 만든 코드입니다.

    import { useEffect, useRef } from 'react';
    
    export const useClickAway = (awayEvent: () => void) => {
      const ref = useRef<HTMLElement>(null);
      const callback = (e: MouseEvent) => {
        const element = ref.current;
        if (!element) {
          return;
        }
        if (!element.contains(e.target as Node)) {
          awayEvent();
        }
      };
      useEffect(() => {
        document.body.addEventListener('click', callback);
        return () => document.body.removeEventListener('click', callback);
      }, []);
      return ref;
    };

    코드 자체는 굉장히 간단합니다.

    useClickAway훅은 콜백함수로 awayEvent를 받습니다.

    또한 ref를 return하는 것을 볼 수 있습니다. ref에는 내가 원하는 컴포넌트에 넣어주면 될 것 같습니다.

     

    callback함수에는 click이벤트가 발생했을 때 실행하도록 하였습니다.

    그렇다면 callbac함수에는 어떤 로직이 들어가야 할까요?

     

    바로 클릭된 타깃이 ref.current 즉, 컴포넌트에 포함되어있는지 확인하면 될 것 같습니다.

    따라서 포함이 되지않는다면 다른 영역을 선택한 것이므로 awayEvent를 실행해주면 될 것 같습니다.

    예를 들어 다른 영역에 포함되어 있다면 모달을 끄는 이벤트를 실행시킨다거나 그러면 잘 동작할것 같습니다.

     

     

    오류 발견

    하지만 잘 사용하고 있던 저에게도 큰 문제가 다가왔습니다.

    문제는 ClickToEdit컴포넌트를 만들면서 나타났습니다.

    출처:&amp;nbsp;https://codestates.notion.site/5f83f7a007664f1abcf0cdbcbbbbd521

     

    위와 같은  컴포넌트를 만들었어야 했는데요, input외의 영역을 클릭했을 때, 반영이 되는 컴포넌트입니다. 그렇다면 결국 사용자는 input의 값이 어떤지, 어떻게 변화하는지는 몰라도 되며, 단순히 상위 컴포넌트에서 밑에 있는 text의 상태만 잘 변경하면 됩니다.

    즉 저는 아래와 같이 상위 컴포넌트에서 사용하길 원했습니다.

            이름 <ClickToEdit setReflectInputValue={setName}></ClickToEdit>
            나이 <ClickToEdit setReflectInputValue={setAge}></ClickToEdit>
            이름 {name} 나이 {age}

    그렇다면 ClickToEdit컴포넌트를 한번 확인해 보겠습니다.

     

    ClickToEdit.tsx

    interface Props {
      setReflectInputValue: Dispatch<SetStateAction<string>>;
    }
    const ClickToEdit = ({ setReflectInputValue }: Props) => {
      const [inputValue, setInputValue] = useState('');
      const inputRef = useClickAway(() => {
        setReflectInputValue(inputValue);
      });
    
      const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
        setInputValue(e.target.value);
      };
      return (
        <Input
          ref={inputRef as MutableRefObject<HTMLInputElement>}
          value={inputValue}
          onChange={handleInputChange}
        />
      );
    };
    export default ClickToEdit;

    하지만 여기서 문제가 발생했는데요 useClickAway에서 콜백으로 넘겨준 부분에서 inputValue가 계속 초기값인 ""빈 스트링을 가지고 있는것이었습니다. 

    여태 잘 쓰고 있는 useClickAway였는데, 문제가 생기니 처음에는 ClickToEdit컴포넌트의 문제일거라 생각했습니다.

    하지만 디버깅을 해보니 ClickToEdit컴포넌트는 아무런 잘못이 없었죠.

     

    다음으로 제가 생각해본것은 ref였습니다. ref는 한번 설정된 다음 변하지 않습니다.

    컴포넌트가 리렌더링되더라도 변하지 않죠.

    그렇기 때문에 DOM객체를 직접 사용하는 용도 외에도 상수를 보관하기 위해 사용하기도 합니다.

    따라서 초기에 설정이 될 때의 input값인 inputValue를 가지고 있어서 변경이 되지 않나?라는 생각을 했습니다. 하지만 이 또한 잘못되었죠. 제가 설정한 ref는 inputValue가 아니라 DOM을 저장하고 있기 때문입니다.

     

    마지막으로 생각한것이 의존성 배열입니다. 어딘가에 의존성 배열을 작성하지 않았다는 생각이 들기 시작합니다. 사실 글로 쓰면 굉장히 짧은 시간으로 보이지만 꽤 많은 시간 고민하고 디버깅했기 때문에 남은 건 의존성 배열밖에 남아있지 않았습니다. 그런데, 저는 ClickToEdit이라는 함수를 사용하면서 의존성 배열을 만든 적이 없습니다. 따라서 여태 잘 사용했지만 useClickAway훅을 다시 한번 자세히 살펴보기로 합니다.

     

     

    문제의 발견

      useEffect(
        () => {
          document.body.addEventListener('click', callback);
          return () => document.body.removeEventListener('click', callback);
        },[]
     );

    그렇습니다.. 문제는 바로 이 부분에서 발생했습니다. useEffect에는 의존성 배열이 없는것을 확인할 수 있습니다. 따라서 초기의 callback함수, 즉 초기의 awayEvent만을 계속 가지고 있는 것이죠.

    하지만 초기의 awayEvent의 경우 아래처럼 당연히 inputValue는 "" 임을 알 수 있습니다. 따라서 inputValue가 업데이트되더라도 useEffect는 나 몰라라 하는 것이었습니다. 

     const inputRef = useClickAway(() => {
        setReflectInputValue(inputValue);
      });

    때문에 useEffect에서 의존성 배열을 추가해주는 로직을 만들었고, useClickAway훅에서는 두 번째 인자로 dependency를 추가할 수 있도록 하였습니다.

     

    useClickAway에 의존성 배열 추가하기

      const inputRef = useClickAway(() => {
        setReflectInputValue(inputValue);
      }, [inputValue]);

     

     

    최종 useClickAway 코드

    import { useEffect, useRef } from 'react';
    
    export const useClickAway = (awayEvent: () => void, dep?: any[]) => {
      const ref = useRef<HTMLElement>(null);
      const callback = (e: MouseEvent) => {
        const element = ref.current;
        if (!element) {
          return;
        }
        if (!element.contains(e.target as Node)) {
          awayEvent();
        }
      };
      useEffect(
        () => {
          document.body.addEventListener('click', callback);
          return () => document.body.removeEventListener('click', callback);
        },
        dep ? [...dep] : [],
      );
      return ref;
    };

     

     

    느낀 점

    react hook을 사용하면서 dependency를 잘 넣어주어야 한다고 했지만, 실제로 에러를 만나게 되니 정말 파악하기 힘들었습니다. 앞으로 커스텀 훅을 만들 때 의존성 배열에 대한 부분을 잘 생각하며 만들어야겠다고 생각했습니다.

    댓글

Designed by Tistory.