·
technicalwebassemblyperformance

향상된 Squoosh 구축: libimagequant-wasm으로 고성능 로컬 이미지 압축

향상된 Squoosh 구축: libimagequant-wasm으로 고성능 로컬 이미지 압축

Google Squoosh의 아키텍처를 libimagequant-wasm, Web Workers 및 최신 웹 API와 결합하여 개인정보 보호 우선, 고성능 이미지 압축 도구를 구축한 방법에 대한 심층 분석.

Google이 2023년 초 Squoosh 종료를 발표했을 때, 웹 개발 커뮤니티는 클라이언트 측 이미지 압축을 위한 귀중한 도구를 잃었습니다. OneImage에서는 이 기능을 보존하는 것뿐만 아니라 향상시킬 기회를 보았습니다. 이 글은 개인정보 보호, 성능 및 개발자 경험을 우선시하는 프로덕션 준비가 된 향상된 Squoosh 버전을 구축하기 위해 취한 기술적 접근 방식을 자세히 설명합니다.

Google Squoosh의 아키텍처 이해

Google Squoosh는 WebAssembly(WASM)를 사용하여 브라우저에서 이미지 인코더를 완전히 실행하는 개념의 선구자였습니다. 원래 아키텍처는 다음으로 구성되었습니다:

  • 클라이언트 측 처리: 모든 압축이 로컬에서 발생하여 개인정보를 보장
  • WebAssembly 코덱: WASM으로 컴파일된 네이티브 속도 인코더(MozJPEG, OxiPNG, WebP, AVIF)
  • Web Workers: UI 차단을 방지하기 위한 무거운 계산 오프로드
  • Canvas API: 이미지 조작 및 미리보기 생성

혁신적이었지만 Squoosh에는 한계가 있었습니다:

  • PNG 압축은 OxiPNG에만 의존했으며, 이는 속도보다 압축 비율을 우선시했습니다
  • 내장 배치 처리 기능 없음
  • 일반적인 사용 사례에 대한 제한된 프리셋 구성
  • UI와 압축 로직이 긴밀하게 결합됨

우리의 향상 전략

1. 우수한 PNG 압축을 위한 libimagequant-wasm 통합

우리의 향상의 핵심은 업계 표준 pngquant 라이브러리의 WebAssembly 포트인 libimagequant-wasm입니다. 이 라이브러리는 간단한 팔레트 축소에 비해 시각적으로 우수한 결과를 생성하는 정교한 색상 양자화 알고리즘을 사용합니다.

왜 libimagequant인가?

  • 지각적 품질: 인간 지각에 최적화된 수정된 중앙값 컷 알고리즘 사용
  • 적응형 팔레트: 이미지 내용을 기반으로 2-256 색상의 최적 팔레트 생성
  • 투명도 처리: 압축하면서 알파 채널 보존
  • 성능: WASM 덕분에 거의 네이티브 속도로 실행

구현 세부사항

압축 파이프라인에 libimagequant를 통합한 방법은 다음과 같습니다:

import LibImageQuant from '@fe-daily/libimagequant-wasm'; 
import * as wasmModule from '@fe-daily/libimagequant-wasm/wasm/libimagequant_wasm.js';

async function compressPNG(imageData: ImageData, level: number): Promise<Uint8Array> {
  const quantizer = new LibImageQuant({ wasmModule });
  
  // 압축 수준 (0-10)을 색상 수 (256-2)로 매핑
  const maxColors = Math.max(2, 256 - (25.6 * level));
  
  const quantized = await quantizer.quantizeImageData(imageData, {
    maxColors: Math.floor(maxColors),
    speed: 1,          // 품질과 속도의 균형
    quality: {
      min: 0,
      target: 100      // 색상 제한 내에서 최고 품질 목표
    }
  });
  
  return new Uint8Array(quantized.pngBytes);
}

주요 매개변수 설명:

  • maxColors: 팔레트 크기 제어. 적은 색상 = 작은 파일, 하지만 잠재적으로 나쁜 품질
  • speed: 1-10 범위, 1은 가장 느리지만 최고 품질
  • quality.target: 대상 품질 임계값 설정 (0-100)

우리의 Squoosh 도구는 이 구현을 사용하여 최소한의 지각적 손실로 60-80%의 압축률을 제공합니다.

2. 강력한 Web Worker 시스템 설계

압축 중(특히 큰 이미지 또는 배치 작업의 경우) 브라우저가 멈추는 것을 방지하기 위해 전용 Web Worker 아키텍처를 구축했습니다:

// compression-worker.ts
import { EncoderOptions, CompressResult } from './squoosh-types';
import LibImageQuant from '@fe-daily/libimagequant-wasm'; 
import * as wasmModule from '@fe-daily/libimagequant-wasm/wasm/libimagequant_wasm.js';

interface CompressMessage {
  type: 'compress';
  imageData: ImageData;
  options: EncoderOptions;
}

self.onmessage = async (e: MessageEvent<CompressMessage>) => {
  const { type, imageData, options } = e.data;

  if (type === 'compress') {
    try {
      const result = await compress(imageData, options);
      self.postMessage({ success: true, result });
    } catch (error) {
      self.postMessage({
        success: false,
        error: error instanceof Error ? error.message : '알 수 없는 오류',
      });
    }
  }
};

async function compress(
  imageData: ImageData,
  options: EncoderOptions
): Promise<CompressResult> {
  switch (options.type) {
    case 'png':
      return await compressPNGWithQuantization(imageData, options);
    case 'jpeg':
      return await compressJPEG(imageData, options);
    case 'webp':
      return await compressWebP(imageData, options);
    case 'avif':
      return await compressAVIF(imageData, options);
  }
}

Worker 이점:

  • 압축 중 차단되지 않는 UI
  • 장기 실행 작업을 취소할 수 있는 기능
  • 배치 작업을 위한 병렬 처리(여러 워커)
  • 메인 스레드 메모리 누수를 방지하는 메모리 격리

3. 스마트 압축 프리셋 구축

원시 인코더 매개변수를 노출하는 대신 일반적인 사용 사례에 최적화된 세 가지 프리셋을 만들었습니다:

const PRESET_CONFIGS = {
  highQuality: {
    png: { level: 3 },     // ~200 색상
    jpeg: { quality: 90 },
    webp: { quality: 90 },
    avif: { quality: 85 }
  },
  balanced: {
    png: { level: 5 },     // ~128 색상
    jpeg: { quality: 80 },
    webp: { quality: 80 },
    avif: { quality: 70 }
  },
  minSize: {
    png: { level: 8 },     // ~50 색상
    jpeg: { quality: 60 },
    webp: { quality: 60 },
    avif: { quality: 50 }
  }
};

이러한 프리셋은 최적의 품질-크기 절충안을 찾기 위해 다양한 이미지 유형(사진, 일러스트레이션, 스크린샷, UI 요소)에 대한 광범위한 테스트를 통해 보정되었습니다.

4. 효율적인 배치 처리 구현

여러 이미지를 압축하는 사용자를 위해 진행률 추적이 있는 대기열 시스템을 구축했습니다:

class BatchCompressor {
  private queue: BatchItem[] = [];
  private activeWorkers: Set<Worker> = new Set();
  private maxConcurrency = navigator.hardwareConcurrency || 4;

  async processBatch(files: File[], options: ProcessorOptions) {
    const batchId = Date.now();
    
    for (const file of files) {
      this.queue.push({
        id: `${batchId}-${file.name}`,
        file,
        status: 'pending',
        options
      });
    }

    await this.processQueue();
  }

  private async processQueue() {
    while (this.queue.some(item => item.status === 'pending')) {
      if (this.activeWorkers.size < this.maxConcurrency) {
        const item = this.queue.find(i => i.status === 'pending');
        if (item) {
          item.status = 'processing';
          await this.processItem(item);
        }
      } else {
        await new Promise(resolve => setTimeout(resolve, 100));
      }
    }
  }
}

이 접근 방식은:

  • 병렬 워커를 실행하여 CPU 사용률 극대화
  • 동시성을 제한하여 브라우저 충돌 방지
  • 사용자에게 실시간 진행 업데이트 제공

OneImage Squoosh에서 배치 처리를 실제로 사용해 보세요.

성능 최적화

메모리 관리

큰 이미지는 브라우저 메모리를 빠르게 고갈시킬 수 있습니다. 여러 완화 전략을 구현했습니다:

async function processLargeImage(file: File): Promise<CompressResult> {
  const MAX_DIMENSION = 4096;
  const img = await loadImage(file);
  
  // 필요한 경우 축소
  let { width, height } = img;
  if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
    const scale = Math.min(MAX_DIMENSION / width, MAX_DIMENSION / height);
    width = Math.floor(width * scale);
    height = Math.floor(height * scale);
  }
  
  // 더 나은 메모리 처리를 위해 가능한 경우 OffscreenCanvas 사용
  const canvas = typeof OffscreenCanvas !== 'undefined'
    ? new OffscreenCanvas(width, height)
    : document.createElement('canvas');
  
  canvas.width = width;
  canvas.height = height;
  
  const ctx = canvas.getContext('2d');
  ctx.drawImage(img, 0, 0, width, height);
  
  const imageData = ctx.getImageData(0, 0, width, height);
  
  // 캔버스 및 이미지 리소스 즉시 해제
  URL.revokeObjectURL(img.src);
  
  return await compress(imageData, options);
}

WASM 모듈 캐싱

WebAssembly 모듈은 공격적인 캐싱의 이점을 얻습니다:

let cachedWasmModule: typeof wasmModule | null = null;

async function getWasmModule() {
  if (!cachedWasmModule) {
    cachedWasmModule = await import(
      '@fe-daily/libimagequant-wasm/wasm/libimagequant_wasm.js'
    );
  }
  return cachedWasmModule;
}

이렇게 하면 초기화 시간이 ~500ms에서 후속 압축에서 거의 즉시로 단축됩니다.

도구 모음 확장

Squoosh는 압축에 집중하지만 보완 도구의 전체 제품군을 구축했습니다:

모든 도구는 동일한 아키텍처 원칙을 공유합니다: 개인정보 보호 우선, WASM 기반, 완전한 클라이언트 측.

브라우저 확장 통합

웹 앱 아키텍처를 브라우저 확장으로 확장하여 다음을 가능하게 했습니다:

  • 도구 모음 팝업을 통한 즉시 액세스
  • 오른쪽 클릭 압축을 위한 컨텍스트 메뉴 통합
  • 탭 기반 상태 관리
  • 프리셋 기본 설정을 위한 로컬 저장소

확장은 동일한 Web Worker 및 WASM 인프라를 재사용하여 모든 플랫폼에서 일관된 동작을 보장합니다.

배포 및 인프라

Cloudflare를 사용한 엣지 컴퓨팅

OneImage Squoosh를 Cloudflare Pages에 배포하여 다음을 활용합니다:

  • 전 세계적으로 <50ms의 초기 로드 시간을 위한 글로벌 CDN
  • 자산을 위한 HTTP/3 및 Brotli 압축
  • WASM 모듈을 위한 스마트 캐싱 헤더
  • 콜드 스타트 없음(정적 자산만)

빌드 최적화

Next.js 구성에는 다음이 포함됩니다:

// next.config.ts
module.exports = {
  webpack: (config, { isServer }) => {
    // .wasm 파일 지원
    config.experiments = {
      asyncWebAssembly: true,
      layers: true,
    };
    
    // 워커 임포트 최적화
    config.module.rules.push({
      test: /\.worker\.(ts|js)$/,
      use: { loader: 'worker-loader' }
    });
    
    return config;
  },
  
  // 공격적인 코드 분할
  experimental: {
    optimizePackageImports: [
      '@jsquash/jpeg',
      '@jsquash/png',
      '@jsquash/webp',
      '@jsquash/avif'
    ]
  }
};

이렇게 하면 인코더가 온디맨드로 로드되어 초기 번들이 100KB(gzip) 미만으로 유지됩니다.

테스트 및 품질 보증

압축 로직에 대한 포괄적인 테스트 커버리지를 유지합니다:

// __tests__/advanced-compressor.test.ts
describe('AdvancedImageCompressor', () => {
  it('libimagequant로 PNG 압축해야 함', async () => {
    const compressor = new AdvancedImageCompressor();
    const mockFile = createMockImageFile('test.png', 1000, 1000);
    
    const result = await compressor.compress(mockFile, {
      encode: { type: 'png', options: { level: 5 } }
    });
    
    expect(result.format).toBe('png');
    expect(result.size).toBeLessThan(mockFile.size);
    expect(result.data).toBeInstanceOf(Uint8Array);
  });
  
  it('동시성으로 배치 처리 처리해야 함', async () => {
    const files = Array(10).fill(null).map((_, i) => 
      createMockImageFile(`test-${i}.png`, 500, 500)
    );
    
    const startTime = Date.now();
    await batchCompress(files, { preset: 'balanced' });
    const duration = Date.now() - startTime;
    
    // 순차 처리보다 빨라야 함
    expect(duration).toBeLessThan(10 * 1000); // <1초/이미지
  });
});

배운 교훈

  1. WASM은 프로덕션 준비가 되었음: 적절한 모듈 로딩 및 캐싱으로 WASM 성능은 네이티브 애플리케이션과 경쟁합니다
  2. Web Workers는 필수: CPU 집약적인 작업의 경우 워커로 오프로드하는 것은 협상할 수 없습니다
  3. 사용자 프리셋 > 원시 제어: 대부분의 사용자는 세밀한 조정보다 "좋은 기본값"을 선호합니다
  4. 메모리 중요: 큰 이미지에서 항상 메모리 사용량을 프로파일링하고 안전 장치를 구현하십시오
  5. 개인정보 보호 판매: "서버 업로드 없음"을 강조하는 것은 사용자에게 강하게 공감합니다

오픈 소스 및 커뮤니티

OneImage Squoosh는 상용 제품이지만 생태계에 기여합니다:

  • @jsquash 관리자에게 버그 보고서 및 PR
  • libimagequant-wasm에 대한 문서 개선
  • 성능 벤치마크 및 모범 사례 공유

결론

향상된 Squoosh를 구축하는 것은 libimagequant-wasm을 통합하는 것 이상을 요구했습니다. Web Workers, 메모리 관리, 사용자 경험 및 배포 인프라에 대한 신중한 아키텍처 결정이 필요했습니다. 결과는 전문가 수준의 압축 성능을 제공하면서 사용자 개인정보를 존중하는 도구입니다.

오늘 OneImage Squoosh를 사용해 보거나 더 빠른 액세스를 위해 브라우저 확장을 탐색하십시오. 유사한 도구를 구축하는 개발자의 경우 이 기술적 심층 분석이 유용한 청사진을 제공하기를 바랍니다.

---

참고 자료 및 추가 자료