[Next.js] TOAST UI 이미지 Blob 처리 및 활용 1편
들어가며
해당 코드는 회사 코드는 아니고 간단하게 요약한 코드여서 바로 사용한다고 해도 퀄리티에 문제가 될 수 있다. 해당 Toast UI를 사용하게 된 이유는 기존 회사 업무를 진행하며 WEB, API, GW 등 버전 업데이트 및 수정사항이 있으면 사용설명서 등 파일을 직접 서버로 이동하여 올려야 한다는 문제점과 인력 낭비라는 문제가 있어 해당 운영측이나 개발측에서 일일히 수정하고 팝업 같은 경우에는 해당 DB에 직접 접속하여 업데이트를 해야한다는 보안 측면이나 인력낭비를 해결하기 위해 (신) 프로젝트에서는 이를 운영측에서 Toast UI를 활용하여 직접 수정이 가능하게 만들게 되었다.
이를 통해 서버, DB로 직접 접속하기 힘든 측에서 권한만 있으면 신규 자료 및 POPUP 창 등을 관리 할 수 있다는 측면에서 효과를 보게 되었다.
먼저 간단한 Toast UI 사용방법과 Image 처리 그리고 간단한 POPUP에서 시간관리 등을 확인해 보겠다,.
Toast UI란
TOAST UI Editor는NHN Cloud에서 개발한 오픈 소스 라이브러리로,마크다운과 위지윅 방식 모두를 지원하는 무료 에디터이다!! (markdown/wysiwyg) 편집 기능 및 이미지,링크, 표 등 효율적이고 간푠하게 위에 있는 사진처럼 간편하게 만들 수 있는 기능을 내포하고 있다.
기본 코드
import React, { useEffect, useState, useCallback } from 'react';
import Box from '@mui/material/Box';
import '@toast-ui/editor/dist/toastui-editor.css';
import { Editor } from '@toast-ui/react-editor';
import axios from 'axios';
// Props 타입 정의
interface ToastUIEditorProps {
setContent: React.Dispatch<React.SetStateAction<string>>;
initialValue?: string;
}
// Editor 인스턴스의 타입 정의
interface EditorInstance {
getMarkdown: () => string;
setMarkdown: (markdown: string) => void;
}
// 이미지 업로드 응답 타입 정의
interface ImageUploadResponse {
imageUrl: string;
}
const ToastUIEditor: React.FC<ToastUIEditorProps> = ({ setContent, initialValue = '' }) => {
// 에디터 인스턴스를 상태로 관리 (타입 지정)
const [editorInstance, setEditorInstance] = useState<EditorInstance | null>(null);
// 초기값이 변경되면 에디터의 내용을 업데이트
useEffect(() => {
if (editorInstance) {
editorInstance.setMarkdown(initialValue);
}
}, [editorInstance, initialValue]);
// 에디터 내용이 변경될 때마다 상위 컴포넌트로 전달
const handleChange = useCallback(() => {
if (editorInstance) {
setContent(editorInstance.getMarkdown());
}
}, [editorInstance, setContent]);
// 이미지 업로드 처리 함수
const uploadImage = useCallback(async (blob: Blob): Promise<string | null> => {
const formData = new FormData();
formData.append('image', blob);
try {
const response = await axios.post<ImageUploadResponse>('/api/uploadImage', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data.imageUrl;
} catch (error) {
console.error('Image upload failed:', error);
return null;
}
}, []);
return (
<Box sx={{
m: 2,
'& .toastui-editor-defaultUI': {
border: '1px solid #e0e0e0',
borderRadius: '4px',
},
'& .toastui-editor-md-container': {
backgroundColor: '#ffffff',
},
}}>
<Editor
initialValue={initialValue}
height="830px"
placeholdr="내용을 입력해주세요"
previewStyle="vertical"
initialEditType="markdown"
toolbarItems={[
['heading', 'bold', 'italic', 'strike'],
['hr', 'quote'],
['ul', 'ol', 'task', 'indent', 'outdent'],
['table', 'image', 'link'],
['code', 'codeblock']
]}
usageStatistics={false}
onChange={handleChange}
onLoad={(editor: EditorInstance) => setEditorInstance(editor)}
hooks={{
addImageBlobHook: async (blob: Blob, callback: (url: string, alt: string) => void) => {
const imageUrl = await uploadImage(blob);
if (imageUrl) {
callback(imageUrl, 'alt text');
} else {
callback('image_load_fail', 'alt text');
}
return false;
}
}}
/>
</Box>
);
};
export default ToastUIEditor;
- initialValue: 초기 마크다운 내용
- height: 에디터 높이
- placeholder: 빈 에디터에 표시될 텍스트
- previewStyle: 미리보기 스타일 ('vertical' = 좌우 분할)
- initialEditType: 초기 편집 모드
- toolbarItems: 툴바에 표시될 도구들
- onChange: 내용 변경 시 호출될 함수
- onLoad: 에디터 로드 완료 시 호출될 함수
- hooks: 이미지 업로드 등의 커스텀 동작 정의
해당 코드에서 addImageBlobHook 을 사용하지 않으면 이미지 업로드 시 base64 로 업로드 되어 DB에 해당 내용을 여러개 저장하게 된다면 길이 문제가 발생할 수 있지만 이를 해결할 수 있다.
이미지 파일 저장
// src/app/api/uploadImage/route.ts
import { NextResponse } from 'next/server';
import { writeFile } from 'fs/promises';
import path from 'path';
export async function POST(request: Request) {
try {
const formData = await request.formData();
const file = formData.get('image') as File;
if (!file) {
return NextResponse.json(
{ error: 'No file received' },
{ status: 400 }
);
}
// 파일 타입 확인
if (!file.type.startsWith('image/')) {
return NextResponse.json(
{ error: 'File must be an image' },
{ status: 400 }
);
}
// 파일을 바이트 배열로 변환
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
// 파일 이름 생성 (현재 시간 + 원본 파일명)
const timestamp = Date.now();
const originalName = file.name;
const fileName = `${timestamp}-${originalName}`;
// public 폴더 내 uploads 디렉토리에 저장
const publicPath = path.join(process.cwd(), 'public', 'uploads');
const filePath = path.join(publicPath, fileName);
// 디렉토리가 없으면 생성
await createUploadDirectory(publicPath);
// 파일 저장
await writeFile(filePath, buffer);
// 클라이언트에서 접근 가능한 URL 반환
const imageUrl = `/uploads/${fileName}`;
return NextResponse.json({
imageUrl,
success: true
});
} catch (error) {
console.error('Upload error:', error);
return NextResponse.json(
{ error: 'Failed to upload image' },
{ status: 500 }
);
}
}
// uploads 디렉토리 생성 함수
async function createUploadDirectory(dir: string) {
try {
const fs = require('fs');
if (!fs.existsSync(dir)) {
await fs.promises.mkdir(dir, { recursive: true });
}
} catch (error) {
console.error('Error creating directory:', error);
throw error;
}
}
// API 라우트 설정
export const config = {
api: {
bodyParser: false, // formData를 직접 처리하기 위해 비활성화
},
};
addImageBlobHook과 에 코드를 추가하게 되면 base64 문제 및 해당 파일을 원하는 폴더에 저장할 수 있게 된다.
다음 글에서는 ToastUI 를 활용한 Popup창 관리 및 하루동안 안보기 등 기본적인 기능을 작성할 예정이다.