๋ณธ๋ฌธ์œผ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ

Project: ๐Ÿšฉcodefolio - Toast UI Editor ์‚ฌ์ง„ ์—…๋กœ๋“œ

ยท ์•ฝ 2๋ถ„
Seung hyo, Lee
Front End Engineer @ Assistfit
๐ŸŽฏ๋ชฉ์ 

์—๋””ํ„ฐ ๊ธฐ์กด์˜ ๋ฌธ์ œ์  ๊ฐœ์„  (์‚ฌ์ง„ ์—…๋กœ๋“œ ์‹œ ๋ทฐ์–ด์—์„œ ์ด๋ฏธ์ง€๊ฐ€ string ํ˜•์‹์œผ๋กœ ์ถœ๋ ฅ๋˜์–ด์ง)

๊ฐœ์„  : image url๊ณผ file name์œผ๋กœ ๋ณด์—ฌ์งˆ ์ˆ˜ ์žˆ๋„๋ก

** ์ตœ์ข… ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„ ๋งค์šฐ ๋ฐ”์œ ๊ด€๊ณ„๋กœ ์šฐ์„  ์ค‘์š” ์ฝ”๋“œ๋งŒ ์ ์–ด๋†“์•˜์Œ **


1. UI tagโ€‹

<Editor
ref={editorRef}
initialValue={postContent ?? null}
previewStyle="vertical"
// previewHighlight={false}

initialEditType="markdown"
useCommandShortcut
toolbarItems={toolbarItems}
language="ko-KR"
plugins={[colorSyntax]}
hooks={{
// @ts-ignore
addImageBlobHook: addImage,
}}
onChange={() => handleOnEditorChange()}
/>

hooks ์‚ฌ์šฉํ•˜์—ฌ addImage ๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ์ปค์Šคํ…€ํ•˜์—ฌ ๋งŒ๋“ค์–ด ์ฃผ์—ˆ๋‹ค.


2. addImage (์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ „์ฒด ๊ณผ์ •)โ€‹

const addImage = async (blob: File, dropImage: HookCallback) => {
const img = await compressImg(blob); // ์ด๋ฏธ์ง€ ์••์ถ•
if (!img) return;
const url = await uploadImage(img); // ์—…๋กœ๋“œ๋œ ์ด๋ฏธ์ง€ ์„œ๋ฒ„ url
if (!url) return;
dropImage(url, `${blob.name}`); // ์—๋””ํ„ฐ์— ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
};

์ด๋ฏธ์ง€ ์‚ฌ์ด์ฆˆ๊ฐ€ ๋„ˆ๋ฌด ํฌ๋ฉด ์—๋Ÿฌ ๋ฐœ์ƒ ์œ„ํ—˜๊ณผ, 1ํšŒ์„ฑ์œผ๋กœ ๋ณด์—ฌ์ค„ ๋‚ด์šฉ์„ ์ˆ˜์‹œ๋กœ ์„œ๋ฒ„ํ†ต์‹ ํ•˜๋Š” ๋ฐ์— ๋ถ€๋‹ด์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฏธ์ง€ ์••์ถ•์„ ํ–ˆ๊ณ , url ์—๋Š” ์„œ๋ฒ„ํ†ต์‹ ์ด ๋œ img๋ฅผ ๋ฐ›๊ณ ์žˆ๋‹ค.


3. uploadImage (์—…๋กœ๋“œ)โ€‹

// ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ
const uploadImage = async (blob: File) => {
try {
const imgPath = crypto.randomUUID();
await supabase.storage.from("post-image").upload(imgPath, blob);

// ์ด๋ฏธ์ง€ ์˜ฌ๋ฆฌ๊ธฐ
const urlResult = await supabase.storage
.from("post-image")
.getPublicUrl(imgPath);
return urlResult.data.publicUrl;
} catch (error) {
console.log(error);
return false;
}
};

ํ•ด๋‹น ํ•จ์ˆ˜๋Š” ์—๋””ํ„ฐ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์‚ฌ์ง„์„ supabase๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐํ˜•ํƒœ๋ฅผ ์ €์žฅํ•ด ์ค€๋‹ค.


4. compressImg (์••์ถ•)โ€‹

// ์ด๋ฏธ์ง€ ์••์ถ•
const compressImg = async (blob: File): Promise<File | void> => {
const options = {
maxSize: 1,
initialQuality: 0.55, // initial 0.7
};
const result = await imageCompression(blob, options)
.then((res) => res)
.catch((e) => console.log(e, "์••์ถ• ์—๋Ÿฌ"));
return result;
};

์ด๋ฏธ์ง€๋ฅผ ์••์ถ•ํ•˜๋ ค๋ฉด imageCompression์ด๋ผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ import ํ•œ ํ›„ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.


5. handleOnEditorChangeโ€‹

const handleOnEditorChange = () => {
// ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
const editorText = editorRef.current?.getInstance().getMarkdown();
if (editorText === " " || editorText === "" || editorText === undefined) {
return;
}
// HTML ๋Œ€์‹ ์— Markdown์œผ๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
setPostContent(editorText);
};

์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋Š” ํ•ด๋‹น ์—๋””ํ„ฐ๊ฐ€ ์•„๋ฌด๋Ÿฐ ํƒ€์ดํ•‘ ์—†์ด posting ํ–ˆ์„ ๋•Œ๋ฅผ ์žก์•„์ค€๋‹ค. ์ดํ›„ ๋ฌธ์ œ ์—†๋‹ค๋ฉด state๋กœ editorText๋ฅผ ์ „์†กํ•œ๋‹ค.