[Practice]react-quill 이미지, 비디오 삽입 및 사이즈 조절

이진솔·2024년 2월 16일
post-thumbnail

react-quill

텍스트 편집기 라이브러리로 반응형 디자인 적용도 잘되고 사용하기 쉽지만 나는 여러모로 애를 먹었다... 그 중 가장 힘들었던 몇 가지를 정리해보고자 한다!

1. image-handler

react-quill(이하 퀼)의 경우 이미지 및 비디오를 삽입하면 엄청나게 긴 src가 삽입되는데 큰 문제는 없지만 나중에 무리가 될 수 있을 것 같아서, 그리고 파일들을 파이어스토어에 저장하기 위해서 이미지 핸들러 함수를 만들었다.

다른 블로그를 봤을때 편집기에 이미지를 삽입하는 즉시 파이어스토어에 저장 -> 편집기에서 이미지를 지울 때 파이어스토어에서 삭제 -> 저장하지 않고 나갈 때에도 파이어스토어에서 삭제하시는 것 같았다.

하지만 내 생각에는 불필요한 서버 통신이라고 생각되었고 서버통신은 즉 비용!!이라고 생각해서 줄일 수 있는 방법을 생각했다.

그래서 내가 생각한 방법은! 일단 createObjectUrl 메서드를 이용해서 짧아진 previewURL을 우선 삽입, 그리고 저장시 필요한 파일 이름과 파일 그리고 previewURL을 useState 객체에 저장했다.

export interface IFile {
  name: string;
  previewURL: string;
  file: File;
}

const [images, setImages] = useState<IFile[]>([]);

  const imageHandler = () => {
    const input = document.createElement("input");
    input.setAttribute("type", "file");
    input.setAttribute("accept", "image/*");
    input.setAttribute("multiple", "true");
    input.click();

    input.onchange = async (event: any) => {
      const imageFiles: FileList = event?.target?.files;
      setIsResizing(true);
      const resizePromises = Array.from(imageFiles).map(async (file) => {
        const id = file.name;
        const previewURL = URL.createObjectURL(file);
        const imageFile = (await resizeFile(file)) as File;

        if (quillRef.current) {
          const Image = Quill.import("formats/image");
          const editor = quillRef.current.getEditor();
          const range = editor.getSelection();
          Image.sanitize = (imageFileURL: string) => imageFileURL;
          editor.insertEmbed(range?.index as number, "image", previewURL);
        }

        return { name: id, previewURL, file: imageFile };
      });
      const resizedImages = await Promise.all(resizePromises);
      setIsResizing(false);
      setImages((prev: IFile[]) => [...prev, ...resizedImages]);
    };
  };

그리고 텍스트 편집기에서 지웠을 때는 편집기에서 이미지 string만 가지고 와서 저장된 객체와 비교하여 삭제된 이미지 string이 있을 시 useState 객체도 수정해주었다.

const content = postForm.getValues("content") || "";

  const quillMedia = useMemo(() => {
    const imageMatches = Array.from(
      content.matchAll(/<img[^>]+src=["']([^'">]+)['"]/gi)
    ).map((match) => match[1]);

    const videoMatches = Array.from(
      content.matchAll(/<iframe[^>]+src=["']([^'">]+)['"]/gi)
    ).map((match) => match[1]);
    return {
      images: imageMatches,
      videos: videoMatches,
    };
  }, [content]);

  useEffect(() => {
    const deletedImageFiles = images?.filter(
      (item: IFile) => !quillMedia.images.includes(item.previewURL)
    );
    const deletedVideoFiles = videos?.filter(
      (item: IFile) => !quillMedia.videos.includes(item.previewURL)
    );
    if (deletedImageFiles.length) {
      setImages((prevImages) => {
        const remainingImages = prevImages.filter((image: IFile) => {
          return !deletedImageFiles.includes(image);
        });
        return remainingImages;
      });
    } else if (deletedVideoFiles.length) {
      setVideos((prevVideos) => {
        const remainingVideos = prevVideos.filter((video: IFile) => {
          return !deletedVideoFiles.includes(video);
        });
        return remainingVideos;
      });
    }
  }, [images, videos, quillMedia]);

그리고 이후 섭밋 버튼 클릭시 mutation 함수로 타이틀 및 컨텐츠와 함께 useState 객체를 같이 인자로 넘겨줬다. 그리고 mutationFn 안에서 useState객체.file은 파이어스토어에 저장, 저장 후 받은 파이어스토어 url을 컨텐츠 안에 들어가있을 previewURL과 바꿔주었다!(replace 메서드 이용)

이렇게해서 사용자가 파일을 추가했다 지웠다하면서 발생할 서버 통신을 대폭 줄일 수 있었다! 물론 이게 정답은 아니겠지만 만족스럽다!

2. video-handler

퀼의 경우 내 컴퓨터에 있는 비디오 파일을 넣지 못하고 유튜브 링크 같은 비디오 링크를 임베드하는 것이 기본인데 나는 사용자의 자유도?를 높이기 위해 비디오 핸들러도 따로 함수를 만들어주었다!

  const videoHandler = () => {
    const input = document.createElement("input");
    input.setAttribute("type", "file");
    input.setAttribute("accept", "video/*");
    input.setAttribute("multiple", "true");
    input.click();

    input.onchange = (event: any) => {
      const videoFiles: FileList = event?.target?.files;
      const vidoeFileObjects: IFile[] = [];

      Array.from(videoFiles).forEach((videoFile: File) => {
        const id = videoFile.name;
        const previewURL = URL.createObjectURL(videoFile);
        vidoeFileObjects.push({ name: id, previewURL, file: videoFile });
        if (quillRef.current) {
          const Video = Quill.import("formats/video");
          const editor = quillRef.current.getEditor();
          const range = editor.getSelection();
          Video.sanitize = (previewURL: string) => previewURL;
          editor.insertEmbed(range?.index as number, "video", previewURL);
        }
      });
      setVideos((prev: IFile[]) => [...prev, ...vidoeFileObjects]);
    };
  };

그리고 파일 저장은 이미지 핸들러와 동일하게 해주었다. 이렇게해서 링크가 아닌 파일 자체를 본문에 넣을 수 있도록 완성하였다! 추가로 핸들러 함수들은 modules에 꼭 추가해야한다!! (아래 3-2번 코드 참고)

3.이미지 및 비디오 크기 조절

정말 가장 힘들었던ㅠㅠ 크기 조절... 사실 이미지 크기 조절은 어렵지 않았다. 퀼을 사용한 개발자 분들이 블로그를 많이 남겨주셔서...!

만약 이미지만 삽입한다면 아래 라이브러리 사용도 괜찮지만 3-2 라이브러리가 더 괜찮은 것 같다. 여튼 내가 먼저 사용한 이미지 라이브러리의 경우 xeger/react quill 이었다.

3-1. xeger/react quill

여기로 접속하면 자세한 방법이 나와있다. 리드미를 보고 그대로 작성하면 쉽게 이용할 수 있다. 여기서 중요한 것은 2가지!!

  • 만약 tailwindCSS를 사용한다면 뭔가 충돌이 있어 css 설정을 따로 해주어야한다! (여기서도 시간 많이 잡아먹은..ㅠㅠ)

    	./styles.css
    
    	ql-editor img {
    	  display: inline-block !important;
    	}
  • 문서대로한다면 사진 정렬만 가능할텐데 아래처럼 포맷에 width와 height를 꼭 넣어줘야만 사이즈 조절도 가능하다!!

    	const formats = ['align', 'float', 'width', 'height'];

3-2.@botom/quill-resize-module

사실 이 라이브러리... 찾기도 어려웠고ㅠㅠ 적용도 안되어서 이미지 사이즈 조절 포기하고 싶었다... 하지만 이미지만 되는 것도 웃기고... 더 큰 문제는 xeger/react-quill을 사용하면 비디오에도 영향을 미치는지 비디오를 클릭하면 사이즈 조절 툴과 정렬 박스가 떴다. 근데 정렬이 되지도 않고 계속 에러를 내뱉는... 그래서 진짜 npm 다 뒤져가며 찾은..

여기로 접속하면 사용 방법 확인 가능하다! 사실 하라는대로만 하면 아주 잘된다... 근데 나는 모듈을 순서대로 안해서... 문제가 꽤나 오래걸렸다... 여기서 중요한 부분은 아래에서 보다시피 resize가 제일 위에 있어야 한다는 부분이다.

const modules = useMemo(
    () => ({
      resize: {
        toolbar: {
          alignTools: false,
        },
      },
      toolbar: {
        container: [
          ["bold", "italic", "underline", "strike"],
          ["blockquote"],
          [{ header: 1 }, { header: 2 }],
          [{ list: "ordered" }, { list: "bullet" }],
          [{ indent: "-1" }, { indent: "+1" }],
          [{ size: ["small", false, "large", "huge"] }],
          [{ header: [1, 2, 3, 4, 5, 6, false] }],

          [{ color: [] }, { background: [] }],
          [{ font: [] }],
          [{ align: [] }],

          ["image", "video", "link"],
        ],
        handlers: {
          image: imageHandler,
          video: videoHandler,
        },
      },
    }),
    []
  );

이렇게만 하면 아주 쉽게 가능하다... 문서와 똑같이 사용해야 한다는 것을 시간들여 배운 기분... 이렇게 하면 이미지와 비디오 모두 사이즈 조절 및 정렬이 가능하다. 두개 모두 사용해봤을 때 솔직히 이게 더 좋다고 생각한다. 추가 css 설정도 필요없고 기능이 더 많다! (성능적으로는 잘 모르겠지만..!)

후기

퀼을 사용하면서 느낀 건 생각보다 타인의 블로그가 굉장히 중요하다라는 것이었다... 서로서로 돕고 사는 그런 느낌... 이전에는 쉽고 흔한 기능?만 했던 것인지 블로그가 참 많다고 생각했는데 점점 깊숙히 들어갈수록 내가 찾는 내용이 너무 없다고 느꼈다..

정말 스택플로우부터 라이브러리 이슈들까지 굉장히 많이 봤고 어떻게든 찾아 해결하면서 느낀건 앞으로 벨로그를 다시 써봐야겠다.. 이거 나만 어렵지 않을텐데..라고 생각했다!

그래서 아직 그럴 정도는 절대 아닌거 알지만 내 블로그가 누군가한테 조금이나마 도움이 되길 바라는 마음에서 작성해본 글...ㅎㅎ.. 개생아 분들 모두 화이팅..! 💪🏻

혹시 멋진 개발자 분들! 제 글을 보시고 잘못된 정보가 있다면 댓글로 알려주시면 감사하겠습니다 🙇

profile
차근차근 배워나가는 개생아🐾

0개의 댓글