Loading
Loading
블로그 개발 일지 #10 - 마크다운 에디터 기능 추가
2024년 12월 18일
개발 블로그에서 글 작성 기능을 향상시키기 위해 마크다운 에디터에 두 가지 주요 기능을 추가했습니다:
이 기능들은 글 작성과 읽기에 도움이 되도록 구현되었습니다.
TOC(목차) 기능은 마크다운에서 사용된 제목(h1
, h2
, h3
)을 자동으로 탐색하여 목차를 생성합니다. 이를 통해 긴 글에서도 원하는 섹션으로 빠르게 이동할 수 있습니다.
정규 표현식을 사용해 마크다운 본문에서 제목을 찾고, 각 제목에 고유한 ID를 자동으로 생성합니다.
const generateId = (text: string): string => text .replace(/^#+ /, "") .trim() .toLowerCase() .replace(/[^a-z0-9ㄱ-ㅎㅏ-ㅣ가-힣\s-]/g, "") .replace(/\s+/g, "-");
id
속성으로 연결합니다.IntersectionObserver
를 사용하여 사용자가 보고 있는 섹션을 실시간으로 감지하고, TOC에서 해당 항목을 강조합니다.
const handleIntersect = useCallback((entries: IntersectionObserverEntry[]) => { entries.forEach((entry) => { const index = headingElementsRef.current.findIndex((el) => el === entry.target); if (entry.isIntersecting && index !== -1) { setSelectedIndex(index); } }); }, []);
h1
, h2
, h3
)에 따라 들여쓰기를 조절합니다.<li onClick={() => { const targetElement = document.getElementById(id); targetElement?.scrollIntoView({ behavior: "smooth" }); }} > <a href={`#${id}`}>{text}</a> </li>
이미지 업로드 기능은 마크다운 에디터에 다음과 같은 기능을 제공합니다:
![Uploading image...]()
와 같은 텍스트로 표시됩니다.이미지를 에디터에 드래그하면 업로드가 자동으로 시작됩니다.
const handleDrop = (event: React.DragEvent) => { event.preventDefault(); const file = event.dataTransfer.files[0]; if (file && file.type.startsWith("image/")) { handleImageUpload(file, "\n![Uploading image...]()"); } };
클립보드에서 이미지를 복사한 후 붙여넣기하면 자동으로 업로드됩니다.
const handlePaste = (event: React.ClipboardEvent) => { const items = event.clipboardData.items; for (let i = 0; i < items.length; i++) { const item = items[i]; if (item.type.startsWith("image/")) { const file = item.getAsFile(); handleImageUpload(file, "\n![Uploading image...]()"); } } };
이미지 파일이 서버로 업로드되면, sharp
라이브러리를 사용해 리사이징을 진행합니다. sharp
는 이미지를 효율적으로 처리하는 Node.js 라이브러리로, 파일 크기를 조정하고 WebP 형식으로 변환하는 데 사용됩니다.
async function processAndUploadImage(buffer, originalName) { const baseName = path.basename(originalName, path.extname(originalName)); const filename = `${Date.now()}-${baseName}.webp`; const metadata = await sharp(buffer).metadata(); const { width: originalWidth, height: originalHeight } = metadata; let resizeOptions; if (originalWidth >= originalHeight) { resizeOptions = { width: 1280, height: Math.round((1280 * originalHeight) / originalWidth), }; } else { resizeOptions = { width: Math.round((1280 * originalWidth) / originalHeight), height: 1280, }; } const resizedBuffer = await sharp(buffer) .resize(resizeOptions.width, resizeOptions.height, { fit: "inside", withoutEnlargement: true, }) .webp({ quality: 90 }) .toBuffer(); const { data, error } = await supabase.storage .from("images") .upload(filename, resizedBuffer, { contentType: "image/webp" }); if (error) throw new Error(`이미지 업로드 중 에러 발생: ${error.message}`); const result = supabase.storage.from("images").getPublicUrl(data.path); return result.data.publicUrl; }
Multer
미들웨어를 사용해 이미지 파일 크기를 5MB로 제한하고, 허용된 파일 형식만 처리합니다.sharp
를 이용해 리사이징되며, 1280px로 크기가 조정됩니다.업로드가 완료되면 반환된 URL을 마크다운 이미지 링크 형식으로 콘텐츠에 삽입할 수 있습니다.
const handleImageUpload = async (file: File, tempText: string) => { setValue("content", `${watch("content")}${tempText}`); try { const imageUrl = await uploadImage(file); setValue("content", watch("content").replace(tempText, `\n`)); } catch (error) { console.error("Image upload failed:", error); } };