[Javascript] Base64 이미지 최적화
References
https://ko.wikipedia.org/wiki/베이스64
https://blue-boy.tistory.com/227
서론
💡 핵심만 보고 싶으신 분은 하단의 “그래서 어떻게 최적화 할 것인가?” 파트만 보시면 됩니다.
글과 사진을 다채롭게 표현 하고자 할 때 위지윅(WYSIWYG, What You See Is What You Get) 기능이 담긴 텍스트 에디터가 필요한 순간이 있다.
그러나 어느 순간 이미지와 같은 데이터가 포함 된 글을 포스팅 하기 전에 반드시 부딪히는 순간이 있는데 바로, Base64 이다.
Base64란?
위키피디아에선 Base64를 다음과 같이 정의하고 있다.
8비트 이진 데이터(예를 들어 실행 파일이나, ZIP 파일 등)를 문자 코드에 영향을 받지 않는 공통 ASCII 영역의 문자들로만 이루어진 일련의 문자열로 바꾸는 인코딩 방식을 가리키는 개념 (… 후략)
즉, 우리가 접하고 있는 이미지와 동영상과 같은 이진(바이너리) 데이터를 컴퓨터가 인식할 수 있는 **로우 레벨 단계(공통 ASCII 코드 문자)**로 번역된 것을 Base64라 이해하면 편할 것이다.
그런데 Base64가 왜?
우선, ‘Base64가 잘못된 기능은 아니다.’ 라고 짚고 넘어가고 싶다.
왜냐하면 앞서 말했다시피 데이터를 다루는 과정에서 컴퓨터가 이해할 수 있도록 보다 안전한 공통 ASCII 코드 문자 형태로 운용되기 때문이다.
다만, 기존 바이너리 데이터에서 상대적으로 저차원인 Base64로 변환하는 경우 그만큼 더 변환되는 크기가 커질 수 밖에 없다.
이는 곧 DB에 Base64가 그대로 적용된 상태에서 insert가 될 경우 그만큼 부하가 온다는 의미가 되는데 그렇다면, 전 후를 비교하여 데이터가 얼마나 커지는지 한 번 알아보자.
base64 적용 전 / 후
텍스트 에디터에 첨부하고자 하는 이미지의 크기는 다음과 같다.
그리고 텍스트 에디터에 적용 후, Base64에 적용한 이미지의 크기는 다음과 같다.
이처럼 1~2개 아닌 데이터의 경우 당장 체감이 오지 않을 수 있지만 이것이 수십개, 수백개 이상의 규모가 될 경우 상당한 크기의 데이터 규모를 부담할 수 밖에 없다.
그래서 어떻게 최적화 할 것인가?
결국 이러한 문제로 인해 Base64 형태의 데이터를 최적화는 필수 불가결 해지는데 필자는 다음과 같이 추상화 및 구현을 하기로 했다.
주어진 조건
- Javascript (예제는 NextJS에서 이루어졌으나 구현 할 코드 대부분이 Plane 자바스크립트 코드)
- Google Firebase Storage
- Quill Editor
요구사항 및 추상화
1. 텍스트 에디터 작성이 완료되고 submit 클릭 이벤트가 이루어질때 최적화 한다.
로직 단순화 및 파이어베이스 스토리지 파일 업로드 및 다운로드 하는 과정을 최소화 하기 위해서 이다.
2. 텍스트 에디터의 base64 이미지 경로를 탐색하여 배열 A에 담는다.
이후 파이어베이스 스토리지에 저장된 이미지 url로 변경할 때 비교 군으로 쓰기 위해서 이다.
3. base64 이미지를 File 객체의 형태로 변환 후 배열 B에 담는다.
파이어베이스 스토리지에 업로드 및 다운로드 과정을 최적화 하기 위해서 이다.
4. File 객체로 변환한 이미지 이름에 규칙을 부여한다.
파이어베이스 스토리지의 경우, 같은 이름의 파일명이 업로드 될 때 알아서 덮어쓰기가 되는 기능이 있다.
따라서, 이후 텍스트 에디터 내용에 UPDATE 기능이 생길 때 이를 대비할 목적으로 각 이미지 명 마다 규칙을 부여 한다.
5. 파이어베이스 스토리지에 업로드 후 데이터의 URL을 받아 배열 C에 담는다.
이유는 2번과 동일…
6. 기존에 작성한 텍스트 에디터 내용을 탐색 후 배열 A의 요소와 매칭될 때 배열 C의 요소로 변경한다.
적용 전 내용, 비교할 내용 들, 변경할 내용 들을 각 그룹으로 두어
최종적으로 파이어베이스 스토리지에 업로드 된 이미지 URL를 적용한 내용으로 변경을 마친다.
요구사항 및 추상화
1. 작성한 내용 속 base64 이미지 경로 추출 (배열 A)
const findBase64s = (includeBase64) => {
// 정규식을 이용하여 base64 경로만 추출한다.
const base64Match = Array.from(includeBase64.matchAll(/<img[^>]+src=["']([^'">]+)['"]/gi));
return base64Match.map(item => item.pop() || '');
}
// contents: event.htmlValue
const extractBase64s = findBase64s(contents);
2. base64 경로를 File 객체로 변환 (배열 B)
const convertBase64ToFiles = (base64URLs, imageUuid) => {
let base64ToFiles = [];
for (let i=0; i<base64URLs.length; i++) {
const arr = base64URLs[i].split(',');
const mime = arr[0].match(/:(.*?);/)[1];
const fileExtension = mime.substring('image/'.length, mime.length);
const bstr = atob(arr[1]);
const n = bstr.length;
const u8arr = new Uint8Array(n);
while(n--) {
u8arr[n] = bstr.charCodeAt(n);
}
base64ToFiles.push(new File([u8arr], `${imageUuid}_${i}.${fileExtension}`, {type:mime}));
}
return base64ToFiles;
}
// imageUuid: 파일명에 규칙성을 부여하기 위해 필자는 Uuid 뒤에 넘버링을 붙이기로 했다
const base64ToFiles = convertBase64ToFiles(extractBase64s, imageUuid);
3. 배열 B를 파이어베이스 스토리지에 업로드 후 downloadURL 가져오기 (배열 C)
/**
* contentsStorageRef 중복코드가 있는데 이건 사전에 함수 scope 밖에 선언하자...
*/
const uploadContentsImages = async (storage, imageFiles, imageUuid, uid) => {
for (let i=0; i<imageFiles.length; i++) {
const contentsStorageRef = firebaseStorage.ref(storage, `customercontentsimages/${uid}/${imageUuid}_${i}`);
await firebaseStorage.uploadBytes(contentsStorageRef, imageFiles[i])
.then((snapshot) => {
console.log(`thumbnail images ${i}`, snapshot);
}).catch((error) => {
console.log("contents image upload error!", error);
});
}
}
const downloadContentsImages = async (storage, imageFiles, imageUuid, uid) => {
let downloadURLs = [];
for (let i=0; i<imageFiles.length; i++) {
const contentsStorageRef = firebaseStorage.ref(storage, `customercontentsimages/${uid}/${imageUuid}_${i}`);
await firebaseStorage.getDownloadURL(contentsStorageRef)
.then((url) => {
downloadURLs.push(url);
}).catch((error) => {
console.log("contents image download error!", error);
});
}
console.log("downloadURLs", downloadURLs);
return downloadURLs;
}
await uploadContentsImages(storage, base64ToFiles, thumbnailUuid, contentObj.uid);
const contentImages = await downloadContentsImages(storage, base64ToFiles, thumbnailUuid, contentObj.uid);
4. 기존 내용, 배열 A와 대조 후 base64 경로가 일치하면 배열 C로 변경하기
let afterContents = contents;
for (let i=0; i<base64ToFiles.length; i++) {
afterContents = afterContents.replaceAll(extractBase64s[i], contentImages[i]);
}
되…된다!
이 후 개선할 사항
작성한 코드는 어디까지나 텍스트 에디터 내용에 이미지가 들어가 있을 경우에만 작성이 되었다.
따라서, 이미지가 없는 일반 글로만 작성된 경우에도 분기처리를 하여야 한다.
또한, 작성글 업데이트시 파이어베이스 스토리지에 더미 데이터가 남는 경우도 감안을 해야 할 것 같다.