안녕하세요! 올리브영에서 리뷰/SNS 프론트엔드 개발을 담당하고 있는 d9입니다.
저희 조직에서는 올리브영의 SNS, 셔터 개발과 운영을 맡고 있습니다. 셔터는 올리브영 앱에서 유저가 콘텐츠를 업로드하고 자유롭게 소통할 수 있는 커뮤니티입니다. 그렇다 보니 이미지 업로드 기능이 사용 경험에 직접적인 영향을 주는 핵심 요소로 자리잡고 있습니다.
그런데 수많은 고화질 이미지를 다루다 보니, 모바일 환경에서 아래와 같은 문제가 종종 발생했습니다.
- 고해상도 이미지 처리에 메모리 과다 사용
- 처리 과정에 UI 반응 저하
- 위 문제가 지속될 경우 브라우저 강제 새로고침

[고해상도 이미지를 렌더링하는 도중 강제 새로고침 되는 예시]
문제 분석
그렇다면 왜 이러한 현상이 발생하는 걸까요? 해결 방안을 찾기 위해 먼저 문제를 분석해보았습니다.
모바일 이미지의 특성
요즘은 스마트폰 하나로도 고해상도 이미지를 생성할 수 있습니다. 기술의 발전에 따라 기기의 기본 성능이 좋아졌고, 속도와 퀄리티에 반응하며 더 좋은 결과물을 추구하는 소비자 입맛에 잘 맞습니다. 예를 들어 아이폰은 HEIC이라는 고급 압축 포맷을 사용하지만, 업로드 시 호환성 문제로 JPEG로 변환됩니다. 이 과정에 파일 용량이 커집니다.
[모바일에서 업로드된 대용량 이미지 예시]
대용량 이미지는 일반적으로 이런 특성이 있습니다.
- 크기 4MB 이상
- 해상도 4000px 이상
- HEIC > JPEG 변환 시 품질 저하 없이 용량 증가
- 메타데이터(EXIF) 포함으로 추가 용량 발생
이로 인해 몇 가지 문제가 발생하는데, Web Worker를 도입하는 과정에 고려한 문제는 두 가지입니다.
문제 1: 브라우저의 이미지 처리 부하
셔터에서는 사용자가 게시물을 작성할 때, 앱 내에서 이미지를 선택하면 업로드 전에 해당 이미지를 미리 볼 수 있도록 이미지 컴포넌트를 렌더링합니다. 이 과정에서 브라우저는 다음과 같은 절차를 따르게 됩니다.
[셔터의 게시물 업로드 화면(좌)과 동작을 표현한 상태 다이어그램(우)]
브라우저에서 진행하는 이미지 처리 과정과 이 때 발생하는 문제를 조금 더 자세히 살펴보겠습니다.
먼저, 이미지를 디코딩 후 메모리에 로드합니다. 이미지를 JPEG로 디코딩한 후 압축 해제합니다. 이후 메모리를 할당해 픽셀 데이터로 저장합니다.
다음은 메인 스레드에서 레이아웃/페인트 작업을 준비합니다. 이미지 크기와 DOM 레이아웃을 계산한 후, 렌더 트리를 구성하면 페인트 명령을 생성할 수 있습니다.
축소된 이미지라도 원본 리소스를 기반으로 처리되기 때문에 원본 이미지의 모든 픽셀 데이터를 유지합니다. 이는 표시 이미지의 크기와 무관하게 메모리를 사용하는 과정이며, 따라서 리샘플링과 스케일링 연산이 필요합니다.
문제는 이미지가 많아질수록 가중됩니다. 레이아웃을 계산하는 시간이 늘어남은 물론, 페인트 과부하가 발생합니다. 그러면 메인 스레드가 막히면서 결국 전체 페이지 성능에 영향을 미치게 됩니다. iOS 환경은 특히 메모리를 엄격하게 제한하고 있어서, UI 반응 속도가 저하되면 브라우저를 다시 로드하는 경우가 있습니다. 포스트 초반에 언급한 강제 새로고침이 바로 그 예입니다.
문제 2: 업로드 성능과 UX
셔터 플랫폼에서는 최대 10장의 이미지를 업로드할 수 있는데, 이 때 최대 40MB의 데이터를 전송해야 합니다.
네트워크 환경별 업로드 시간 분석해보면, 일반적인 LTE 환경과 불안정한 네트워크의 업로드 속도와 예상 소요 시간은 약 2배에 달하는 차이를 보입니다.
[일반적인 4G LTE 환경]
- 평균 업로드 속도: 11.34Mbps
- 40MB = 320 Megabits
- 예상 소요 시간: 320 ÷ 11.34Mbps ≈ 28.2초
[불안정한 네트워크 환경]
- 평균 업로드 속도: 5Mbps
- 예상 소요 시간: 320 ÷ 5 = 64초
업로드 시간이 길어지면 또다른 문제를 야기합니다.
- 사용자 불만족
업로드 시간이 오래 걸려도 즉각적인 피드백이 없다 보니, 업로드 진행 상태를 명확히 확인할 수 없습니다. 업로드에 실패할 경우 재시도해야 하는 불편함도 있어 사용자의 불만이 쌓이게 됩니다.
- 리소스 낭비
불필요한 네트워크 대역폭을 사용하며 서버 저장공간 비효율적으로 사용하게 됩니다. 이로 인해 CDN 비용이 증가하는 리소스 낭비를 피할 수 없습니다.
- 에러 발생 가능성
네트워크 타임아웃이나 메모리 부족 에러가 발생할 수 있습니다. 업로드 중단 또한 피할 수 없는 이슈 중 하나입니다.
이처럼 모바일 네트워크 환경에서 업로드 시간이 길어지면 사용자는 곧바로 리뷰에 불만을 표시하는 경향이 있었습니다.
문제 해결 시도
이러한 문제를 해결하기 위해 시도한 방법을 소개합니다.
1. 포맷 변환
이미지를 WebP 등으로 변환해보았지만, 복잡한 이미지일수록 압축 효율이 낮았습니다. 이는 손실 압축 방식의 특성상, 복잡한 이미지에서는 줄일 수 있는 정보가 적기 때문입니다. 효율적인 용량 감소를 위해 품질을 낮추면 시각적인 퀄리티 저하가 발생했고, 그로 인해 실제 적용에는 제약이 있었습니다.
2. 이미지 리사이징 (Canvas)
Canvas를 활용해 이미지를 리사이징하여 크기를 줄이는 시도는 성공적이었으며, 파일 크기 감소로 업로드 속도는 크게 개선되었습니다. 그러나 새로운 문제가 발생했는데, 바로 Canvas 자체 처리 비용으로 메인 스레드를 점유하는 것, 디코딩이나 인코딩 과정에 JS 블로킹이 발생하는 것. 뿐만 아니라 디스크 IO까지 밀리는 바람에 클릭 이벤트를 무시하는 현상도 발생했습니다.
[메인 스레드가 잠기면서 클릭이 지연되는 예시]
왜 Web Worker가 필요했나?
앞서 포맷 변환, 리사이징, 압축 등의 방식으로 자체 해결을 시도했지만, 대부분의 메인 스레드에서 처리되면서 오히려 새로운 병목을 초래했습니다. 결국, 구조적 개선이 필요하다는 결론에 도달하게 되었죠.
Web Worker가 적합한 이유
이미지 업로드 전 처리 작업은 아래와 같은 특성을 갖고 있습니다.
- CPU 연산이 집중적으로 필요한 작업 (디코딩, 리사이징, 포맷 변환 등)
- DOM 접근이 불필요한 비동기 연산
- UI 스레드와 병렬로 동작하면 이상적인 작업
이 조건에 완벽히 부합하는 것이 바로 Web Worker입니다. Web Worker를 사용하면 여러가지 개선 효과를 얻을 수 있었습니다.
- UI 반응성 유지하면서 이미지 처리 가능
- 메모리 사용 분산
- 모바일에서도 안정적인 UX 확보
Web Worker란?
그렇다면 Web Worker는 무엇일까요? Web Worker는 JavaScript에서 사용할 수 있는 백그라운드 스레드 실행 환경입니다. 브라우저의 메인 스레드와 완전히 분리되어 비동기 작업을 처리할 수 있습니다.
주요 특징
Web Worker의 가장 큰 특징은 메인 스레드와 완전 분리되어 백그라운드에서 실행된다는 점입니다. 이로 인해 CPU 집약적인 작업을 별도로 처리할 수 있지만, 제약 사항도 있습니다. Worker는 DOM에 접근할 수 없으며, 일부 window 객체의 메소드 사용이 제한됩니다. 메인 스레드와는 postMessage
와 onmessage
방식의 메시지 기반 통신으로만 데이터를 주고받을 수 있습니다.
Web Worker 종류
종류 | 주요 특징 |
---|---|
Dedicated Worker | 단일 페이지에서 동작하는 독립적인 Worker. 1:1 통신으로 데이터 처리에 최적화 |
Shared Worker | 여러 탭/창에서 공유되는 Worker. 상태 공유나 실시간 데이터 처리에 활용 |
Service Worker | 네트워크 프록시 역할을 하는 Worker. 오프라인 지원, 캐싱, 푸시 알림 등에 사용 |
셔터에 적합한 Worker는?
여러 Worker 종류 중에서 이미지 처리에는 Dedicated Worker가 가장 적합했습니다. 1:1 통신 구조로 단순하고, 독립적인 처리가 가능하며, 리소스 관리도 용이하기 때문입니다. 반면 Shared Worker나 Service Worker는 이미지 처리에 불필요한 기능들이 많아 배제했습니다.
실제 적용 사례
OffscreenCanvas를 활용한 이미지 처리
OffscreenCanvas를 사용하면 메모리 효율적으로 이미지를 리사이징할 수 있습니다. Worker 없이도 간단하게 이미지 크기를 조정하고 포맷을 변경할 수 있죠.
async function resizeImage(file, maxWidth = 800, maxHeight = 800) {
// 이미지 파일을 비트맵으로 변환
const bitmap = await createImageBitmap(file);
// 비율 계산 (가로/세로 비율 유지)
const ratio = Math.min(maxWidth / bitmap.width, maxHeight / bitmap.height, 1);
const width = bitmap.width * ratio;
const height = bitmap.height * ratio;
// OffscreenCanvas 생성 및 이미지 그리기
const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0, width, height);
// JPEG 형식의 Blob으로 변환
return canvas.convertToBlob({
type: 'image/webp',
quality: 0.8
});
}
// 사용 예시
const compressedBlob = await resizeImage(imageFile);
이 방식은 메인 스레드에서 실행되지만, OffscreenCanvas는 원래 Web Worker에서 사용하기 위해 설계된 API로, 일반 Canvas와 달리 DOM에 연결되지 않습니다. 이러한 특성 덕분에 메인 스레드에서 사용하더라도 메모리 효율성이 높고 렌더링 성능에 이점이 있습니다. 특히 단일 이미지 처리나 간단한 리사이징 작업에 효과적입니다.
Base64 → File 변환 워커
네이티브 앱과의 연동 과정에서 base64 이미지 데이터를 처리하는 경우가 많았습니다. 이 변환 과정을 Worker로 분리한 이유는 두 가지입니다.
- 대용량 문자열 처리
Base64 문자열은 바이너리 대비 약 1.37배 크기입니다. 그래서 디코딩 과정에 메모리 사용량이 급증하고, 그 과정에 메인 스레드 블로킹 현상이 발생하기에 이를 대비해야 했습니다.
- 반복적인 변환 작업
셔터는 여러 이미지를 업로드할 수 있어 이미지 처리 역시 동시에 진행해야 합니다. 이미지를 변환할 때마다 연산 처리가 무거워지면서 UI 응답 속도 또한 저하되는 문제를 해결해야 했습니다.
구현 코드
// base64-worker.js
self.onmessage = function(event) {
const { base64String, fileName, mimeType } = event.data;
try {
// base64 데이터 정제
const pureBase64 = base64String.split(',')[1] || base64String;
// base64 → 바이너리 변환
const binary = atob(pureBase64);
const byteArray = new Uint8Array([...binary].map(char => char.charCodeAt(0)));
// Blob 생성 및 File 객체로 변환
const blob = new Blob([byteArray], { type: mimeType });
const file = new File([blob], fileName, { type: mimeType });
self.postMessage({ success: true, file });
} catch (error) {
self.postMessage({ success: false, error: error.message });
}
};
// main.js
const worker = new Worker('base64-worker.js');
function convertBase64ToFile(base64String, fileName, mimeType) {
return new Promise((resolve, reject) => {
worker.onmessage = (e) => {
if (e.data.success) resolve(e.data.file);
else reject(new Error(e.data.error));
};
worker.postMessage({ base64String, fileName, mimeType });
});
}
성능 측정 결과
Base64 문자열 데이터를 이미지 파일로 변환하고, 해당 이미지를 리사이징하여 화면에 표시할 준비를 마치는 데까지 걸린 총 시간과 이 과정에서의 메인 스레드 블로킹 시간을 Worker 도입 전후로 비교 측정한 결과입니다.
[Base64 변환 - 2.31MB 이미지 데이터 기준]
- 처리 시간: 388ms → 137ms (65% 감소)
- 메인 스레드 블로킹: 350ms → 8ms (97% 감소)
메인 스레드 블로킹 시간이 크게 줄어들었을 뿐만 아니라, 총 처리 시간까지 단축된 것을 볼 수 있습니다. 이는 Worker가 별도 스레드/코어에서 메인 스레드의 다른 작업과 경쟁 없이 병렬로 실행되어, 통신 오버헤드를 상쇄할 만큼 전담 처리 효율이 높기 때문입니다. 물론, 이 결과는 특정 테스트 환경에서의 측정치이며, 실제 사용자 환경(기기 사양, 브라우저 등)에 따라 달라질 수 있습니다.
마무리
Web Worker는 성능 최적화를 넘어 웹 아키텍처를 구조적으로 개선할 수 있는 강력한 도구입니다. 이미지 처리처럼 무거운 연산을 메인 스레드에서 분리함으로써 UI는 부드럽게 유지하고, 메모리를 효율적으로 활용할 수 있었습니다. 이 덕분에 사용자 경험 역시 크게 향상됩니다.
Worker로 성능 개선에 성공했으니, 앞으로는 이를 전략적으로 활용해볼 계획입니다. 더 나은 서비스를 기대하셔도 좋습니다!
읽어주셔서 감사합니다! 😊