next.js에서 react-quill 이미지 업로드 하기

junsangyu·2021년 10월 18일
13

next.js에서 react-quill ssr 문제

react-quill 라이브러리를 next.js에서 그냥 import를 하게 되면 next.js가 ssr에서 react-quill 라이브러리를 렌더링 할때 document 객체를 찾을 수 없는 에러가 뜬다.

이런 에러를 발생하는 이유는 react-quill 라이브러리는 내부적으로 document 객체를 사용하는데 next.js 에서 ssr로 렌더링될때 브라우저가 아닌 서버에서 렌더링이 되기 때문에 브라우저에서만 사용 가능한 window 객체가 없고 그로 인해 window.document 객체가 존재하지 않기 때문이다.
이를 해결할려면 next/dynamic에서 ssr: false 로 import를 하면 react-quill 컴포넌트가 ssr에서는 렌더링이 되지 않고 브라우저에서만 렌더링을 하게 만들어서 해결할 수 있다.

react-quill 라이브러리에서 기본 이미지 업로드 문제

react-quill 라이브러리에서 기본적으로 이미지는 base64로 인코딩되어서 img 태그의 src 속성에 삽입된다. 이러면 글의 길이가 굉장히 길어지게 된다.

이런 문제를 해결할려면 AWS S3같은 이미지 서버에 업로드를 해서 업로드된 이미지의 링크를 img 태그의 src속성에 넣어주면 된다. react-quill에서 이미지 업로드를 수정할려면 modules 속성에서 handles: { image: imageHandler } 로 커스텀 이미지 핸들러를 만들어 준다. 이 이미지 핸들러는 input 엘리먼트로 파일을 받고 파일을 이미지 서버에 올리고 현재 커서 위치에 이미지를 삽입하는 코드를 작성하면 된다.

ReactQuill 컴포넌트 ref 못가져오는 문제

위에서 설명한것처럼 dynamic으로 react-quill 라이브러리를 가져오고, 이미지 커서 위치에 이미지를 삽입하고 커서를 다음 위치로 변경할려면 ReactQuill 컴포넌트의 ref가 필요한데 그냥 ref속성으로 가져오면 못가져오는 버그가 난다.

이런 버그가 발생하는 이유는 next/dynamic으로 import를 하면 그냥 ref속성을 사용해서는 ref를 가져올 수 없다. ref를 가져올라면 next/dynamic에서 함수로 react-quill을 dynamic import를 해서 forwardRef를 시켜주고 ReactQuill컴포넌트에서 forwardedRef 속성으로 ref를 접근하면 된다.
사실 이 코드 부분은 검색으로 해결해서 자세히는 이해하지 못했다...

브라우저에서 S3로 직접 업로드를 하기 위해서 Presigned URL를 사용했다.

import { getS3PresignedURL, uploadImage } from 'apis/image';
import dynamic from "next/dynamic";
import { useMemo, useRef } from 'react';

const ReactQuill = dynamic(async () => {
  const { default: RQ } = await import('react-quill');
  return function comp({ forwardedRef, ...props }) {
    return <RQ ref={forwardedRef} {...props} />;
  };
}, { ssr: false });

const formats = [
  "header",
  "font",
  "size",
  "bold",
  "italic",
  "underline",
  "strike",
  "blockquote",
  "list",
  "bullet",
  "indent",
  "link",
  "image",
  "video",
];

function ReactQuillContainer({ description, setDescription }) {
  const quillRef = useRef();

  const imageHandler = () => {
    const input = document.createElement('input');

    input.setAttribute('type', 'file');
    input.setAttribute('accept', 'image/*');
    document.body.appendChild(input);
    
    input.click();
  
    input.onchange = async () => {
      const [file] = input.files;
      
      // S3 Presigned URL로 업로드하고 image url 받아오기
      const { preSignedPutUrl: presignedURL, readObjectUrl: imageURL } = (await getS3PresignedURL(file.name)).data;
      await uploadImage(presignedURL, file);
      
      // 현재 커서 위치에 이미지를 삽입하고 커서 위치를 +1 하기
      const range = quillRef.current.getEditorSelection();
      quillRef.current.getEditor().insertEmbed(range.index, 'image', imageURL)
      quillRef.current.getEditor().setSelection(range.index + 1);
      document.body.querySelector(':scope > input').remove()
    };
  }

  // useMemo를 사용한 이유는 modules가 렌더링마다 변하면 에디터에서 입력이 끊기는 버그가 발생
  const modules = useMemo(() => ({
    toolbar: {
      container: [
        [{ 'header': [1, 2, false] }],
        ['bold', 'italic', 'underline','strike', 'blockquote'],
        [{'list': 'ordered'}, {'list': 'bullet'}, {'indent': '-1'}, {'indent': '+1'}],
        ['link', 'image'],
        ['clean']
      ],
      handlers: { image: imageHandler }
    }
  }), []);

  return (
    <ReactQuill
      forwardedRef={quillRef}
      placeholder="본문을 입력하세요..."
      modules={modules}
      formats={formats}
      value={description}
      onChange={setDescription}
    />
  );
}

export default ReactQuillContainer;

2021.11.04)
iOS 브라우저에서 input태그의 onChange가 작동하지 않는다...
그 이유는 input태그가 DOM에 append되어야지 제대로 작동을 한다.

document.body.appendChild(input);
document.body.removeChild(input);

을 추가해야 iOS 브라우저에서 정상적으로 작동한다.
https://stackoverflow.com/questions/47664777/javascript-file-input-onchange-not-working-ios-safari-only

2021.12.10)

  • input태그를 삭제안되는 버그 수정
  • eslint display-name 에러 수정
profile
👨🏻‍💻

3개의 댓글

comment-user-thumbnail
2021년 12월 10일

const ReactQuill = dynamic(async () => {
const { default: RQ } = await import('react-quill');
return ({ forwardedRef, ...props }) => <RQ ref={forwardedRef} {...props} />
}, {
ssr: false,
});
로 빌드를 하면 missing display Name eslint 에러가 나서 빌드가 안되는걸로 나오는데
혹시 해결방법이 있을까요?

1개의 답글