React-Quill 사용하기
yarn add 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를 지정해줘야 에디터가 제대로 보여진다.
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을 적용한다.
이미지 업로드 처리는 따로 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 사용하기
// 이미지 업로드 핸들러
const imageHandler = () => {
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*');
input.click();
};
이미지 업로드 시 작동할 이미지 핸들러 함수를 만든다.
이미지 업로드시 input태그를 만들고 file을 업로드 할 수 있도록 한다.
툴 바에서 이미지를 클릭할 시 만들어준 input 태그가 클릭 되도록 한다.
// 이미지 업로드 핸들러
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를 통해 가져올 수 있다.
// 에디터 접근을 위한 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">
과 같이 적용된다.
업로드된 이미지 바로 다음에 커서가 위치하도록 한다.
import Editor from './Editor';
import { useState } from 'react';
const Write = () => {
const [body, setBody] = useState<string>('');
return (
<>
<Editor setBody={setBody} />
</>
);
};
export default Write;
Write
컴포넌트에서 선언해준 setBody
를 Editor
컴포넌트로 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에 대해 공부하고 이해하는 것을 동시에 하려다 보니 한참을 헤맸다.
자세한 글 잘봤습니다. 이거 보고 저도 구현해 볼게요! 꿈은 더 크게 가지세요. 그여름 선창하면 후창으로 우리는 해주세요