Project: ๐ฉcodefolio - ํ ์คํธ ์๋ํฐ ๋ณ๊ฒฝ
Post, Edit post, Detail - ํ์ด์ง ๋ณ ํ ์คํธ ์๋ํฐ ๋ณ๊ฒฝ
- ๊ธฐ์กด : Toast UI Editor
- ๋ณ๊ฒฝ : UIW Markdown Editor
- Toast UI Editor : ์ด๋ฏธ์ง ์ ๋ก๋ ๊ธฐ๋ฅ์ด ์์ผ๋ ๋ฒ๋ค ์ฌ์ด์ฆ๊ฐ ๋ฌด๊ฒ๊ณ SSR ์ง์์ด ์๋จ.
- UIW Markdown Editor : ๋งํฌ๋ค์ด ๊ธฐ๋ณธ ๋ด์ฅ, ๋ฒ๋ค์ฌ์ด์ฆ ๊ฐ๋ณ๊ณ , ์ด๋ฏธ์ง ์ ๋ก๋ ๊ธฐ๋ฅ ์์
UIW Markdown Editor ํด๋ฐ ์ปค์คํ ํ์ฌ ์ฌ์ง ์ ๋ก๋ ๋ฐ ์ฝ์ , ๋๋๊ทธ ์ค ๋๋, ๋ถ์ฌ๋ฃ๊ธฐ ๊ธฐ๋ฅ ๊ตฌํ ๊ธฐ๋ก
๋ชฉ์ฐจโ
- ์๋ํฐ ์ ์ฉ
- ์๋ํฐ ์ปค์คํ
- ์ด๋ฏธ์ง ์ ๋ก๋
- ์ด๋ฏธ์ง ์ฝ์
- ๋๋๊ทธ ์ค ๋๋, ๋ถ์ฌ๋ฃ๊ธฐ ๊ตฌํ
- ๊ฒฐ๊ณผ ํ๋ฉด
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 ํ์์ผ๋ก 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 = ``;
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 ๋ฐฐ์ด์ ํ๋์ฉ ์ถ๊ฐํด ์ฃผ์๋ค.
๋๋๊ทธ ์ค ๋๋๊ณผ ๋ถ์ฌ๋ฃ๊ธฐ๋ก ์
๋ก๋ํ๋ฉด ์๋ํฐ์๋  ํํ๋ก ์ด๋ฏธ์ง ์ฃผ์๊ฐ ์ฝ์
๋๊ณ , ์ค๋ฅธ์ชฝ ๋ฏธ๋ฆฌ๋ณด๊ธฐ์์ ์ฌ์ง์ด ๋ณด์ธ๋ค.
6. ๊ฒฐ๊ณผ ํ๋ฉดโ

๐ ์ฐธ๊ณ ๋งํฌโ
- ํ์ ํฌํ ๊ฒฐ๊ณผ Toast UI Editor VS Tiptap VS ETC... ยท react-challengers/Codefolio ยท Discussion #198
- Toast UI Editor TOAST UI :: Make Your Web Delicious!
- UIW Markdown Editor Markdown Editor for React.
- ์๋ํฐ๋ฅผ ๋ณ๊ฒฝํ๊ณ ์ด๋ฏธ์ง ์ ๋ก๋ ๊ธฐ๋ฅ์ ์ถ๊ฐ๋ก ๊ตฌํํ์ต๋๋ค.
- ์ถ๊ฐ๋ก ํ์ผ ์ค๋ณต ์ ํ์ด ๊ฐ๋ฅํ๊ฒ
multiple์ด ์๋ํ๊ฒfileList๋ฅผ ๋ฐ์์ค๋๋ค. - ๋ฐ์์จ ์ด๋ฏธ์ง๋ฅผ
fileList์์ ํ๋์ฉ ์ ํจ์ฑ ๊ฒ์ฌ ํ storage์ ์ ๋ก๋ ํฉ๋๋ค. - ์๋ฒ์ ์ ๋ก๋ํ๊ณ ์์ฑ๋ URL์ ๋ค์ ๋ฐ์ ์ ํํ ์์น์ ์ด๋ฏธ์ง๋ฅผ ์ฝ์ ์์ผ์ฃผ์์ต๋๋ค.
- ๋๋๊ทธ ์ค ๋๋, ๋ถ์ฌ๋ฃ๊ธฐ๋ฅผ ํ ๋ ์๋ ์ด๋ฏธ์ง๊ฐ ์ ๋ก๋ ๋ ์ ์๋๋ก ๊ณ ๋ํ ํ์์ต๋๋ค.
