admin 페이지 개발 중, React-quill 에디터를 통해 붙여넣은 사진을 파일 형태로 서버에 업로드해야 했다. 이때 발생한 에러와 그를 해결하는 과정을 함께 살펴보자.
(초기 코드는 quill-image-drop-and-paste 공식문서를 참고하였다.)
https://github.com/chenjuneking/quill-image-drop-and-paste#handler
기존의 코드를 보자.
const dropAndPasteHandler = async (imageData: IImageData) => {
const blob = imageData.toBlob();
const file = imageData.toFile();
const formData = new FormData();
formData.append("file", blob);
formData.append("file", file);
formData.append("ikey", ikeyData);
formData.append("email", values.email);
// upload image to your server
const config = {
mod: "cors",
headers: {
"Content-Type": "multipart/form-data",
},
};
const result = await axios.post("/itempic.upload", formData, config);
const IMG_URL = "./itempic.get/" + result.data.pkey;
const editor: any = quillRef?.current?.getEditor();
if (editor) {
const range = editor.getSelection();
editor.insertEmbed(range.index, "image", IMG_URL);
}
};
// React-quill 모듈
const modules = useMemo(() => {
return {
toolbar: {
container: [
["image"],
[{ header: [1, 2, 3, false] }],
["bold", "italic", "underline", "strike", "blockquote"],
],
handlers: {
image: imageHandler,
},
},
imageDropAndPaste: {
handler: dropAndPasteHandler,
},
};
}, []);
dropAndPasteHandler의 기능을 대략적으로 설명해보겠다.
imageData로부터 blob 및 file 객체를 생성한다.
HTTP 요청을 위한 데이터를 준비한다 (formData 생성)
생성된 formData 객체에는 blob, file, ikey, email 데이터가 추가된다. (서버로 보내질 데이터들)
config 객체를 사용하여 HTTP 요청의 헤더와 관련 설정을 지정한다.
/itempic.upload 경로로 formData를 POST 요청으로 전송한다.
서버 응답으로부터 받은 데이터인 result.data.pkey를 사용하여 이미지의 URL을 생성하고, 이 URL은 IMG_URL 변수에 저장된다.
quillRef를 통해 Quill 에디터의 인스턴스를 가져온다.
에디터의 현재 선택된 위치 정보를 가져온 후, 해당 위치에 이미지를 삽입한다.
요약하면, 이 함수는 주어진 이미지 데이터를 받아서 서버에 업로드하고, 그 후 업로드된 이미지의 URL을 Quill 에디터에 삽입하는 작업을 수행한다.
imageData 오브젝트에 대해서 .toBlob() 및 .toFile() 메소드를 호출할 때 에러가 발생하였다.
에러 메세지는 다음과 같다.
imageData.toBlob is not a function
TypeError: imageData.toBlob is not a function
at ImageDropAndPaste.dropAndPasteHandler (http://localhost:3000/static/js/bundle.js:3577:28)
at http://localhost:3000/static/js/bundle.js:80438:31
at reader.onload (http://localhost:3000/static/js/bundle.js:80468:11)
imageData.toBlob이라는 함수가 없다고 나온다. 이것은 imageData 객체에 toBlob이라는 메소드가 없다는 의미로 해석된다.
초기의 코드는 imageData가 Blob 또는 File 형식이라고 가정하고 있었지만, 실제로는 Base64로 인코딩된 Data URL 형식이었다. 따라서 표준 Blob 또는 File API에는 .toBlob() 및 .toFile() 메소드가 없어서 에러가 발생한 것이다.
imageData는 Base64로 인코딩된 Data URL 형식을 가지고 있다. 이 Data URL을 파싱하여 Base64 인코딩 부분을 추출한 후, 바이너리 데이터로 디코딩하였다. 이렇게 디코딩된 바이너리 데이터를 바탕으로 Blob 오브젝트를 생성했고, 이 오브젝트를 서버에 업로드하는 방법으로 로직을 변경하였다. 수정된 코드를 보자.
const dropAndPasteHandler = async (imageData: string) => {
const blob = dataURLtoBlob(imageData);
setSelectedFile(blob);
const formData = new FormData();
formData.append("file", blob);
formData.append("ikey", ikeyData);
formData.append("email", values.email);
const config = {
mod: "cors",
headers: {
"Content-Type": "multipart/form-data",
},
};
const result = await axios.post("/itempic.upload", formData, config);
const IMG_URL = "./itempic.get/" + result.data.pkey;
const editor: any = quillRef?.current?.getEditor();
if (editor) {
const range = editor.getSelection();
editor.insertEmbed(range.index, "image", IMG_URL);
}
};
const dataURLtoBlob = (dataUrl: string) => {
const arr = dataUrl.split(",");
const mimeMatch = arr[0].match(/:(.*?);/);
if (!mimeMatch || mimeMatch.length < 2) {
throw new Error("Invalid data URL");
}
const mime = mimeMatch[1];
const bstr = atob(arr[1]);
const n = bstr.length;
const u8arr = new Uint8Array(n);
for (let i = 0; i < n; i++) {
u8arr[i] = bstr.charCodeAt(i);
}
return new Blob([u8arr], { type: mime });
};
const modules = useMemo(() => {
return {
toolbar: {
container: [
["image"],
[{ header: [1, 2, 3, false] }],
["bold", "italic", "underline", "strike", "blockquote"],
],
handlers: {
image: imageHandler,
},
},
imageDropAndPaste: {
handler: dropAndPasteHandler,
},
};
}, []);
dataURLtoBlob 함수에 대해서 알아보자
const arr = dataUrl.split(",");
Data URL은
data:[<mediatype>][;base64],<data>
와 같은 형태를 갖는다. 따라서,
const arr = dataUrl.split(",");는 쉼표(,)를 기준으로 문자열을 분할하여 메타데이터(예: data:image/png;base64)와 실제 데이터를 나눈다.
const mimeMatch = arr[0].match(/:(.*?);/);
if (!mimeMatch || mimeMatch.length < 2) {
throw new Error("Invalid data URL");
}
const mime = mimeMatch[1];
정규식 패턴 :(.*?);을 사용하여 MIME 타입(예: image/png)을 추출한다. 이 MIME 타입은 나중에 Blob을 생성할 때 사용된다.
const bstr = atob(arr[1]);
atob 함수는 Base64로 인코딩된 문자열을 디코딩하여 원래의 바이너리 문자열을 반환한다.
const n = bstr.length;
const u8arr = new Uint8Array(n);
for (let i = 0; i < n; i++) {
u8arr[i] = bstr.charCodeAt(i);
}
디코딩된 ASCII 문자열을 순회하며 각 문자의 charCodeAt 값을 사용하여 해당 바이트 값을 얻는다. 그런 다음 이 값을 Uint8Array (8비트 부호 없는 정수 배열)에 할당한다.
return new Blob([u8arr], { type: mime });
마지막으로, Uint8Array와 앞서 추출한 MIME 타입을 사용하여 Blob 객체를 생성한다.
일련의 문자열을 의미있는 토큰(token)으로 분해하고 그것들로 구성된 구조를 파악하는 과정을 말한다.
블로그의 관련 내용: 본 블로그에서는 dataUrl.split(",")와 같은 코드를 사용하여 Data URL을 분해하고 있다. 이렇게 문자열을 분해하는 과정도 파싱의 일종이라 할 수 있다. arr[0].match(/:(.*?);/)를 통해 MIME 타입을 추출하는 부분도 파싱의 한 예시로, 정규 표현식을 사용하여 특정 패턴에 맞는 문자열을 파싱하는 과정이다.
파일의 형식이나 내용을 설명하는 방법 중 하나이다. 원래는 이메일에서 비텍스트 데이터를 보내기 위해 만들어졌지만, 현재는 웹에서도 널리 사용된다. MIME 타입은 주요타입/부타입 형식으로 이루어져 있다.
예시)
text/plain: 일반 텍스트 파일
text/html: HTML 문서
image/jpeg: JPEG 이미지
image/png: PNG 이미지
audio/mp3: MP3 음악 파일
블로그의 관련 내용: Data URL 형식에서 Base64로 인코딩된 데이터 앞부분에 data:[MIME-type];base64,의 형식으로 MIME 타입이 포함될 수 있다. 이 MIME 타입은 해당 데이터가 어떤 형식의 파일인지를 나타낸다. 예를 들어, 이미지 데이터의 경우 data:image/jpeg;base64,와 같은 형식으로 시작할 수 있다.
1960년대에 만들어진 문자 인코딩 표준이다. 기본적인 영문 알파벳, 숫자, 특수 문자들을 7비트의 숫자 코드로 표현한다.
블로그의 관련 내용: Base64 인코딩은 바이너리 데이터를 ASCII 문자열 형태로 변환하여 웹에서 안전하게 전송하거나 저장하기 위한 방법이다. 이 ASCII 기반의 문자열은 블로그에서 제시된 Data URL의 일부로 볼 수 있다.
컴퓨터가 직접 읽고 쓸 수 있는 0과 1로 이루어진 데이터를 의미한다. 대부분의 파일(이미지, 동영상, 문서 등)은 바이너리 형식으로 저장된다.
블로그의 관련 내용: Base64로 인코딩된 Data URL을 디코딩하면 원래의 바이너리 데이터를 얻을 수 있다. 이 바이너리 데이터는 이미지를 표현하는 원시 데이터이다.
데이터를 특정한 형식이나 규칙에 따라 변환하는 과정이다. 웹에서는 데이터를 안전하게 전송하거나 표시하기 위해 특정한 형식으로 변환할 필요가 있다.
블로그의 관련 내용: 본 블로그의 문제 상황에서 imageData는 Base64로 인코딩된 Data URL 형식으로 제공되었다. Base64 인코딩은, 바이너리 데이터를 ASCII 문자열 형태로 변환하여 웹에서 안전하게 전송하거나 저장하기 위한 방법 중 하나이다.
인코딩된 데이터를 원래의 형태나 규칙으로 복원하는 과정이다.
블로그의 관련 내용: 해결과정에서 Data URL에서 추출한 Base64 인코딩된 부분을 atob 함수를 사용하여 디코딩하였다. 디코딩된 결과는 원래의 바이너리 데이터이다.
데이터를 URL 형식으로 표현하는 방법이다. 주로 Base64 인코딩된 데이터를 data: 접두사와 함께 MIME 타입과 결합하여 사용한다.
블로그의 관련 내용: 본 블로그에서 imageData는 Base64로 인코딩된 Data URL 형식으로 제공되었다. Data URL은 웹에서 이미지나 다른 리소스를 직접 임베드하는데 사용된다.
대용량의 바이너리 데이터를 나타내는 오브젝트이다. 웹에서는 파일 I/O 작업이나 바이너리 데이터의 전송 및 조작을 위해 Blob을 사용한다.
블로그의 관련 내용: 해결과정에서 디코딩한 바이너리 데이터를 사용하여 Blob 오브젝트를 생성하였다. 생성된 Blob은 서버에 이미지 데이터로 업로드하기 위해 사용되었다.
File 객체는 Blob을 기반으로 하며, Blob의 모든 특징을 가지고 있지만 추가적으로 파일 이름이나 수정 시간과 같은 메타데이터를 가질 수 있다.
블로그의 관련 내용: 블로그에서는 Data URL로부터 추출한 바이너리 데이터를 Blob 객체로 변환하는 과정을 보여준다.
(이 Blob 객체는 다음 단계에서 File 객체로 변환되거나 다른 용도로 활용될 수 있다.)
이미지나 다른 바이너리 데이터를 처리할 때는 해당 데이터의 형식과 특성을 정확히 파악하는 것이 중요하다. 특히 웹 개발에서 다양한 데이터 형식과 API가 있기 때문에, 이들 간의 차이와 사용 방법을 명확히 이해하는 것이 중요하다. 본 문제도 Data URL과 Blob 간의 차이점을 공부하여 이해함으로써 해결할 수 있었다.