⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

모델 최적화와 양자화 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 27. · 2 Views

모델 최적화와 양자화 완벽 가이드

웹 브라우저에서 AI 모델을 효율적으로 실행하기 위한 최적화 기법을 다룹니다. 양자화부터 WebGPU 가속, Web Worker 활용까지 초급 개발자도 쉽게 따라할 수 있도록 설명합니다.


목차

  1. 양자화_개념_이해
  2. ONNX_모델_형식
  3. 모델_크기_최적화
  4. WebGPU_가속
  5. Web_Worker_활용
  6. 메모리_관리

1. 양자화 개념 이해

김개발 씨는 회사에서 처음으로 AI 프로젝트를 맡게 되었습니다. 학습된 모델을 웹 서비스에 적용하려는데, 모델 파일 크기가 무려 500MB나 됩니다.

"이걸 사용자한테 다운로드 받으라고 해야 하나요?" 난감한 표정을 짓는 김개발 씨에게 박시니어 씨가 다가왔습니다. "양자화를 적용해 봤어요?"

**양자화(Quantization)**는 모델의 가중치를 더 작은 비트로 표현하는 기법입니다. 마치 고화질 사진을 적당한 품질로 압축하는 것과 같습니다.

32비트 부동소수점을 8비트 정수로 변환하면 모델 크기가 4분의 1로 줄어들면서도 정확도는 대부분 유지됩니다.

다음 코드를 살펴봅시다.

// 양자화 전후 메모리 사용량 비교 예제
const modelConfig = {
  // 원본 모델: 32비트 부동소수점 (FP32)
  originalPrecision: 'float32',
  originalSize: 500, // MB

  // 양자화 모델: 8비트 정수 (INT8)
  quantizedPrecision: 'int8',
  quantizedSize: 125, // MB (약 75% 감소)

  // 정확도 손실은 보통 1-2% 이내
  accuracyLoss: 0.015
};

// 양자화 타입 선택 함수
function selectQuantizationType(useCase) {
  if (useCase === 'mobile') return 'int8';  // 모바일: 최대 압축
  if (useCase === 'web') return 'fp16';     // 웹: 균형잡힌 선택
  return 'fp32';                             // 서버: 최대 정확도
}

김개발 씨는 입사 6개월 차 주니어 개발자입니다. 회사에서 이미지 분류 AI를 웹 서비스에 적용하라는 미션을 받았습니다.

열심히 준비한 모델을 테스트 서버에 올렸는데, 페이지 로딩 시간이 무려 30초나 걸렸습니다. 선배 개발자 박시니어 씨가 문제를 살펴봅니다.

"모델 파일이 500MB나 되네요. 양자화를 적용해서 크기를 줄여봐요." 그렇다면 양자화란 정확히 무엇일까요?

쉽게 비유하자면, 양자화는 마치 음악 파일을 WAV에서 MP3로 변환하는 것과 같습니다. WAV 파일은 원본 그대로의 고품질이지만 용량이 큽니다.

MP3로 변환하면 사람의 귀로는 거의 구분하기 어려운 수준으로 품질을 유지하면서도 파일 크기는 10분의 1로 줄어듭니다. 양자화도 마찬가지로 AI 모델의 핵심 성능은 유지하면서 크기만 획기적으로 줄입니다.

양자화가 없던 시절에는 어땠을까요? 개발자들은 거대한 모델 파일을 그대로 배포해야 했습니다.

사용자는 느린 로딩 속도에 지쳐 서비스를 떠났습니다. 특히 모바일 환경에서는 데이터 요금 문제까지 발생했습니다.

더 큰 문제는 메모리 부족이었습니다. 스마트폰이나 저사양 기기에서는 모델을 아예 실행할 수 없었습니다.

바로 이런 문제를 해결하기 위해 양자화가 등장했습니다. 양자화의 핵심 원리는 숫자의 정밀도를 낮추는 것입니다.

컴퓨터에서 숫자를 표현할 때, 32비트를 사용하면 매우 정밀한 값을 저장할 수 있습니다. 하지만 AI 모델에서는 그 정도의 정밀도가 항상 필요하지는 않습니다.

8비트만 사용해도 충분히 좋은 결과를 낼 수 있는 경우가 많습니다. 양자화에는 여러 종류가 있습니다.

동적 양자화는 모델을 실행할 때 실시간으로 변환합니다. 구현이 간단하지만 변환 오버헤드가 있습니다.

정적 양자화는 미리 대표 데이터로 변환 범위를 계산해둡니다. 더 정확하지만 추가 작업이 필요합니다.

양자화 인식 학습은 학습 단계부터 양자화를 고려합니다. 가장 정확하지만 시간이 오래 걸립니다.

위의 코드를 살펴보겠습니다. modelConfig 객체에서 원본 모델과 양자화된 모델의 차이를 비교합니다.

float32에서 int8로 변환하면 500MB가 125MB로 줄어듭니다. accuracyLoss가 0.015라는 것은 정확도가 1.5% 정도만 감소한다는 의미입니다.

selectQuantizationType 함수는 사용 환경에 따라 적절한 양자화 타입을 선택합니다. 모바일에서는 최대한 압축하고, 서버에서는 정확도를 우선시하는 방식입니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 쇼핑몰에서 상품 이미지를 자동 분류하는 AI를 만든다고 가정해봅시다.

관리자 페이지에서는 서버의 고성능 모델을 사용하고, 고객용 모바일 앱에서는 양자화된 경량 모델을 사용합니다. 같은 AI지만 환경에 맞게 최적화하는 것입니다.

하지만 주의할 점도 있습니다. 양자화를 너무 과도하게 적용하면 정확도가 크게 떨어질 수 있습니다.

특히 이미 작은 모델에 양자화를 적용하면 성능 저하가 심해집니다. 따라서 반드시 양자화 전후의 정확도를 비교 테스트해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언대로 INT8 양자화를 적용한 결과, 모델 크기가 125MB로 줄었습니다.

로딩 시간도 8초로 단축되었고, 정확도는 기존 대비 1.2%만 감소했습니다. "이 정도면 충분해요!" 김개발 씨가 환하게 웃었습니다.

실전 팁

💡 - 처음에는 FP16(반정밀도)부터 시도하세요. INT8보다 정확도 손실이 적습니다.

  • 양자화 후에는 반드시 핵심 기능의 정확도를 테스트하세요.
  • 모바일 배포 시에는 INT8, 웹 배포 시에는 FP16을 권장합니다.

2. ONNX 모델 형식

김개발 씨가 파이썬으로 학습한 PyTorch 모델을 웹에서 실행하려고 합니다. "JavaScript에서 PyTorch를 어떻게 실행하죠?" 당황한 김개발 씨에게 박시니어 씨가 말했습니다.

"ONNX로 변환하면 어디서든 실행할 수 있어요."

**ONNX(Open Neural Network Exchange)**는 AI 모델의 공용 파일 형식입니다. 마치 PDF가 어떤 프로그램에서든 문서를 열 수 있게 해주는 것처럼, ONNX는 어떤 프레임워크에서 만든 모델이든 동일하게 실행할 수 있게 해줍니다.

PyTorch, TensorFlow 등 다양한 프레임워크의 모델을 웹 브라우저에서 실행할 수 있습니다.

다음 코드를 살펴봅시다.

// ONNX Runtime Web을 사용한 모델 로딩 및 추론
import * as ort from 'onnxruntime-web';

async function runInference(imageData) {
  // ONNX 모델 세션 생성
  const session = await ort.InferenceSession.create(
    '/models/image-classifier.onnx',
    { executionProviders: ['webgl'] }  // WebGL 가속 사용
  );

  // 입력 텐서 생성 (배치, 채널, 높이, 너비)
  const inputTensor = new ort.Tensor(
    'float32',
    imageData,
    [1, 3, 224, 224]
  );

  // 추론 실행
  const results = await session.run({ input: inputTensor });

  // 결과 반환
  return results.output.data;
}

김개발 씨는 회사의 데이터 사이언스 팀에서 만든 이미지 분류 모델을 받았습니다. 파일을 열어보니 PyTorch로 학습된 .pt 파일이었습니다.

문제는 이 모델을 웹 브라우저에서 실행해야 한다는 것입니다. "JavaScript에서 PyTorch를 직접 실행할 수는 없잖아요?" 김개발 씨가 고민에 빠졌습니다.

박시니어 씨가 해결책을 제시합니다. "ONNX라는 공용 형식으로 변환하면 돼요.

브라우저에서도 실행할 수 있어요." ONNX란 정확히 무엇일까요? 쉽게 비유하자면, ONNX는 마치 번역가와 같습니다.

한국어로 쓴 책이 있다고 해봅시다. 일본 독자는 한국어를 모르니 읽을 수 없습니다.

하지만 이 책을 영어로 번역해두면, 영어를 아는 사람이라면 누구나 읽을 수 있습니다. ONNX는 AI 세계의 공용어입니다.

PyTorch든 TensorFlow든 ONNX로 변환하면 어디서든 실행할 수 있습니다. ONNX가 없던 시절에는 어땠을까요?

각 프레임워크마다 고유한 형식을 사용했습니다. PyTorch 모델은 PyTorch에서만, TensorFlow 모델은 TensorFlow에서만 실행할 수 있었습니다.

웹에서 AI를 실행하려면 모델을 처음부터 다시 구현해야 했습니다. 시간과 비용이 엄청나게 들었습니다.

바로 이런 문제를 해결하기 위해 마이크로소프트, 페이스북, AWS 등 주요 기업들이 힘을 합쳐 ONNX를 만들었습니다. ONNX 파일 안에는 무엇이 들어있을까요?

모델의 구조가중치가 담겨 있습니다. 구조는 신경망의 레이어가 어떻게 연결되어 있는지를 나타냅니다.

가중치는 학습을 통해 얻은 숫자들입니다. 이 두 가지가 있으면 어떤 환경에서든 동일한 추론을 수행할 수 있습니다.

위의 코드를 살펴보겠습니다. 먼저 onnxruntime-web 라이브러리를 가져옵니다.

이 라이브러리가 브라우저에서 ONNX 모델을 실행해주는 핵심 엔진입니다. InferenceSession.create로 모델을 로딩할 때 executionProviders 옵션을 통해 WebGL 가속을 사용합니다.

입력 데이터는 Tensor 객체로 감싸야 합니다. 이미지 분류 모델이므로 [1, 3, 224, 224] 형태로 만듭니다.

이는 배치 1개, RGB 3채널, 224x224 해상도를 의미합니다. session.run을 호출하면 실제 추론이 실행됩니다.

결과는 분류 확률 배열로 반환됩니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 고객 서비스 챗봇을 만든다고 가정해봅시다. 데이터 사이언스 팀에서 PyTorch로 감정 분석 모델을 학습합니다.

이 모델을 ONNX로 변환하면 웹 개발팀은 JavaScript만으로 브라우저에서 실시간 감정 분석을 구현할 수 있습니다. 서버 호출 없이 사용자의 메시지 감정을 즉시 파악할 수 있습니다.

하지만 주의할 점도 있습니다. 모든 PyTorch 연산이 ONNX로 완벽하게 변환되는 것은 아닙니다.

특수한 커스텀 레이어가 있다면 변환이 실패할 수 있습니다. 따라서 변환 후에는 반드시 원본 모델과 동일한 결과가 나오는지 검증해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 데이터 사이언스 팀에 ONNX 변환을 요청했더니, 하루 만에 변환된 파일을 받았습니다.

코드 몇 줄만으로 웹 브라우저에서 이미지 분류가 가능해졌습니다. "프레임워크가 달라도 문제없네요!" 김개발 씨가 감탄했습니다.

실전 팁

💡 - ONNX 변환 시 opset 버전을 명시하세요. 버전에 따라 지원되는 연산이 다릅니다.

  • 변환 후 반드시 원본 모델과 출력값을 비교 검증하세요.
  • onnxruntime-web의 executionProviders는 webgl, wasm, webgpu 순서로 시도합니다.

3. 모델 크기 최적화

김개발 씨가 웹 서비스의 성능 리포트를 보다가 깜짝 놀랐습니다. AI 모델 로딩 때문에 초기 페이지 로딩이 10초나 걸리고 있었습니다.

"모델 파일이 너무 크네요. 더 줄일 방법이 없을까요?" 박시니어 씨가 다양한 최적화 기법을 알려주기 시작했습니다.

모델 크기 최적화는 양자화 외에도 다양한 기법으로 모델을 경량화하는 것입니다. 마치 이사 갈 때 불필요한 짐을 정리하고 꼭 필요한 것만 효율적으로 포장하는 것과 같습니다.

프루닝, 지식 증류, 레이어 퓨전 등의 기법을 조합하면 모델 크기를 획기적으로 줄일 수 있습니다.

다음 코드를 살펴봅시다.

// 모델 최적화 설정 및 로딩 전략
const optimizationConfig = {
  // 프루닝: 중요도 낮은 연결 제거
  pruning: {
    enabled: true,
    sparsity: 0.5  // 50%의 연결 제거
  },

  // 그래프 최적화: 연산 병합
  graphOptimization: {
    level: 'all',  // 'basic', 'extended', 'all'
    enableFusion: true  // 레이어 퓨전 활성화
  }
};

// 최적화된 세션 생성
async function createOptimizedSession(modelPath) {
  const options = {
    executionProviders: ['webgl'],
    graphOptimizationLevel: 'all',
    enableCpuMemArena: true,  // 메모리 풀 사용
    enableMemPattern: true     // 메모리 패턴 최적화
  };

  return await ort.InferenceSession.create(modelPath, options);
}

김개발 씨는 양자화를 적용해서 모델 크기를 상당히 줄였습니다. 하지만 여전히 사용자 경험에는 아쉬움이 남았습니다.

모바일 사용자들의 이탈률이 높았습니다. "양자화 말고 다른 최적화 방법은 없나요?" 김개발 씨가 물었습니다.

박시니어 씨가 화이트보드에 그림을 그리기 시작합니다. "모델 최적화에는 여러 가지 기법이 있어요." **프루닝(Pruning)**이란 무엇일까요?

가지치기라고도 불리는 프루닝은 마치 정원사가 나무의 불필요한 가지를 쳐내는 것과 같습니다. 신경망에서 중요도가 낮은 연결을 제거합니다.

가중치가 0에 가까운 연결은 결과에 거의 영향을 주지 않습니다. 이런 연결을 제거하면 모델이 가벼워지면서도 성능은 유지됩니다.

**레이어 퓨전(Layer Fusion)**은 또 다른 강력한 기법입니다. 여러 개의 작은 연산을 하나의 큰 연산으로 합치는 것입니다.

예를 들어 합성곱, 배치 정규화, 활성화 함수를 각각 실행하면 3번의 메모리 접근이 필요합니다. 하지만 이 세 연산을 하나로 합치면 메모리 접근이 1번으로 줄어듭니다.

연산 속도가 크게 향상됩니다. **지식 증류(Knowledge Distillation)**는 조금 다른 접근입니다.

큰 모델(선생님)의 지식을 작은 모델(학생)에게 전달하는 기법입니다. 마치 베테랑 선배가 핵심 노하우만 후배에게 전수하는 것과 같습니다.

학생 모델은 크기가 작지만 선생님의 핵심 능력을 물려받습니다. 위의 코드를 살펴보겠습니다.

optimizationConfig에서 프루닝 설정을 볼 수 있습니다. sparsity가 0.5라는 것은 전체 연결의 50%를 제거한다는 의미입니다.

graphOptimization의 enableFusion이 true이면 레이어 퓨전이 적용됩니다. createOptimizedSession 함수에서는 ONNX Runtime의 세션 옵션을 설정합니다.

graphOptimizationLevel을 'all'로 설정하면 가능한 모든 그래프 최적화가 적용됩니다. enableCpuMemArena와 enableMemPattern은 메모리 사용을 최적화합니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 실시간 얼굴 필터 앱을 만든다고 가정해봅시다.

초당 30프레임을 처리해야 하므로 모델이 매우 가벼워야 합니다. 프루닝으로 50%의 연결을 제거하고, 레이어 퓨전으로 연산을 병합하고, INT8 양자화까지 적용합니다.

이 세 가지를 조합하면 원본 대비 10배 이상 가벼운 모델을 만들 수 있습니다. 하지만 주의할 점도 있습니다.

최적화를 과도하게 적용하면 정확도가 급격히 떨어질 수 있습니다. 특히 프루닝은 잘못 적용하면 모델의 핵심 기능이 손상됩니다.

따라서 조금씩 최적화를 적용하면서 정확도를 모니터링해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

여러 최적화 기법을 조합한 결과, 모델 크기가 원본의 8분의 1로 줄었습니다. 로딩 시간은 2초로 단축되었고, 모바일 사용자 이탈률도 크게 감소했습니다.

실전 팁

💡 - 최적화는 한 번에 하나씩 적용하고 효과를 측정하세요.

  • 프루닝 비율은 30-50%부터 시작해서 점진적으로 높이세요.
  • graphOptimizationLevel은 'all'이 가장 공격적인 최적화입니다.

4. WebGPU 가속

김개발 씨가 최적화한 모델이 데스크톱에서는 빠르게 동작했습니다. 그런데 노트북에서 테스트하니 눈에 띄게 느렸습니다.

"같은 모델인데 왜 속도 차이가 이렇게 나죠?" 박시니어 씨가 설명했습니다. "GPU 가속을 활용하지 않아서 그래요.

WebGPU를 적용해 봅시다."

WebGPU는 웹 브라우저에서 GPU의 연산 능력을 활용할 수 있게 해주는 최신 API입니다. 마치 일반 도로 대신 고속도로를 이용하는 것과 같습니다.

CPU로 순차적으로 처리하던 연산을 GPU에서 병렬로 처리하면 AI 추론 속도가 수십 배 빨라질 수 있습니다.

다음 코드를 살펴봅시다.

// WebGPU 지원 확인 및 최적 실행 환경 설정
async function setupExecutionProvider() {
  // WebGPU 지원 여부 확인
  if ('gpu' in navigator) {
    const adapter = await navigator.gpu.requestAdapter();
    if (adapter) {
      console.log('WebGPU 사용 가능');
      return ['webgpu', 'wasm'];  // WebGPU 우선, WASM 폴백
    }
  }

  // WebGL 폴백 확인
  const canvas = document.createElement('canvas');
  const gl = canvas.getContext('webgl2');
  if (gl) {
    console.log('WebGL2 사용 가능');
    return ['webgl', 'wasm'];
  }

  // 최종 폴백: WebAssembly
  console.log('WASM으로 폴백');
  return ['wasm'];
}

// 최적 환경으로 세션 생성
async function createAcceleratedSession(modelPath) {
  const providers = await setupExecutionProvider();
  return await ort.InferenceSession.create(modelPath, {
    executionProviders: providers
  });
}

김개발 씨는 모델 최적화에 성공했지만, 새로운 문제에 부딪혔습니다. 고성능 데스크톱에서는 빠르게 동작하는 모델이 일반 노트북에서는 버벅거렸습니다.

사용자 환경은 천차만별인데, 모든 환경에서 좋은 성능을 내려면 어떻게 해야 할까요? "GPU 가속을 활용해야 해요." 박시니어 씨가 말했습니다.

"WebGPU라는 새로운 기술이 있어요." GPU란 무엇이고, 왜 AI에 중요할까요? CPU는 복잡한 작업을 순서대로 빠르게 처리하는 데 특화되어 있습니다.

반면 GPU는 간단한 작업을 동시에 대량으로 처리하는 데 특화되어 있습니다. 마치 CPU가 천재 한 명이라면, GPU는 평범한 사람 수천 명이 협력하는 것과 같습니다.

AI 연산은 대부분 행렬 곱셈입니다. 행렬 곱셈은 많은 숫자를 동시에 곱하고 더하는 작업입니다.

이런 작업은 GPU가 CPU보다 수십, 수백 배 빠르게 처리할 수 있습니다. WebGPU는 이런 GPU의 힘을 웹 브라우저에서 활용할 수 있게 해줍니다.

예전에는 WebGL이라는 기술을 사용했습니다. WebGL은 원래 3D 그래픽을 위한 것이어서 AI 연산에는 제약이 많았습니다.

WebGPU는 처음부터 범용 연산을 고려해서 설계되었습니다. AI뿐만 아니라 과학 계산, 시뮬레이션 등 다양한 분야에서 활용할 수 있습니다.

위의 코드를 살펴보겠습니다. setupExecutionProvider 함수는 사용 가능한 가장 빠른 실행 환경을 선택합니다.

먼저 navigator.gpu가 있는지 확인합니다. 이것이 WebGPU 지원 여부를 알려줍니다.

requestAdapter로 실제 GPU 어댑터를 요청해봅니다. WebGPU를 사용할 수 없다면 WebGL로 폴백합니다.

WebGL도 없다면 최종적으로 WebAssembly를 사용합니다. WebAssembly는 CPU에서 실행되지만, 일반 JavaScript보다는 훨씬 빠릅니다.

executionProviders 배열의 순서가 중요합니다. 앞에 있는 것부터 시도하고, 실패하면 다음 것을 시도합니다.

따라서 가장 빠른 WebGPU를 맨 앞에 두었습니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 실시간 영상 스타일 변환 서비스를 만든다고 가정해봅시다. 사용자의 웹캠 영상을 유명 화가의 화풍으로 바꿔주는 서비스입니다.

CPU만으로는 초당 2-3프레임밖에 처리하지 못합니다. WebGPU를 활용하면 초당 30프레임 이상 처리할 수 있어서 실시간 서비스가 가능해집니다.

하지만 주의할 점도 있습니다. WebGPU는 아직 모든 브라우저에서 지원되지 않습니다.

2024년 기준으로 크롬과 엣지에서는 지원하지만, 파이어폭스와 사파리는 아직 개발 중입니다. 따라서 반드시 폴백 로직을 구현해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. WebGPU 가속을 적용한 결과, 고성능 GPU가 있는 기기에서는 추론 속도가 10배 빨라졌습니다.

GPU가 없는 기기에서도 WebAssembly 덕분에 안정적으로 동작했습니다.

실전 팁

💡 - WebGPU 지원 여부는 반드시 런타임에 확인하고 폴백을 준비하세요.

  • executionProviders 순서는 빠른 것부터 느린 것 순으로 배치하세요.
  • WebGPU는 초기 셋업 시간이 있으므로 세션을 재사용하세요.

5. Web Worker 활용

김개발 씨가 AI 기능을 웹페이지에 적용했더니 이상한 문제가 생겼습니다. 모델이 추론하는 동안 페이지 전체가 멈춰버렸습니다.

버튼도 안 눌리고, 스크롤도 안 됩니다. "왜 페이지가 얼어버리죠?" 박시니어 씨가 답했습니다.

"메인 스레드를 막고 있어서 그래요. Web Worker를 써봐요."

Web Worker는 브라우저에서 백그라운드 스레드를 만들어주는 기능입니다. 마치 주방에서 요리사가 음식을 만드는 동안 서빙 직원이 손님을 응대하는 것과 같습니다.

무거운 AI 연산을 별도 스레드에서 처리하면 메인 화면은 항상 부드럽게 반응합니다.

다음 코드를 살펴봅시다.

// worker.js - AI 추론 전용 워커
importScripts('https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js');

let session = null;

// 모델 로딩 (한 번만 실행)
async function loadModel(modelPath) {
  session = await ort.InferenceSession.create(modelPath, {
    executionProviders: ['wasm']
  });
  self.postMessage({ type: 'ready' });
}

// 추론 실행
async function runInference(inputData) {
  const tensor = new ort.Tensor('float32', inputData, [1, 3, 224, 224]);
  const results = await session.run({ input: tensor });
  self.postMessage({ type: 'result', data: Array.from(results.output.data) });
}

// 메시지 수신 처리
self.onmessage = async (e) => {
  if (e.data.type === 'load') await loadModel(e.data.modelPath);
  if (e.data.type === 'infer') await runInference(e.data.input);
};

김개발 씨는 이미지 분류 AI를 웹페이지에 성공적으로 적용했습니다. 기능은 잘 동작했지만, 사용자 경험에 심각한 문제가 있었습니다.

AI가 이미지를 분석하는 몇 초 동안 페이지 전체가 멈춰버렸습니다. "분석 버튼을 누르면 화면이 얼어요.

로딩 애니메이션도 안 돌아가요." 김개발 씨가 보고했습니다. 박시니어 씨가 원인을 설명합니다.

"브라우저의 메인 스레드가 막혔기 때문이에요." 브라우저에서 JavaScript는 기본적으로 싱글 스레드로 실행됩니다. 화면 렌더링, 사용자 입력 처리, 애니메이션, 그리고 우리의 AI 연산까지 모두 하나의 스레드에서 순서대로 처리됩니다.

문제는 AI 연산이 너무 오래 걸린다는 것입니다. AI가 계산하는 동안 다른 모든 작업이 대기해야 합니다.

사용자가 버튼을 클릭해도 반응이 없고, 애니메이션도 멈추고, 스크롤도 안 됩니다. 마치 고속도로에서 대형 트럭이 모든 차선을 막고 천천히 가는 것과 같습니다.

Web Worker가 바로 이 문제를 해결합니다. Web Worker는 별도의 백그라운드 스레드를 만듭니다.

마치 고속도로 옆에 화물 전용 도로를 만드는 것과 같습니다. 무거운 화물차는 전용 도로로, 일반 차량은 기존 도로로 다닙니다.

서로 방해하지 않습니다. 위의 코드를 살펴보겠습니다.

worker.js 파일은 Web Worker에서 실행될 코드입니다. importScripts로 ONNX Runtime 라이브러리를 로드합니다.

loadModel 함수는 모델을 한 번만 로드하고 세션을 유지합니다. runInference 함수는 실제 추론을 수행합니다.

self.postMessage로 메인 스레드에 결과를 전송합니다. self.onmessage로 메인 스레드에서 오는 명령을 받습니다.

이렇게 메인 스레드와 워커는 메시지로 통신합니다. 메인 스레드에서는 어떻게 사용할까요?

javascript const worker = new Worker('worker.js'); worker.postMessage({ type: 'load', modelPath: '/model.onnx' }); worker.onmessage = (e) => { if (e.data.type === 'result') displayResults(e.data.data); }; 이렇게 하면 AI 연산이 백그라운드에서 실행되는 동안에도 메인 화면은 부드럽게 반응합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 문서 OCR 서비스를 만든다고 가정해봅시다. 사용자가 문서 사진을 업로드하면 AI가 텍스트를 추출합니다.

이 과정이 5-10초 걸릴 수 있습니다. Web Worker를 사용하면 추출하는 동안에도 사용자는 다른 문서를 업로드하거나 UI를 조작할 수 있습니다.

하지만 주의할 점도 있습니다. Web Worker에서는 DOM에 직접 접근할 수 없습니다.

화면을 직접 업데이트할 수 없고, 반드시 메시지를 통해 메인 스레드에 요청해야 합니다. 또한 데이터를 주고받을 때 복사가 일어나므로, 큰 데이터는 Transferable ObjectsSharedArrayBuffer를 사용하는 것이 좋습니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. Web Worker를 적용한 후, AI가 분석하는 동안에도 로딩 애니메이션이 부드럽게 돌아갔습니다.

사용자들의 불만이 사라졌습니다.

실전 팁

💡 - 모델 로딩은 시간이 오래 걸리므로 페이지 로드 시 미리 해두세요.

  • 큰 배열을 전송할 때는 Transferable Objects를 사용하세요.
  • 워커에서 발생하는 에러도 반드시 처리하세요 (worker.onerror).

6. 메모리 관리

김개발 씨의 AI 서비스가 잘 운영되고 있었습니다. 그런데 사용자들로부터 이상한 리포트가 들어오기 시작했습니다.

"페이지를 오래 사용하면 점점 느려져요." 개발자 도구를 열어보니 메모리 사용량이 계속 증가하고 있었습니다. 메모리 누수가 발생하고 있었던 것입니다.

메모리 관리는 AI 모델을 사용할 때 반드시 신경 써야 하는 부분입니다. 마치 호텔에서 체크아웃할 때 방을 비워주는 것처럼, 사용이 끝난 텐서와 세션은 명시적으로 해제해야 합니다.

그렇지 않으면 메모리가 계속 쌓여서 브라우저가 느려지거나 충돌할 수 있습니다.

다음 코드를 살펴봅시다.

// 메모리 관리가 적용된 추론 클래스
class InferenceManager {
  constructor() {
    this.session = null;
    this.tensors = [];  // 생성된 텐서 추적
  }

  async initialize(modelPath) {
    this.session = await ort.InferenceSession.create(modelPath);
  }

  async predict(inputData) {
    // 입력 텐서 생성 및 추적
    const inputTensor = new ort.Tensor('float32', inputData, [1, 3, 224, 224]);
    this.tensors.push(inputTensor);

    const results = await this.session.run({ input: inputTensor });

    // 사용 완료된 텐서 해제
    this.clearTensors();

    return results.output.data;
  }

  clearTensors() {
    // 모든 추적된 텐서 메모리 해제
    this.tensors.forEach(tensor => tensor.dispose());
    this.tensors = [];
  }

  async dispose() {
    this.clearTensors();
    if (this.session) {
      await this.session.release();  // 세션 메모리 해제
      this.session = null;
    }
  }
}

김개발 씨의 AI 서비스가 출시된 지 한 달이 지났습니다. 대부분의 사용자는 만족했지만, 일부 사용자가 불만을 제기했습니다.

"처음에는 빠른데, 10분쯤 사용하면 점점 느려져요." 김개발 씨가 개발자 도구의 메모리 탭을 열어봤습니다. 페이지를 사용할수록 메모리 사용량이 계속 증가하고 있었습니다.

한 번도 내려가지 않았습니다. "메모리 누수네요." 박시니어 씨가 진단했습니다.

메모리 누수란 무엇일까요? 마치 수도꼭지를 틀어놓고 잠그지 않는 것과 같습니다.

물을 조금씩 계속 사용하면 언젠가는 물탱크가 바닥납니다. 마찬가지로 메모리를 사용하고 해제하지 않으면 언젠가는 브라우저가 버텨내지 못합니다.

JavaScript는 가비지 컬렉션이라는 자동 메모리 관리 기능이 있습니다. 사용하지 않는 객체를 자동으로 찾아서 메모리를 해제합니다.

그런데 왜 메모리 누수가 발생했을까요? 문제는 ONNX Runtime의 텐서입니다.

텐서는 GPU 메모리나 WebAssembly 메모리를 사용합니다. 이런 메모리는 JavaScript의 가비지 컬렉터가 관리하지 않습니다.

따라서 개발자가 명시적으로 해제해야 합니다. tensor.dispose()를 호출하지 않으면 메모리가 계속 쌓입니다.

위의 코드를 살펴보겠습니다. InferenceManager 클래스는 메모리 관리를 체계적으로 처리합니다.

tensors 배열로 생성한 모든 텐서를 추적합니다. predict 메서드에서 추론이 끝나면 clearTensors를 호출해서 사용한 텐서를 해제합니다.

dispose 메서드는 매니저 전체를 정리합니다. 모든 텐서를 해제하고, 세션도 release합니다.

페이지를 떠날 때나 AI 기능을 더 이상 사용하지 않을 때 반드시 호출해야 합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 SPA(Single Page Application)에서 AI 기능이 특정 페이지에만 있다고 가정해봅시다. 사용자가 해당 페이지를 떠날 때 반드시 dispose를 호출해야 합니다.

React라면 useEffect의 클린업 함수에서, Vue라면 onUnmounted에서 처리합니다. javascript useEffect(() => { const manager = new InferenceManager(); manager.initialize('/model.onnx'); return () => { manager.dispose(); // 컴포넌트 언마운트 시 정리 }; }, []); 하지만 주의할 점도 있습니다.

dispose를 너무 자주 호출하면 오히려 성능이 떨어집니다. 세션을 다시 생성하는 데 시간이 걸리기 때문입니다.

따라서 세션은 가능한 한 오래 유지하고, 텐서만 매번 정리하는 것이 좋습니다. 메모리 사용량을 모니터링하는 것도 중요합니다.

javascript if (performance.memory) { console.log('Used:', performance.memory.usedJSHeapSize / 1024 / 1024, 'MB'); } 크롬 브라우저에서는 performance.memory로 메모리 사용량을 확인할 수 있습니다. 주기적으로 체크해서 비정상적인 증가가 없는지 확인하세요.

다시 김개발 씨의 이야기로 돌아가 봅시다. InferenceManager 클래스를 적용하고 모든 텐서를 추적해서 해제했습니다.

메모리 사용량이 일정 수준에서 유지되기 시작했습니다. 장시간 사용해도 느려지지 않는 안정적인 서비스가 완성되었습니다.

실전 팁

💡 - 텐서를 생성할 때마다 배열에 추적하고, 사용 후 dispose하세요.

  • 페이지 이동이나 컴포넌트 언마운트 시 반드시 세션을 release하세요.
  • 개발 중에는 메모리 탭을 열어두고 누수 여부를 주기적으로 확인하세요.

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#JavaScript#Quantization#ONNX#WebGPU#WebWorker#MemoryManagement#AI,ML,JavaScript

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.