React-Quill Editor 이미지 처리하기 (Firebase Storage)

2023년 02월 14일

TOC

React

React-Quill은 간편하게 사용할 수 있는 Rich Text Editor이다.

하이퍼링크, 글꼴, 색상, 스타일, 이미지 등의 양식을 쉽게 구성할 수 있다.

React-Quill 구성하기

React App에 react-quill을 설치한다.

$ yarn add react-quill

App.js에 다음과 같이 Quill 에디터를 구성하였다.
상단 버튼을 클릭하면 콘솔에서 에디터의 value를 확인할 수 있게 만들었다.

// src/App.js
import { useMemo, useRef, useState } from "react"
import "react-quill/dist/quill.snow.css"
import ReactQuill from "react-quill"

function App() {
  const quillRef = useRef()
  const [content, setContent] = useState("")

  // quill에서 사용할 모듈
  // useMemo를 사용하여 modules가 렌더링 시 에디터가 사라지는 버그를 방지
  const modules = useMemo(() => {
    return {
      toolbar: {
        container: [
          [{ header: [1, 2, 3, false] }],
          ["bold", "italic", "underline", "strike"],
          ["blockquote"],
          [{ list: "ordered" }, { list: "bullet" }],
          [{ color: [] }, { background: [] }],
          [{ align: [] }, "link", "image"],
        ],
      },
    }
  }, [])

  return (
    <div style={{ margin: "50px" }}>
      <button onClick={() => console.log(content)}>Value</button>
      <ReactQuill
        style={{ width: "600px", height: "600px" }}
        placeholder="Quill Content"
        theme="snow"
        ref={quillRef}
        value={content}
        onChange={setContent}
        modules={modules}
      />
    </div>
  )
}

export default App

quill_1

Html 형태의 문자열이 content 값에 들어왔다.
이 포스팅에서 CRUD까지 다루진 않지만 만약 DB에 저장된 content 값을 출력하고자 한다면 다음과 같이 dangerouslySetInnerHTML, DOMPurify를 사용하면 된다.

$ yarn add isomorphic-dompurify
// src/ReadQuill.js
import DOMPurify from "isomorphic-dompurify"
import "react-quill/dist/quill.core.css"

function ReadQuill() {
  const content = "<h1>안녕하세요!</h1><h2>안녕하세요!</h2><h3>안녕하세요!</h3>"
  return (
    <div
      className="view ql-editor" // react-quill css
      dangerouslySetInnerHTML={{
        __html: DOMPurify.sanitize(content),
      }}
    />
  )
}

export default ReadQuill

이렇게 dangerouslySetInnerHTML을 사용하여 직접적으로 HTML을 설정하는 것은 Cross Site Scripting(XSS)에 취약하다고 한다. 그래서 XSS 공격을 방지하는 dompurify와 같은 sanitization library와 함께 사용해 주었다.

에디터에 이미지를 올리면?

일반적인 텍스트가 입력되면 content값은 그렇게 크지 않지만, 이미지가 들어오게 될 경우 문제가 생긴다. 이미지를 에디터에 올려보고 value를 체크해보자.

quill_2

<img src="data:image/png;base64, 엄청나게 긴 base64 value...

React-Quill은 이미지를 올리게 되면 8비트 이진 데이터를 문자 코드에 영향을 받지 않는 공통 ASCII 문자열로 바꾼다. (base64) 따라서 위와 같은 base64 인코딩이 된 형태로 이미지가 저장된다. 이렇게 value에 담긴 값을 DB에 저장하려 하면 전송에 실패하게 된다.

Firebase Storage로 이미지 처리하기

이미지 업로드 시 base64 문자열이 들어가는 방식 대신 이미지가 Firebase Storage에 저장이 되고, 저장된 이미지 URL을 에디터에 삽입하는 방식으로 구현하려고 한다.

Firebase Console에서 새 프로젝트를 생성한다.

Firebase 프로젝트 생성 > 웹 앱 추가 > Storage > 시작하기 (지역 : asia-east1)

fb1 fb2
fb3 fb4

프로젝트에 firebase를 설치하고 SDK를 작성한다.

$ yarn add firebase
// src/Firebase.js
// Import the functions you need from the SDKs you need
import { initializeApp, getApp, getApps } from "firebase/app"
import { getStorage } from "firebase/storage"
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "xxxxxxxxxxxxxxxxxx",
  authDomain: "xxxxxxxxxxxxxxxxxx",
  projectId: "xxxxxxxxxxxxxxxxxx",
  storageBucket: "xxxxxxxxxxxxxxxxxx",
  messagingSenderId: "xxxxxxxxxxxxxxxxxx",
  appId: "xxxxxxxxxxxxxxxxxx",
}

// Initialize Firebase
const app = !getApps().length ? initializeApp(firebaseConfig) : getApp()
export const storage = getStorage()
export default app

Storage의 업로드 및 읽기 권한 설정을 위해 firebase-tools를 설치하고 로그인 및 초기 설정을 진행한다.

$ yarn add firebase-tools
$ firebase login
$ firebase init
    -> Storage: Configure a security rules fileㅤfor Cloud Storage
    -> Use an existing project
    -> 프로젝트 선택

프로젝트에 생성된 storage.rules 파일을 다음과 같이 수정한다.
수정 후 firebase deploy --only storage 로 적용하면 권한이 부여된다.

# /storage.rules
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if true;
    }
  }
}

modules에 설정할 이미지 핸들러를 작성하였다.
이미지 선택이 완료되면 Firebase Storage에 파일명 image/Date.now()로 저장이 되고, getDownloadURL 메서드로 받은 이미지 URL을 에디터에 삽입하는 흐름이다.

(Date.now() 메서드는 UTC 기준으로 1970년 1월 1일 0시 0분 0초부터 현재까지 경과된 밀리 초를 반환한다.)

// src/App.js
...
...
import {storage} from "./Firebase";
import { uploadBytes, getDownloadURL, ref } from "firebase/storage";
...
...
  // 이미지 핸들러
  const imageHandler = () => {
    const input = document.createElement("input");
    input.setAttribute("type", "file");
    input.setAttribute("accept", "image/*");
    input.click();

    input.addEventListener("change", async () => {
      const editor = quillRef.current.getEditor();
      const file = input.files[0];
      const range = editor.getSelection(true);

      try {
        // 파일명을 "image/Date.now()"로 저장
        const storageRef = ref(
          storage,
          `image/${Date.now()}`
        );
        // Firebase Method : uploadBytes, getDownloadURL
        await uploadBytes(storageRef, file).then((snapshot) => {
          getDownloadURL(snapshot.ref).then((url) => {
            // 이미지 URL 에디터에 삽입
            editor.insertEmbed(range.index, "image", url);
            // URL 삽입 후 커서를 이미지 뒷 칸으로 이동
            editor.setSelection(range.index + 1);
          });
        });
      } catch (error) {
        console.log(error);
      }
    });
  };
...
...

Storage에 성공적으로 업로드 되었고, 이미지 URL 또한 에디터에 잘 반영이 되었다.

quill_3

quill_4


Reference

사이트 간 스크립팅 - 위키백과, 우리 모두의 백과사전

Quill React 에디터 사용해보기 (이미지 업로드 및 사이즈 조절)

[Firebase 웹] 파이어베이스 스토리지 이미지 업로드 및 링크 가져오기

Quill 에디터 - 이미지 처리하기

Date.now() - JavaScript | MDN

Written by

yhuj79

🌱 Junior Developer