Base64 디코딩 성능 최적화
프론트엔드에서 실시간으로 Base64로 인코딩된 온도 데이터를 디코딩해 Float32Array로 변환해야 하는 작업이 있었습니다. 그런데 개발자 도구로 성능을 분석해보니, 디코딩 과정에서 상당한 시간이 소요되고 있었습니다. 개선 전에는 Promise 기반의 비동기 로직을 사용했지만, 실제로는 동기 처리만으로 충분한 작업이었기에 오히려 오버헤드를 유발하고 있었습니다.
이 코드 개선만으로도 993.9ms → 60.5ms, 약 16.4배의 성능 향상을 얻을 수 있었고, 그 과정을 아래에 정리했습니다.
개발 환경
- Node v22.16.0
- React v18.3.1 (JavaScript) + CRA
Base64란 무엇인가?
Base64는 바이너리 데이터를 문자 데이터(영문자, 숫자, 특수문자로만 이루어진 텍스트)로 인코딩하는 표준 방식 중 하나입니다. 주로 이미지, 오디오, 파일 등 이진 데이터를 텍스트로 변환하여, 텍스트만 처리 가능한 환경에서 데이터를 안전하게 전송하기 위해 사용됩니다.
일부 시스템(예: 이메일, 웹 API, JSON, XML 등)은 이진 데이터를 직접 전송할 수 없습니다. 이때, 이진 데이터를 텍스트로 변환해 전달하면 데이터 손상 없이 안정적으로 주고받을 수 있습니다.
1. 기존 코드
기존 코드는 if 조건문 안에서 Promise를 생성해 모든 디코딩 로직을 비동기 방식으로 처리했습니다. 하지만, 실질적으로 계산 작업에서는 Promise(비동기)를 사용한다고 해서 성능이 크게 저하되지는 않습니다. 오히려 성능 저하의 주요 원인은, 반복적으로 호출되는 콜백 함수에서 발생하는 함수 호출 오버헤드입니다.
- base64로 인코딩된 문자열을 atob() 함수로 바이너리 문자열로 디코딩
- c.charCodeAt(0)을 사용해 바이너리 문자열의 각 문자를 아스키 코드(숫자)로 변환 → 이 부분이 콜백됨
- 이 숫자들을 Uint8Array로 바이트 배열에 담아 tempBuffer에 저장 → 콜백 함수 사용하여 반복
- new Float32Array(tempBuffer.buffer)를 사용해 tempBuffer(바이트 배열)의 메모리 버퍼를 4바이트씩 끊어 실수(32비트 float) 배열로 해석
- 이렇게 변환된 실수 배열(temperatureByteArray)을 반환하여 온도 데이터로 사용
export async function decodeBase64TempData(temp) {
if (temp) {
return new Promise((resolve, reject) => {
try {
const tempBuffer = Uint8Array.from(atob(temp), c => c.charCodeAt(0));
const temperatureByteArray = new Float32Array(tempBuffer.buffer);
resolve(temperatureByteArray);
} catch (error) {
console.log("decode error!");
reject(error);
}
});
} else {
return;
}
}
문제점 분석
- Uint8Array.from의 성능 저하 (콜백 함수 사용)
- 불필요한 Promise 비동기 처리 (I/O가 없는 순수 계산 코드에 사용할 경우 오히려 컨텍스트 스위칭 비용이 발생)
- 불필요한 if-else 구조
2. 개선 코드
불필요한 Promise를 제거하고 전체 로직을 동기적으로 단순화했습니다. Base64 디코딩 이후에는 for 루프를 사용해 바이트 배열을 직접 구성하며, Float32Array로 변환했습니다.
- 입력된 base64 문자열이 없으면 아무 작업도 하지 않고 종료
- base64로 인코딩된 문자열을 atob() 함수로 바이너리 문자열로 디코딩
- 바이너리 문자열의 길이를 저장하고, Uint8Array로 그 길이만큼의 bytes(바이트 배열) 생성 (초기화)
- 바이트 문자열의 길이만큼 c.charCodeAt(0)을 반복하여 각 문자를 아스키 코드(숫자)로 변환하여 bytes의 각 자리에 저장
- new Float32Array(bytes.buffer)를 사용해 bytes(바이트 배열)의 메모리 버퍼를 4바이트 단위로 끊어서 실수(32비트 float) 배열로 해석
- 이렇게 변환된 실수 배열(temperatureByteArray)을 반환하여 온도 데이터로 사용
export async function decodeBase64TempData(temp) {
if (!temp) return;
try {
const binaryString = atob(temp);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return new Float32Array(bytes.buffer);
} catch (err) {
console.error("decode error", err);
throw err;
}
}
3. 개선 결과
실행 시간은 기존 993.9ms에서 60.5ms로 대폭 줄어들었으며, 이는 약 16.4배의 성능 개선에 해당합니다.
콜백 함수 방식과 for문 방식의 성능적 차이
콜백 함수 방식
- 문자열의 각 문자마다 콜백 함수(c => c.charCodeAt(0))가 개별적으로 실행되므로, 문자 개수만큼 함수 호출 오버헤드가 발생합니다.
- 데이터가 많을수록 이 오버헤드는 무시할 수 없을 정도로 커집니다.
for문 방식
- 함수 호출 없이, 단순히 인덱스를 증가시키며 배열에 값을 할당합니다.
- 불필요한 함수 호출이 없어 더 빠르고 메모리 사용도 효율적입니다.
- 특히 대용량 데이터 처리에서 for문 방식이 월등히 우수합니다.
개발자 도구로 확인
'개발 기록 > [CTSF] 25.05.12-' 카테고리의 다른 글
[영상 렌더링] React + canvas로 열화상 영상 실시간 렌더링하기 (1) | 2025.06.17 |
---|