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

Project: ๐Ÿšฉcodefolio - ํ…์ŠคํŠธ ์—๋””ํ„ฐ ๋ณ€๊ฒฝ

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

Post, Edit post, Detail - ํŽ˜์ด์ง€ ๋ณ„ ํ…์ŠคํŠธ ์—๋””ํ„ฐ ๋ณ€๊ฒฝ

  • ๊ธฐ์กด : Toast UI Editor
  • ๋ณ€๊ฒฝ : UIW Markdown Editor
๐Ÿฅธ ์žฅ-๋‹จ์ ์— ๋”ฐ๋ฅธ ๊ฒฐ์ •
  • Toast UI Editor : ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์ด ์žˆ์œผ๋‚˜ ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ๊ฐ€ ๋ฌด๊ฒ๊ณ  SSR ์ง€์›์ด ์•ˆ๋จ.
  • UIW Markdown Editor : ๋งˆํฌ๋‹ค์šด ๊ธฐ๋ณธ ๋‚ด์žฅ, ๋ฒˆ๋“ค์‚ฌ์ด์ฆˆ ๊ฐ€๋ณ๊ณ , ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ ์—†์Œ

UIW Markdown Editor ํˆด๋ฐ” ์ปค์Šคํ…€ํ•˜์—ฌ ์‚ฌ์ง„ ์—…๋กœ๋“œ ๋ฐ ์‚ฝ์ž…, ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋ž, ๋ถ™์—ฌ๋„ฃ๊ธฐ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๊ธฐ๋ก

๋ชฉ์ฐจโ€‹

  1. ์—๋””ํ„ฐ ์ ์šฉ
  2. ์—๋””ํ„ฐ ์ปค์Šคํ…€
  3. ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ
  4. ์ด๋ฏธ์ง€ ์‚ฝ์ž…
  5. ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋ž, ๋ถ™์—ฌ๋„ฃ๊ธฐ ๊ตฌํ˜„
  6. ๊ฒฐ๊ณผ ํ™”๋ฉด

1. ์—๋””ํ„ฐ ์ ์šฉโ€‹

import "@uiw/react-md-editor/markdown-editor.css";
import "@uiw/react-markdown-preview/markdown.css";
import dynamic from "next/dynamic";
import { useState } from "react";

const MDEditor = dynamic(
() => import("@uiw/react-md-editor"),
{ ssr: false }
);

const PostEditor = () => {
const [value, setValue] = useState("**Hello world!!!**");

return (
<div>
<MDEditor value={value} onChange={setValue} />
</div>
);
};

export default PostEditor;

์šฐ์„  ๊ณต์‹๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•ด์„œ MDEditor๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค. ํ•˜์ง€๋งŒ useState ๋ฅผ ์‚ฌ์šฉํ•ด์„œ PostEditor ์ปดํฌ๋„ŒํŠธ์—์„œ๋งŒ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”๊ฒŒ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ธฐ์กด ์ฒ˜๋Ÿผ Recoil ๋กœ ์ƒํƒœ๋ฅผ ์ „์—ญ์œผ๋กœ ๋ฐ”๊พธ์–ด ์ค€๋‹ค.

const [postContent, setPostContent] = useRecoilState(recoilPostContent);

์ „์—ญ ๋ฐ์ดํ„ฐ์ธ recoilPostContent ์ ์šฉ

<MDEditor
value={postContent}
onChange={(value) => {
setPostContent(value || "");
}}
height={600}
textareaProps={{
placeholder: "Fill in your markdown for the coolest of the cool.",
}}
/>

์œ„์—์„œ ์‚ฌ์šฉํ–ˆ๋˜ state์˜ ๊ธฐ๋ณธ ๊ฐ’์œผ๋กœ placeholder ์ฒ˜๋Ÿผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์—๋””ํ„ฐ์—์„œ ์ง€์›ํ•˜๋Š” textareaProps๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ถŒ์žฅ ๋ฐฉ์‹๋Œ€๋กœ placeholder๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

+ ์™œ๋ƒํ•˜๋ฉด ์‹ค์ˆ˜๋กœ ๊ฒŒ์‹œ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ initial State๊ฐ€ ์ „๋‹ฌ๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•จ


2. ์—๋””ํ„ฐ ์ปค์Šคํ…€โ€‹

๊ธฐ๋ณธ ์—๋””ํ„ฐ ์ƒํƒœ

์œ„๋Š” ๊ธฐ์กด ์—๋””ํ„ฐ๊ฐ€ ๊ธฐ๋ณธ์ ์šฉ๋œ ์—๋””ํ„ฐ์˜ ์ƒํƒœ์ด๋‹ค.

ํ•˜์ง€๋งŒ ์œ ์ €๊ฐ€ ์‚ฌ์ง„์„ ์—…๋กœ๋“œ ํ•˜๋ ค๊ณ  ์‚ฌ์ง„ ์•„์ด์ฝ˜์„ ํด๋ฆญ ํ•˜๋ฉด,

์ด๋ฏธ์ง€ ์•„์ด์ฝ˜ ํด๋ฆญ ์‹œ ์‚ฝ์ž…๋˜๋Š” Markdown ํ˜•์‹

Markdown ํ˜•์‹์œผ๋กœ Image url์„ ์ž‘์„ฑํ•˜๊ณ , ์ž‘์„ฑํ•œ url์ด ์กด์žฌํ•ด์•ผ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋‹ค. (์—๋””ํ„ฐ์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ธฐ์กด url๋„ ์กด์žฌํ•˜์ง€ ์•Š๋Š” url ์ž„..)

ํ”„๋กœ์ ํŠธ ํŠน์„ฑ์ƒ ์‚ฌ์ง„์„ ๋งŽ์ด ๊ฒŒ์‹œํ•ด์•ผํ•˜๋Š”๋ฐ์— ๋ถˆํŽธํ•จ์„ ๋งŽ์ด ๋А๋ผ๊ฒ ๋‹ค๋Š” ํŒ๋‹จ์„ ํ–ˆ๊ณ , ์ด ๋ถ€๋ถ„์„ ์ˆ˜์ •ํ•ด ๋ณด๊ณ ์ž ํ–ˆ๋‹ค.

๊ทธ๋ž˜์„œ toolbar๋ฅผ ์ˆ˜์ • ํ•  ์ˆ˜ ์žˆ๋Š” ์†์„ฑ์œผ๋กœ commands๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— commands๋ฅผ ์ถ”๊ฐ€ํ•ด์„œ ์ปค์Šคํ…€ ํ•ด์•ผํ–ˆ๋‹ค.

<MDEditorStyled
value={postContent}
onChange={(value) => {
setPostContent(value || "");
}}
height={600}
textareaProps={{
placeholder: "ํ”„๋กœ์ ํŠธ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.",
}}
commands={[]} // ์ปค์Šคํ…€ ์†์„ฑ
/>

10๋ฒˆ ์ค„์— ์ฃผ์„์„ ๋‹ฌ์•„๋†“์€ ๊ตฌ๋ฌธ(commands)์„ ์ถ”๊ฐ€ํ•˜๋‹ˆ

ํˆด๋ฐ”๊ฐ€ ์‚ฌ๋ผ์ง„ ์—๋””ํ„ฐ

์ด๋ ‡๊ฒŒ ํˆด๋ฐ”๊ฐ€ ๋ชจ๋‘ ์ดˆ๊ธฐํ™” ๋˜์–ด๋ฒ„๋ ธ๋‹ค...

๊ทธ๋ž˜์„œ ๊ธฐ๋ณธ ์†์„ฑ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” commands ์†์„ฑ์„ ๋‹ค์‹œ ๊ฐ€์ ธ์™”๋‹ค.

commands={[
commands.bold,
commands.italic,
commands.strikethrough,
commands.hr,
commands.title,
commands.divider,

commands.link,
commands.quote,
commands.code,
commands.divider,

commands.unorderedListCommand,
commands.orderedListCommand,
commands.checkedListCommand,
commands.divider,
]}

์ด์ œ ์—ฌ๊ธฐ์„œ ํ•˜๋‚˜์˜ ์ปค์Šคํ…€๋œ ์‚ฌ์ง„์—…๋กœ๋“œ ์•„์ด์ฝ˜์„ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•ด ๊ทธ๋ฃน์„ ํ•˜๋‚˜ ๋งŒ๋“ค์–ด์„œ ์ปค์Šคํ…€ ํ•ด์ค˜์•ผํ–ˆ๋‹ค....

commands.group([], {
name: "image",
groupName: "image",
icon: (
<svg width="12" height="12" viewBox="0 0 20 20">
<path
fill="currentColor"
d="M15 9c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm4-7H1c-.55 0-1 .45-1 1v14c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 13l-6-5-2 2-4-5-4 8V4h16v11z"
/>
</svg>
),
children: (handle: any) => {
// API๊ฐ€ any๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
return (
<CustomImageContainer>
<ImageContainerCloseButtonWrapper>
<Image
src="/icons/close.svg"
width={16}
height={16}
alt="๋‹ซํž˜๋ฒ„ํŠผ"
onClick={() => handle.close()}
/>
</ImageContainerCloseButtonWrapper>
<label htmlFor="file">
<ImageUploadButton>
<ImageUploadText />
</ImageUploadButton>
</label>
<ImageInput
type="file"
id="file"
value=""
multiple
accept="image/*"
onChange={(e) => {
onImagePasted(e.target.files);
handle.close();
}}
/>
</CustomImageContainer>
);
},
buttonProps: { "aria-label": "Insert image" },
// ...
});

๊ทธ๋ฃน๋ช…์„ ์ง€์ •ํ•˜๊ณ , ๊ธฐ์กด์— ์‚ฌ์šฉํ•˜๋˜ ์•„์ด์ฝ˜์„ ๊ทธ๋Œ€๋กœ ๊ฐ€์ ธ์™€์„œ ๋„ฃ์–ด์ฃผ์—ˆ๋‹ค.

๋˜ ์ž์‹์œผ๋กœ ์•„์ด์ฝ˜์„ ํด๋ฆญํ•˜๋ฉด ์ผ์–ด๋‚˜๊ฒŒ ๋  ๋ชจ๋‹ฌ์„ ๋„์›Œ์ค˜์•ผ ํ–ˆ๋‹ค. ( input ์—์„œ image ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜๊ณ  ๋ฐ›์•„์ค˜์•ผ ํ•˜๋ฏ€๋กœ )

์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋ชจ๋‹ฌ

์œ„์ฒ˜๋Ÿผ ์ƒ์„ฑ๋œ ๋ชจ๋‹ฌ์—์„œ input์œผ๋กœ ์ด๋ฏธ์ง€ํŒŒ์ผ์„ ๋ฐ›๊ฒŒ๋˜๋ฉด onChange๋ฅผ ํ†ตํ•ด onImagePasted ๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ๋ชจ๋‹ฌ์„ close ํ•œ๋‹ค.

<ImageInput
type="file"
id="file"
value=""
multiple
accept="image/*"
onChange={(e) => {
onImagePasted(e.target.files);
handle.close();
}}
/>

์•„๋ž˜์—์„œ ์ด๋ฏธ์ง€ storage์— ์—…๋กœ๋“œํ•˜๋Š” ๊ณผ์ •์„ ์„ค๋ช… ํ•˜๊ฒ ๋‹ค.


3. ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ(supabase storage)โ€‹

์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œ ํ•˜๋ฉด ๋ฐ›์•„์„œ ์ €์žฅํ•˜๊ณ  ์žˆ๋Š” storage๊ฐ€ ํ•„์š”ํ–ˆ๊ณ , ์ €์žฅํ•œ ์ด๋ฏธ์ง€์ฃผ์†Œ๋ฅผ ๋ฐ˜ํ™˜ํ•˜์—ฌ Editor ์— ์ „๋‹ฌํ•ด์•ผํ•œ๋‹ค. (storage, url ํ•„์š”)

ํ•˜์ง€๋งŒ ํ”„๋กœ์ ํŠธ ํŠน์„ฑ์ƒ ์ด๋ฏธ์ง€ ์—ฌ๋Ÿฌ๊ฐœ๋ฅผ ํ•œ๊บผ๋ฒˆ์— ์˜ฌ๋ฆฌ๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ๋„ ๊ณ ๋ คํ•ด์•ผํ•œ๋‹ค. ์ด๋Ÿด ๋•Œ๋Š” input์— multiple ์†์„ฑ์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์—ฌ๋Ÿฌ์žฅ์˜ ํŒŒ์ผ์„ ์ค‘๋ณต ์„ ํƒ ํ•  ์ˆ˜์žˆ๋‹ค.

์ž ์ค‘๋ณต์„ ๊ณ ๋ คํ•ด์„œ ๋“ค์–ด์˜จ ํŒŒ์ผ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ฝ˜์†”์— ๋“ค์–ด์˜จ ํŒŒ์ผ์„ ์ถœ๋ ฅํ•ด๋ณด๋ฉด

FileList {0: File, length: 1}

FileList๋ผ๋Š” ๊ฐ์ฒด๋กœ ๋“ค์–ด์˜จ ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

FileList {0: File, 1: File, length: 2}

2๊ฐœ ์ด์ƒ์˜ ํŒŒ์ผ์ด ์—…๋กœ๋“œ ๋˜์–ด๋„ ์˜ค๋ฅ˜ ์—†์ด FileList์— ์ž˜ ๋“ค์–ด์™”๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด ์ด์ œ ์ด ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„ Array ํ˜•ํƒœ๋กœ ์ €์žฅํ•œ ํ›„ ์ˆœ์„œ๋Œ€๋กœ Storage์— ์ €์žฅํ•ด์•ผ ํ•œ๋‹ค.

+ ๋ฐฐ์—ด ํ˜•ํƒœ๋กœ ์ €์žฅํ•˜๋Š” ์ด์œ ๋Š” ์•„๋ž˜์—์„œ ์„ค๋ช…ํ•œ๋‹ค.

const files: File[] = [];

if (dataTransfer instanceof FileList) {
for (let index = 0; index < dataTransfer.length; index += 1) {
const file = dataTransfer[index];

// ์ด๋ฏธ์ง€ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ํ›„ ๊ฒฝ๊ณ  ๋ชจ๋‹ฌ ์ƒ์„ฑ(ํŒŒ์ผ ์‚ฌ์ด์ฆˆ, ํ™•์žฅ์ž)
if (!validateFile(file)) {
setShowModal(true);
return;
}
files.push(file);
}
}

์ด๋ฏธ์ง€ ํŒŒ์ผ์ด ์•„๋‹ ๋•Œ ์•Œ๋ฆผ ๋ชจ๋‹ฌ

ํŒŒ์ผ์„ ํ•˜๋‚˜์”ฉ ๋ฐฐ์—ด๋กœ ๋ฐ›์œผ๋ฉด์„œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ์ง„ํ–‰ํ–ˆ๋‹ค. ๊ฒ€์‚ฌ์— ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•˜๋ฉด ์•Œ๋ฆผ ๋ชจ๋‹ฌ์„ ๋„์šด๋‹ค!

files.map(async (file) => {
const fileId = uuidv4();

const uploadResult = await uploadImage(
file,
"post-image",
`${isPostId}/${fileId}`
);
if (!uploadResult) return;

const insertString = `![${file.name}](${uploadResult})`;
const resultString = insertToTextArea(insertString);

setPostContent(resultString || "");
});

์œ„์—์„œ ๋งŒ๋“ค์–ด์ค€ ์ด๋ฏธ์ง€ํŒŒ์ผ์˜ ๋ฐฐ์—ด์„ ํ•˜๋‚˜์”ฉ ์ˆœํšŒํ•˜๋ฉด์„œ ์„œ๋กœ ๋‹ค๋ฅธ id๋ฅผ ์‚ฝ์ž…ํ•ด์•ผ ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฐฐ์—ด์˜ ํ˜•ํƒœ๋กœ uuid๋ฅผ ํ™œ์šฉ ํ•ด id๋ฅผ ๋„ฃ์–ด์ฃผ๊ณ  ํ•˜๋‚˜์”ฉ Storage์— ์—…๋กœ๋“œ ํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์—…๋กœ๋“œ ํ–ˆ๋‹ค.

๋˜ 12๋ฒˆ์งธ ์ค„์— ์žˆ๋Š” insertToTextArea ํ•จ์ˆ˜๋Š” ์—…๋กœ๋“œํ•œ ์ด๋ฏธ์ง€ URL์ฃผ์†Œ๋ฅผ ์‹ค์ œ๋กœ ์—๋””ํ„ฐ์— ์‚ฝ์ž…์„ ํ•˜๋Š” ํ•จ์ˆ˜์ธ๋ฐ ์ด ๋ถ€๋ถ„์ด ์–ด๋ ค์› ๋‹ค.


4. ์ด๋ฏธ์ง€ ์‚ฝ์ž…(insertToTextArea)โ€‹

์„œ๋ฒ„์— ์—…๋กœ๋“œ๊ฐ€ ๋˜์—ˆ๋‹ค๋ฉด ๋‹ค์‹œ Markdown์˜ ํ…์ŠคํŠธ ํ˜•ํƒœ๋กœ ์—…๋กœ๋“œํ•œ URL์ฃผ์†Œ๋ฅผ ๋„ฃ์–ด์ฃผ์–ด์•ผ ์—๋””ํ„ฐ์—์„œ ํ•ด๋‹น ์‚ฌ์ง„์„ ์ฝ์–ด์˜ฌ ์ˆ˜ ์žˆ๋‹ค.

ํ•˜์ง€๋งŒ ๋‚ด๊ฐ€ ์ž…๋ ฅํ•˜๋˜ (์ปค์„œ๊ฐ€ ์žˆ๋Š”) ์ž๋ฆฌ์— ์ •ํ™•ํžˆ ์ด๋ฏธ์ง€๋ฅผ ๋„ฃ์ง€ ์•Š๋Š”๋‹ค๋ฉด, ๊ฒŒ์‹œ๊ธ€์„ ์ž‘์„ฑ์ค‘์ธ ๊ณณ์ด ์•„๋‹Œ ๊ธ€์˜ ์ค‘๊ฐ„์—์„œ ์—…๋กœ๋“œ๋ฅผ ํ•˜๋Š” ๊ฒฝ์šฐ๋ผ๋ฉด ์ž‘์„ฑ์ค‘์ด๋˜ ๊ธ€์˜ ๋งˆ์ง€๋ง‰ ๋ถ€๋ถ„์— ์ด๋ฏธ์ง€๊ฐ€ ์—…๋กœ๋“œ ๋˜๋ฉด์„œ ์˜๋„ํ•˜์ง€ ์•Š์€ ์œ„์น˜์— ์ด๋ฏธ์ง€๊ฐ€ ์—…๋กœ๋“œ ๋˜์—ˆ๋‹ค.

๊ทธ๋ž˜์„œ ๊ทธ ์œ„์น˜๋ฅผ ์ฐพ์•„์ฃผ๊ธฐ ์œ„ํ•œ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด ์ค˜์•ผ ํ–ˆ๋‹ค.

const insertToTextArea = (intsertString: string) => {
const textarea = document.querySelector("textarea");
if (!textarea) return null;

let sentence = textarea.value;
const len = sentence.length;
const pos = textarea.selectionStart;
const end = textarea.selectionEnd;

const front = sentence.slice(0, pos);
const back = sentence.slice(pos, len);

sentence = front + intsertString + back;

textarea.value = sentence;
textarea.selectionEnd = end + intsertString.length;
return sentence;
};

์ž‘์„ฑ์ค‘์ธ ๋ถ€๋ถ„์˜ ์•ž๋ถ€๋ถ„(front)๊ณผ ๋’ท๋ถ€๋ถ„(back)์˜ ์ค‘๊ฐ„์— ์ด๋ฏธ์ง€URL์„ ๋„ฃ์–ด์ฃผ๊ณ  ๋Š˜์–ด๋‚œ ํ…์ŠคํŠธ ๊ธธ์ด(insertString.length)๋งŒํผ selectionEnd๋ฅผ ๋’ค๋กœ ๋ฐ€์–ด์ค€๋‹ค.

์ด๋ ‡๊ฒŒ ๊ธ€์˜ ์ค‘๊ฐ„์— ์ด๋ฏธ์ง€ ์‚ฝ์ž…๋„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

์œ„ ๊ณผ์ •์„ ๋งˆ์นœ๋‹ค๋ฉด ๋‹ค์‹œ setPostContentํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์ด๋ฏธ์ง€URL์ด ์ถ”๊ฐ€๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•œ๋‹ค.


5. ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋ž, ๋ถ™์—ฌ๋„ฃ๊ธฐ ๊ตฌํ˜„โ€‹

์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œ ํ•˜๋Š” ๋ฐฉ์‹์—์„œ ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋ž์„ ์ ์šฉํ•ด ๋ณด๊ณ  ์‹ถ๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๊ณ  ๊ธฐ์ˆ ์  ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์š”ํ•˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์— ์Šค์Šค๋กœ ๊ฒฐ์ •ํ•˜๊ณ  ์ ์šฉํ–ˆ๋˜ ์ถ”๊ฐ€ ๊ตฌํ˜„์ด์˜€๋‹ค.

๋˜ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ๋ถ™์—ฌ๋„ฃ๊ธฐ๋ฅผ ํ•˜๋Š” ๊ณผ์ •์—์„œ ์‚ฌ์ง„์ด ์žˆ์„ ์ˆ˜ ์žˆ๊ธฐ๋•Œ๋ฌธ์— ํ•ด๋‹น ๋ถ€๋ถ„๋„ ์ถ”๊ฐ€๋กœ ๊ตฌํ˜„ํ–ˆ๋‹ค.

๊ธฐ์กด์˜ onChange์™€ ๊ฐ™์ด html ์†์„ฑ์œผ๋กœ ๊ธฐ๋ณธ ์†์„ฑ์ด ์žˆ์—ˆ๋‹ค. ํƒ€์ž…์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค

(property) onDrop: (event: DragEvent<HTMLDivElement>) => void
(property) onPaste: (event: ClipboardEvent<HTMLDivElement>) => void
<MDEditorStyled
value={postContent}
onChange={(value) => {
setPostContent(value || "");
}}
height={600}
onPaste={handleOnPaste}
onDrop={(event) => {
event.preventDefault();
onImagePasted(event.dataTransfer);
}}
textareaProps={{
placeholder: "ํ”„๋กœ์ ํŠธ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.",
}}
commands={[
...
]}
/>

ํฌ๋กฌ์˜ ๊ธฐ๋ณธ DragEvent ๊ฐ€ ์ผ์–ด๋‚˜๋ฉด์„œ ์ƒˆํƒญ์œผ๋กœ ์‚ฌ์ง„์ด ์—ด๋ฆฌ๋Š” ํ˜„์ƒ์ด ์žˆ์–ด์„œ preventDefault(); ๋กœ ๊ธฐ๋ณธ ๋‚ด์žฅ ๊ธฐ๋Šฅ์„ ์ดˆ๊ธฐํ™” ํ•ด์ฃผ์—ˆ๋‹ค.

์ดํ›„ ๊ธฐ์กด์ฒ˜๋Ÿผ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œ ํ–ˆ์„ ๋•Œ์™€ ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋ž์œผ๋กœ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œ ํ–ˆ์„ ๋•Œ ๋‹ค๋ฅธ ํƒ€์ž…์˜ ๊ฐ์ฒด๊ฐ€ ์ถœ๋ ฅ๋˜์—ˆ๋‹ค.

FileList {0: File, length: 1}
DataTransfer {dropEffect: 'none', effectAllowed: 'all', items: DataTransferItemList, types: Array(1), files: FileList}

์œ„ = FileList๋Š” ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋ฒ„ํŠผ(input)์œผ๋กœ ๊ฐ€์ ธ์™”์„ ๋•Œ, ์•„๋ž˜ = DataTransfer๋Š” ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋ž๊ณผ ๋ถ™์—ฌ๋„ฃ๊ธฐ๋กœ ๊ฐ€์ ธ์™”์„ ๋•Œ ์ด๋‹ค.

์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ํ•จ์ˆ˜์˜ ํŒŒ๋ผ๋ฏธํ„ฐ์— ํƒ€์ž…์„ ์šฐ์„  ์ถ”๊ฐ€ํ•ด ์ฃผ๊ณ , ํ•จ์ˆ˜ ๋‚ด๋ถ€์—์„œ ์กฐ๊ฑด ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ์—ˆ๋‹ค.

if (dataTransfer instanceof DataTransfer) {
for (let index = 0; index < dataTransfer.items.length; index += 1) {
const file = dataTransfer.items[index].getAsFile();

if (file && !validateFile(file)) {
setShowModal(true);
return;
}
files.push(file as File);
}
}

๊ธฐ์กด ๊ณผ ๊ฐ™์ด ์—ฌ๋Ÿฌ ํŒŒ์ผ์ด ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ํ•˜๋‚˜์”ฉ for๋ฌธ์„ ๋Œ๋ฉด์„œ getAsFile์ด๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ•ด๋‹น items๋ฅผ ํŒŒ์ผ๋กœ ๋ณ€ํ™˜ํ•ด ์ฃผ์—ˆ๋‹ค. ์ดํ›„์—๋Š” ์œ ํšจ์„ฑ๊ฒ€์‚ฌ๋ฅผ ์ง„ํ–‰ ํ•˜๊ณ ,

์„œ๋ฒ„์— ์ „์†ก๋  files ๋ฐฐ์—ด์— ํ•˜๋‚˜์”ฉ ์ถ”๊ฐ€ํ•ด ์ฃผ์—ˆ๋‹ค.

๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋ž๊ณผ ๋ถ™์—ฌ๋„ฃ๊ธฐ๋กœ ์—…๋กœ๋“œํ•˜๋ฉด ์—๋””ํ„ฐ์—๋Š” ![์Šคํฌ๋ฆฐ์ƒท ....png](https://...supabase.co/storage/v1/object/public/post-image/...) ํ˜•ํƒœ๋กœ ์ด๋ฏธ์ง€ ์ฃผ์†Œ๊ฐ€ ์‚ฝ์ž…๋˜๊ณ , ์˜ค๋ฅธ์ชฝ ๋ฏธ๋ฆฌ๋ณด๊ธฐ์—์„œ ์‚ฌ์ง„์ด ๋ณด์ธ๋‹ค.


6. ๊ฒฐ๊ณผ ํ™”๋ฉดโ€‹

๊ฒฐ๊ณผ ํ™”๋ฉด


๐Ÿ˜Ž ์ฐธ๊ณ  ๋งํฌโ€‹


์ตœ์ข… ์ •๋ฆฌ
  1. ์—๋””ํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ  ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€๋กœ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
  2. ์ถ”๊ฐ€๋กœ ํŒŒ์ผ ์ค‘๋ณต ์„ ํƒ์ด ๊ฐ€๋Šฅํ•˜๊ฒŒ multiple์ด ์ž‘๋™ํ•˜๊ฒŒ fileList ๋ฅผ ๋ฐ›์•„์ค๋‹ˆ๋‹ค.
  3. ๋ฐ›์•„์˜จ ์ด๋ฏธ์ง€๋ฅผ fileList ์—์„œ ํ•˜๋‚˜์”ฉ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ํ›„ storage์— ์—…๋กœ๋“œ ํ•ฉ๋‹ˆ๋‹ค.
  4. ์„œ๋ฒ„์— ์—…๋กœ๋“œํ•˜๊ณ  ์ƒ์„ฑ๋œ URL์„ ๋‹ค์‹œ ๋ฐ›์•„ ์ •ํ™•ํ•œ ์œ„์น˜์— ์ด๋ฏธ์ง€๋ฅผ ์‚ฝ์ž… ์‹œ์ผœ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
  5. ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋ž, ๋ถ™์—ฌ๋„ฃ๊ธฐ๋ฅผ ํ•  ๋•Œ ์—๋„ ์ด๋ฏธ์ง€๊ฐ€ ์—…๋กœ๋“œ ๋  ์ˆ˜ ์žˆ๋„๋ก ๊ณ ๋„ํ™” ํ•˜์˜€์Šต๋‹ˆ๋‹ค.