Skip to content

성능 최적화

Sharp는 이미 고성능 이미지 처리 라이브러리이지만, 일부 최적화 기술을 통해 성능을 더욱 향상시킬 수 있습니다.

벤치마크

Sharp가 다른 이미지 처리 라이브러리와 비교한 성능 우위:

  • ImageMagick보다 4-5배 빠름
  • GraphicsMagick보다 4-5배 빠름
  • 메모리 사용량이 더 낮음
  • 스트림 처리 지원

메모리 최적화

스트림 처리 사용

대용량 파일의 경우 스트림 처리를 사용하면 메모리 사용량을 크게 줄일 수 있습니다:

javascript
import fs from 'fs';

// ❌ 권장하지 않음: 전체 파일을 메모리에 로드
const buffer = fs.readFileSync('large-image.jpg');
await sharp(buffer).resize(800, 600).toFile('output.jpg');

// ✅ 권장: 스트림 처리 사용
fs.createReadStream('large-image.jpg')
  .pipe(sharp().resize(800, 600).jpeg())
  .pipe(fs.createWriteStream('output.jpg'));

청크 처리

초대용량 파일의 경우 청크 단위로 처리할 수 있습니다:

javascript
import fs from 'fs';

async function processLargeFile(inputPath, outputPath, chunkSize = 1024 * 1024) {
  const pipeline = sharp()
    .resize(800, 600)
    .jpeg({ quality: 80 });

  return new Promise((resolve, reject) => {
    const readStream = fs.createReadStream(inputPath, { highWaterMark: chunkSize });
    const writeStream = fs.createWriteStream(outputPath);

    readStream
      .pipe(pipeline)
      .pipe(writeStream)
      .on('finish', resolve)
      .on('error', reject);
  });
}

await processLargeFile('large-input.jpg', 'output.jpg');

메모리 즉시 해제

javascript
// 처리 완료 후 즉시 정리
async function processImage(inputPath, outputPath) {
  const sharpInstance = sharp(inputPath);
  
  try {
    await sharpInstance
      .resize(800, 600)
      .jpeg({ quality: 80 })
      .toFile(outputPath);
  } finally {
    // 수동 정리 (Node.js가 자동으로 가비지 수집하지만)
    sharpInstance.destroy();
  }
}

동시성 최적화

동시성 수 제어

javascript
async function processWithConcurrency(files, concurrency = 3) {
  const results = [];
  
  for (let i = 0; i < files.length; i += concurrency) {
    const batch = files.slice(i, i + concurrency);
    const batchPromises = batch.map(file => 
      sharp(file)
        .resize(300, 200)
        .jpeg({ quality: 80 })
        .toFile(`processed-${file}`)
    );
    
    const batchResults = await Promise.all(batchPromises);
    results.push(...batchResults);
  }
  
  return results;
}

const files = ['file1.jpg', 'file2.jpg', 'file3.jpg', 'file4.jpg'];
await processWithConcurrency(files, 2);

Worker 스레드 사용

CPU 집약적인 작업의 경우 Worker 스레드를 사용할 수 있습니다:

javascript
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';

if (isMainThread) {
  // 메인 스레드
  async function processWithWorkers(files, numWorkers = 4) {
    const workers = [];
    const results = [];
    
    for (let i = 0; i < numWorkers; i++) {
      const worker = new Worker('./image-worker.js', {
        workerData: { files: files.slice(i * Math.ceil(files.length / numWorkers), (i + 1) * Math.ceil(files.length / numWorkers)) }
      });
      
      worker.on('message', (result) => {
        results.push(result);
      });
      
      workers.push(worker);
    }
    
    await Promise.all(workers.map(worker => new Promise(resolve => worker.on('exit', resolve)));
    return results;
  }
  
  const files = ['file1.jpg', 'file2.jpg', 'file3.jpg'];
  await processWithWorkers(files);
} else {
  // Worker 스레드
  const { files } = workerData;
  
  for (const file of files) {
    await sharp(file)
      .resize(300, 200)
      .jpeg({ quality: 80 })
      .toFile(`processed-${file}`);
  }
  
  parentPort.postMessage('done');
}

캐시 최적화

처리 결과 캐싱

javascript
import crypto from 'crypto';
import fs from 'fs';

class ImageCache {
  constructor(cacheDir = './cache') {
    this.cacheDir = cacheDir;
    if (!fs.existsSync(cacheDir)) {
      fs.mkdirSync(cacheDir, { recursive: true });
    }
  }

  generateCacheKey(inputPath, options) {
    const content = JSON.stringify({ inputPath, options });
    return crypto.createHash('md5').update(content).digest('hex');
  }

  async getCachedResult(cacheKey) {
    const cachePath = `${this.cacheDir}/${cacheKey}.jpg`;
    if (fs.existsSync(cachePath)) {
      return cachePath;
    }
    return null;
  }

  async setCachedResult(cacheKey, resultPath) {
    const cachePath = `${this.cacheDir}/${cacheKey}.jpg`;
    fs.copyFileSync(resultPath, cachePath);
  }

  async processImage(inputPath, options) {
    const cacheKey = this.generateCacheKey(inputPath, options);
    const cached = await this.getCachedResult(cacheKey);
    
    if (cached) {
      console.log('캐시 결과 사용');
      return cached;
    }

    const outputPath = `output-${Date.now()}.jpg`;
    await sharp(inputPath)
      .resize(options.width, options.height)
      .jpeg({ quality: options.quality })
      .toFile(outputPath);

    await this.setCachedResult(cacheKey, outputPath);
    return outputPath;
  }
}

const cache = new ImageCache();
await cache.processImage('input.jpg', { width: 300, height: 200, quality: 80 });

알고리즘 최적화

적절한 크기 조정 알고리즘 선택

javascript
// 사진의 경우 lanczos3 커널 사용
await sharp('photo.jpg')
  .resize(800, 600, { kernel: sharp.kernel.lanczos3 })
  .toFile('photo-resized.jpg');

// 아이콘이나 선 그래픽의 경우 nearest 커널 사용
await sharp('icon.png')
  .resize(32, 32, { kernel: sharp.kernel.nearest })
  .toFile('icon-resized.png');

// 빠른 처리가 필요한 이미지의 경우 cubic 커널 사용
await sharp('image.jpg')
  .resize(300, 200, { kernel: sharp.kernel.cubic })
  .toFile('image-resized.jpg');

JPEG 품질 최적화

javascript
// 이미지 내용에 따라 품질 조정
async function optimizeJPEGQuality(inputPath, outputPath) {
  const metadata = await sharp(inputPath).metadata();
  
  // 이미지 크기에 따라 품질 조정
  let quality = 80;
  if (metadata.width > 1920 || metadata.height > 1080) {
    quality = 85; // 큰 이미지는 더 높은 품질 사용
  } else if (metadata.width < 800 && metadata.height < 600) {
    quality = 75; // 작은 이미지는 낮은 품질 사용 가능
  }
  
  await sharp(inputPath)
    .jpeg({ 
      quality,
      progressive: true, // 프로그레시브 JPEG
      mozjpeg: true      // mozjpeg 최적화 사용
    })
    .toFile(outputPath);
}

네트워크 최적화

다양한 크기 사전 생성

javascript
const sizes = [
  { width: 320, suffix: 'sm' },
  { width: 640, suffix: 'md' },
  { width: 1024, suffix: 'lg' },
  { width: 1920, suffix: 'xl' }
];

async function pregenerateSizes(inputPath) {
  const promises = sizes.map(size => 
    sharp(inputPath)
      .resize(size.width, null, { fit: 'inside' })
      .jpeg({ quality: 80 })
      .toFile(`output-${size.suffix}.jpg`)
  );
  
  await Promise.all(promises);
}

await pregenerateSizes('input.jpg');

현대적 형식 사용

javascript
// 다양한 브라우저 지원을 위해 여러 형식 생성
async function generateModernFormats(inputPath) {
  const promises = [
    // JPEG를 대체로 사용
    sharp(inputPath)
      .resize(800, 600)
      .jpeg({ quality: 80 })
      .toFile('output.jpg'),
    
    // WebP를 현대 브라우저용으로 사용
    sharp(inputPath)
      .resize(800, 600)
      .webp({ quality: 80 })
      .toFile('output.webp'),
    
    // AVIF를 최신 브라우저용으로 사용
    sharp(inputPath)
      .resize(800, 600)
      .avif({ quality: 80 })
      .toFile('output.avif')
  ];
  
  await Promise.all(promises);
}

모니터링 및 디버깅

성능 모니터링

javascript
import { performance } from 'perf_hooks';

async function measurePerformance(fn) {
  const start = performance.now();
  const result = await fn();
  const end = performance.now();
  
  console.log(`실행 시간: ${end - start}ms`);
  return result;
}

await measurePerformance(async () => {
  await sharp('input.jpg')
    .resize(800, 600)
    .jpeg({ quality: 80 })
    .toFile('output.jpg');
});

메모리 사용 모니터링

javascript
import { performance } from 'perf_hooks';

function getMemoryUsage() {
  const usage = process.memoryUsage();
  return {
    rss: `${Math.round(usage.rss / 1024 / 1024)}MB`,
    heapTotal: `${Math.round(usage.heapTotal / 1024 / 1024)}MB`,
    heapUsed: `${Math.round(usage.heapUsed / 1024 / 1024)}MB`,
    external: `${Math.round(usage.external / 1024 / 1024)}MB`
  };
}

console.log('처리 전 메모리 사용:', getMemoryUsage());

await sharp('input.jpg')
  .resize(800, 600)
  .jpeg({ quality: 80 })
  .toFile('output.jpg');

console.log('처리 후 메모리 사용:', getMemoryUsage());

모범 사례 요약

  1. 대용량 파일에 스트림 처리 사용
  2. 동시성 수 제어
  3. 처리 결과 캐싱
  4. 적절한 크기 조정 알고리즘 선택
  5. 출력 형식 및 품질 최적화
  6. 일반적인 크기 사전 생성
  7. 성능 지표 모니터링

성능 비교

작업SharpImageMagickGraphicsMagick
크기 조정100ms450ms420ms
형식 변환80ms380ms360ms
필터 적용120ms520ms480ms
메모리 사용낮음높음높음

다음 단계

  • API 문서를 확인하여 사용 가능한 모든 메서드 알아보기
  • 예제를 학습하여 더 많은 사용법 알아보기
  • 변경 로그를 확인하여 최신 기능 알아보기

Apache 2.0 라이선스에 따라 릴리스되었습니다.