ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Custom Hook에 ref를 전달하기 (Feat. React-Quill)
    Studying/React 2023. 7. 14. 17:03

    문제 발생

    React-Quill에서 이미지를 삽입할 때 사용할 이미지 핸들러를 만들고 사용하려는 과정에서 발생한 문제입니다.

    이미지 핸들러에 대한 커스텀 훅을 만들었고, 이미지 핸들러는 ref를 통해 <ReactQuill> DOM에 접근해서 현재 마우스 커서 위치를 파악해야 하는데, 커스텀 훅에 ref 값을 전달할 방법이 없었습니다.

     

    <ReactQuill>에 다음과 같이 ref를 등록했습니다.

    const quillRef = useRef<ReactQuill>();
    
    return (
          <ReactQuill
            ref={(element) => {
              if (element !== null) {
                quillRef.current = element;
              }
            }}
          />
      )

     

    useImageHandler 훅에게 초기값을 전달하고 반환 값을 받습니다.

    const [imageUrlHandler, imageHandler] = useImageHandler({
    	quillEditor: quillRef.current?.getEditor(),
    })
    • 전달 값: ref 값에 접근해서 getEditor()를 통해 에디터 객체만 뽑아 전달한다.
    • 반환 값: 이미지를 핸들링하는 imageUrlHandler, imageHandler 함수를 반환한다.

    문제는 바로 이 부분이었습니다.

     

    커스텀 훅에 초기값이 전달되는 시점에서 ref 값은 undefined인 것입니다.

     

    ReactQuill이 DOM으로 형성되기 전에 훅에 초기값이 전달되기 때문입니다.

     

    그래서 React Hook-flow에 대해 다시 한 번 찾아보았습니다.

     

    🎁 React Hook-flow 이해하기

     

    참고 블로그를 꼼꼼히 읽던 중, 지금껏 제가 잘못 판단하고 있던 부분을 발견했습니다.

     

    https://sambalim.tistory.com/153

    useState의 초기값을 가지고 DOM 업데이트 라는 부분이었습니다.

     

    커스텀 훅은 왜인지 useEffect 훅이 DOM 업데이트 이후에 실행되는 것처럼 DOM 업데이트 후에 실행된다고 생각했습니다. 초기값이 등록되는 것까지 DOM 업데이트 이후라고 착각한 것 같습니다.

     

    괜히 콘솔에 훅 플로우를 찍어보기까지 했습니다. QuillEditor.tsx 파일이 실행되고 발생하는 훅 플로우 입니다.

     

    콘솔에 출력한 훅 플로우

    QuillEditor의 컴포넌트가 렌더링 되기 전에 커스텀 훅이 실행되는 게 보입니다.

     

    그렇다면 커스텀 훅에 ref 값을 전달하는 방법은 아예 없는 걸까요?

     

     

    해결 방법

    매우 단순한 해결 방법이 있었습니다.

     

    imageHandler 함수 props로 ref를 전달하면 됩니다.

    왜 커스텀 훅의 초기값으로 ref를 전달한 뒤, imageHandler 함수에서 사용하는 방법만 고집한 건지 모르겠습니다.

     

    다음 코드가 해결책을 반영한 imageHandler, imageUrlHandler 입니다.

    const imageUrlHandler = useCallback((editor: any) => {
        const range = editor.getSelection();
        const url = prompt("");
        if (url) {
          editor.insertEmbed(range.index, "image", url);
        }
      }, [])
    
      const imageHandler = useCallback((editor: any) => {
        const input = document.createElement("input");
        input.setAttribute("type", "file");
        input.setAttribute("accept", "image/*");
        input.setAttribute("name", "file");
        input.click();
    
        input.onchange = async (event: any) => {
          const file: File = event?.target?.files[0];
          const formData = new FormData();
          formData.append("file", file);
          uploadImage(formData)
            .then((res) => {
              const range = editor.getSelection();
              editor.insertEmbed(range.index, "image", res.imageUrl);
              editor.setSelection(range.index + 1);
            })
        }
      }, [])

    과 같이 editor를 인자로 받아 사용함으로써 imageHandler를 훅으로 분리하되, ref.current.getEditor()도 접근할 수 있게 되었습니다.

     

    any 타입을 쓴 이유는...

    editor의 경우 Quill 타입이라는데, 빠른수정을 통해 import가 안 되어서 명시하지 못 했습니다.

    event의 경우 React의 ChangeEvent라고 명시했더니 event.target.files[0]가 null일 수 있다고 하여 보류했습니다.

     

    Quill Editor의 모듈도 다음과 같이 정의합니다.

    const modules = useMemo(
        () => ({
          toolbar: {
            container: [
              [{ 'header': [1, 2, 3, 4, 5, 6, false] }],
              [{ 'font': [] }],
              ["bold", "italic", "underline", "strike", "blockquote"],
              [{ color: [] }, { 'background': [] }],
              [
                { list: "ordered" },
                { list: "bullet" },
                { indent: "-1" },
                { indent: "+1" },
              ],
              ['link'],
              ['image', 'video'],
              ['clean']
            ],
            handlers: {
              imageUrl: () => imageUrlHandler(quillRef.current?.getEditor()),
              image: () => imageHandler(quillRef.current?.getEditor()),
            },
          },
        }), []);

    handlers에 imageUrl, image를 훅을 통해 받은 핸들러 함수로 지정해줍니다.

     

    prop을 전달하지 않을 경우 imageUrl: imageUrlHandler 와 같이 단순하게 적을 수 있겠지만, 하는 수 없이 저만큼 길게 적혔습니다.

     

    마음에 들지 않는 부분이라면 quillRef.current?.getEditor() 같이 긴 코드가 두 번이나 반복되어 적힌 것입니다. 그런데 editor 변수를 정의하여 바깥에 빼둘 경우 quillRef가 undefined일 때 정의되기 때문에 방법이 없었습니다.

     

    editor를 상태로 관리하여 useEffect가 발생할 때 editor를 갱신하고,(그 시점에는 DOM이 생성되었으니까)

    handlers에  imageHandler(editor) 와 같이 정의했는데,

    handlers가 정의되는 순간 editor는 마찬가지로 초기값 undefined이기 때문에 별 소용이 없었습니다.

     

    하여튼 훅으로 분리하는 것에 성공해서 다행이라는 생각이 듭니다.

     


     

    + 그런데 이 당시 왜 훅으로 분리한 건지 잘 기억이 안 나 모르겠고, 그냥 유틸 함수로 분리하는 게 더 목적에 부합하지 않나 생각이 듭니다.

     

Designed by Tistory.