230818 React-Quill, Supabase storage

나윤빈·2023년 8월 18일
0

TIL

목록 보기
37/55

React-Quill 사용하기

1. 설치하기

yarn add react-quill

2. React-Quill 불러오기

import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';

const Editor = () => {
  return (
    <>
      <ReactQuill theme="snow" />
    </>
  );
};
export default Editor;

import ReactQuill를 통해 컴포넌트로 바로 불러온다.
이때, css도 가져와서 theme를 지정해줘야 에디터가 제대로 보여진다.

3. React-Quill 설정하기

import React, { useRef } from 'react';

import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';

const Editor = () => {
  // 에디터 설정
  const modules = React.useMemo(
    () => ({
      // 툴바 설정
      toolbar: {
        container: [
          [{ header: [1, 2, 3, 4, 5, 6, false] }], // header 설정
          ['bold', 'italic', 'underline', 'strike', 'blockquote'], // 굵기, 기울기, 밑줄 등 부가 tool 설정
          [{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }], // 리스트, 인덴트 설정
          ['link', 'image'], // 링크, 이미지, 비디오 업로드 설정
          [{ align: [] }, { color: [] }, { background: [] }], // 정렬, 글자 색, 글자 배경색 설정
          ['clean'] // toolbar 설정 초기화 설정
        ]
      }
    }),
    []
  );

  // 툴바에 사용되는 툴 포맷
  const formats = [
    'header',
    'bold',
    'italic',
    'underline',
    'strike',
    'blockquote',
    'list',
    'bullet',
    'indent',
    'link',
    'image',
    'align',
    'color',
    'background'
  ];

  return (
    <>
      <ReactQuill
        style={{ width: '630px' }}
        theme="snow"
        modules={modules}
        formats={formats}
        placeholder="내용을 입력하세요."
      />
    </>
  );
};

export default Editor;

toolbar:container 에서 에디터에서 사용할 툴바 목록을 직접 설정할 수 있다.
fomats 을 통해 위에서 설정한 모듈들의 포맷을 설정한다.
ReactQuill 컴포넌트에 위에서 설정한 modules과 formats을 적용한다.

4. Handler 설정하기

이미지 업로드 처리는 따로 handler를 설정하고 적용하기로 한다.

import React, { useRef } from 'react';

import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';

const Editor = () => {
  // 에디터 설정
  const modules = React.useMemo(
    () => ({
      // 툴바 설정
      toolbar: {
        container: [
          [{ header: [1, 2, 3, 4, 5, 6, false] }], // header 설정
          ['bold', 'italic', 'underline', 'strike', 'blockquote'], // 굵기, 기울기, 밑줄 등 부가 tool 설정
          [{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }], // 리스트, 인덴트 설정
          ['link', 'image'], // 링크, 이미지, 비디오 업로드 설정
          [{ align: [] }, { color: [] }, { background: [] }], // 정렬, 글자 색, 글자 배경색 설정
          ['clean'] // toolbar 설정 초기화 설정
        ]
        
        // 핸들러 설정
        handlers: {
          image: imageHandler // 이미지 tool 사용에 대한 핸들러 설정
        }
      }
    }),
    []
  );

  // 툴바에 사용되는 툴 포맷
  const formats = [
    'header',
    'bold',
    'italic',
    'underline',
    'strike',
    'blockquote',
    'list',
    'bullet',
    'indent',
    'link',
    'image',
    'align',
    'color',
    'background'
  ];

  return (
    <>
      <ReactQuill
        style={{ width: '630px' }}
        theme="snow"
        modules={modules}
        formats={formats}
        placeholder="내용을 입력하세요."
      />
    </>
  );
};

export default Editor;

toolbar:handlers 에서 에디터의 기본 처리가 아닌 직접 핸들러 함수를 만들어서 처리하기 위한 모듈을 설정한다.

Supabase storage 사용하기

1. 이미지 업로드 핸들러 만들기

// 이미지 업로드 핸들러
const imageHandler = () => {
  const input = document.createElement('input');

  input.setAttribute('type', 'file');
  input.setAttribute('accept', 'image/*');

  input.click();
};

이미지 업로드 시 작동할 이미지 핸들러 함수를 만든다.
이미지 업로드시 input태그를 만들고 file을 업로드 할 수 있도록 한다.
툴 바에서 이미지를 클릭할 시 만들어준 input 태그가 클릭 되도록 한다.

2. supabase storage에 이미지 업로드 하고 url 받아오기

// 이미지 업로드 핸들러
const imageHandler = () => {
  const input = document.createElement('input');

  input.setAttribute('type', 'file');
  input.setAttribute('accept', 'image/*');

  input.click();

  input.addEventListener('change', async () => {
    if (input.files) {
      const file = input.files[0];

      try {
        const fileName = randomFileName(file.name);
        const renamedFile = new File([file], fileName);

        const { data } = await supabase.storage.from('images').upload(`post/${renamedFile.name}`, renamedFile);

        if (data) {
          // 업로드 성공 시 이미지 URL을 받아옴
          const imageUrl = data.path;
        }
      } catch (error) {
        console.error(error);
      }
    }
  });
};

클릭한 input 태그에 이미지를 업로드 하면 즉, change 이벤트가 발생할 때,
supabase storage에 이미지를 저장한다.
이때 file의 name을 가공해주는 방법을 이용했다.
supabase storage에 이미지 업로드를 성공할 시 이미지 url을 data의 path를 통해 가져올 수 있다.

3. 받아온 url 에디터에 적용하기

// 에디터 접근을 위한 ref return
const quillRef = useRef<ReactQuill | null>(null);
 
// 이미지 업로드 핸들러
const imageHandler = () => {
  const input = document.createElement('input');

  input.setAttribute('type', 'file');
  input.setAttribute('accept', 'image/*');

  input.click();

  input.addEventListener('change', async () => {
    if (input.files) {
      const file = input.files[0];

      try {
        const fileName = randomFileName(file.name);
        const renamedFile = new File([file], fileName);

        const { data } = await supabase.storage.from('images').upload(`post/${renamedFile.name}`, renamedFile);

        if (data) {
          // 업로드 성공 시 이미지 URL을 받아옴
          const imageUrl = data.path;

          const editor = quillRef.current?.getEditor();
          if (editor) {
            const range = editor.getSelection();
            
            // 받아온 이미지 URL 에디터에 넣어줌
            editor.insertEmbed(range?.index || 0, 'image', `${process.env.REACT_APP_SUPABASE_STORAGE_URL}${imageUrl}`);

            // 업로드된 이미지 바로 다음으로 커서를 위치
            editor.setSelection((range?.index || 0) + 1, 0);
          }
        }
      } catch (error) {
        console.error(error);
      }
    }
  });

에디터에 접근하기 위한 useRef를 사용하여 quillRef 객체를 생성한다.
supabase storage에 이미지 업로드 성공 시 받아온 이미지 url를 에디터에 적용한다.
적용하게 되면 <img src="imageUrl"> 과 같이 적용된다.
업로드된 이미지 바로 다음에 커서가 위치하도록 한다.

4. state에 에디터 입력값 담아주기

import Editor from './Editor';

import { useState } from 'react';

const Write = () => {
  const [body, setBody] = useState<string>('');

  return (
    <>
      <Editor setBody={setBody} />
    </>
  );
};

export default Write;

Write 컴포넌트에서 선언해준 setBodyEditor 컴포넌트로 props로 내려준다.

import React, { useRef } from 'react';

import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';

import { EditorProps } from '../../../types/props';
import { randomFileName } from '../../../hooks/useHandleImageName';
import { supabase } from '../../../api/supabase';

const Editor = ({ setBody }: EditorProps) => {
  // 에디터 접근을 위한 ref return
  const quillRef = useRef<ReactQuill | null>(null);

  // 이미지 업로드 핸들러
  const imageHandler = () => {
    const input = document.createElement('input');

    input.setAttribute('type', 'file');
    input.setAttribute('accept', 'image/*');

    input.click();

    input.addEventListener('change', async () => {
      if (input.files) {
        const file = input.files[0];

        try {
          const fileName = randomFileName(file.name);
          const renamedFile = new File([file], fileName);

          const { data } = await supabase.storage.from('images').upload(`post/${renamedFile.name}`, renamedFile);

          if (data) {
            // 업로드 성공 시 이미지 URL을 받아옴
            const imageUrl = data.path;

            const editor = quillRef.current?.getEditor();
            if (editor) {
              const range = editor.getSelection();
              // 받아온 이미지 URL 에디터에 넣어줌
              editor.insertEmbed(
                range?.index || 0,
                'image',
                `${process.env.REACT_APP_SUPABASE_STORAGE_URL}${imageUrl}`
              );
              // 업로드된 이미지 바로 다음으로 커서를 위치
              editor.setSelection((range?.index || 0) + 1, 0);
            }
          }
        } catch (error) {
          console.error(error);
        }
      }
    });
  };

  // 에디터 설정
  const modules = React.useMemo(
    () => ({
      // 툴바 설정
      toolbar: {
        container: [
          [{ header: [1, 2, 3, 4, 5, 6, false] }], // header 설정
          ['bold', 'italic', 'underline', 'strike', 'blockquote'], // 굵기, 기울기, 밑줄 등 부가 tool 설정
          [{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }], // 리스트, 인덴트 설정
          ['link', 'image'], // 링크, 이미지, 비디오 업로드 설정
          [{ align: [] }, { color: [] }, { background: [] }], // 정렬, 글자 색, 글자 배경색 설정
          ['clean'] // toolbar 설정 초기화 설정
        ],

        // 핸들러 설정
        handlers: {
          image: imageHandler // 이미지 tool 사용에 대한 핸들러 설정
        }
      }
    }),
    []
  );

  // 툴바에 사용되는 툴 포맷
  const formats = [
    'header',
    'bold',
    'italic',
    'underline',
    'strike',
    'blockquote',
    'list',
    'bullet',
    'indent',
    'link',
    'image',
    'align',
    'color',
    'background'
  ];

  return (
    <>
      <ReactQuill
        style={{ width: '630px' }}
        ref={quillRef}
        theme="snow"
        modules={modules}
        formats={formats}
        placeholder="내용을 입력하세요."
        onChange={setBody}
      />
    </>
  );
};

export default Editor;

Editor 컴포넌트에서 props로 내려받은 setBody에 에디터 입력값을 담아준다.
이는 이후에 Write 컴포넌트에서 post를 create 해줄 때 사용할 것!

😭 react-quill 라이브러리에 대해 이해하는 것과 supabase storage에 대해 공부하고 이해하는 것을 동시에 하려다 보니 한참을 헤맸다.

참고블로그

profile
프론트엔드 개발자를 꿈꾸는

1개의 댓글

comment-user-thumbnail
2023년 8월 19일

자세한 글 잘봤습니다. 이거 보고 저도 구현해 볼게요! 꿈은 더 크게 가지세요. 그여름 선창하면 후창으로 우리는 해주세요

답글 달기