모바일 브라우저에서 비디오 처리는 메모리와 성능의 한계로 인해 까다로운 작업입니다. 특히 안드로이드 크롬에서는 8분 이상의 비디오 처리 시 메모리 부족으로 브라우저가 크래시되는 경우가 빈번합니다. ffmpeg.wasm을 활용하면 클라이언트 측에서 비디오 트랜스코딩이 가능하지만, 모바일 환경의 제약사항을 신중히 고려해야 합니다. 이번 글에서는 실제 프로젝트에서 겪은 문제점들과 해결 방법을 공유하겠습니다.
1. ffmpeg.wasm 설치와 기본 설정
ffmpeg.wasm은 WebAssembly로 컴파일된 FFmpeg을 브라우저에서 실행할 수 있게 해주는 라이브러리입니다. 모바일 환경에서는 코어 버전을 사용하여 메모리 사용량을 최소화하는 것이 중요합니다. SharedArrayBuffer를 지원하지 않는 환경을 고려한 설정도 필요합니다.
npm install @ffmpeg/ffmpeg @ffmpeg/core
// 모바일 최적화 설정
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { toBlobURL } from '@ffmpeg/util';
const initFFmpeg = async () => {
const ffmpeg = new FFmpeg();
await ffmpeg.load({
coreURL: await toBlobURL('/ffmpeg-core.js', 'text/javascript'),
wasmURL: await toBlobURL('/ffmpeg-core.wasm', 'application/wasm'),
workerURL: await toBlobURL('/ffmpeg-worker.js', 'text/javascript'),
});
return ffmpeg;
};
CDN을 통한 로딩보다는 로컬 파일을 사용하는 것이 안정성 면에서 유리합니다. 또한 Service Worker를 활용해 FFmpeg 코어 파일들을 캐싱하면 재사용 시 로딩 시간을 단축할 수 있습니다.
2. 모바일 메모리 제한 대응
안드로이드 크롬에서는 약 2GB의 메모리 제한이 있어 대용량 비디오 처리가 어렵습니다. 비디오 길이와 해상도를 체크하여 처리 가능 여부를 미리 판단하고, 필요시 분할 처리하는 전략이 필요합니다. 8분 이상의 비디오는 세그먼트 단위로 나누어 처리하는 것이 안전합니다.
// 모바일 메모리 제한 체크
const checkVideoProcessable = (file, duration) => {
const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
if (isMobile) {
const fileSize = file.size / (1024 * 1024); // MB
const maxDuration = 480; // 8분
if (duration > maxDuration || fileSize > 100) {
return {
processable: false,
reason: '모바일에서는 8분 이하, 100MB 이하 파일만 처리 가능합니다.',
suggestSegment: true
};
}
}
return { processable: true };
};
3. 실시간 진행률과 에러 핸들링
모바일에서는 처리 중 브라우저가 백그라운드로 전환되거나 메모리 부족이 발생할 수 있습니다. 진행률을 실시간으로 표시하고, 에러 발생 시 적절한 복구 로직을 구현하는 것이 중요합니다. 사용자에게 명확한 피드백을 제공하여 처리 상태를 투명하게 공개합니다.
// 진행률 추적과 에러 핸들링
class MobileVideoProcessor {
constructor() {
this.ffmpeg = null;
this.isProcessing = false;
this.progressCallback = null;
}
async initialize() {
try {
this.ffmpeg = await initFFmpeg();
this.ffmpeg.on('progress', ({ progress, time }) => {
if (this.progressCallback) {
this.progressCallback({
progress: Math.min(progress * 100, 100),
time: time,
status: 'processing'
});
}
});
this.ffmpeg.on('log', ({ message }) => {
console.log('FFmpeg:', message);
if (message.includes('Cannot allocate memory')) {
this.handleMemoryError();
}
});
} catch (error) {
throw new Error(`FFmpeg 초기화 실패: ${error.message}`);
}
}
handleMemoryError() {
this.isProcessing = false;
if (this.progressCallback) {
this.progressCallback({
progress: 0,
status: 'error',
message: '메모리 부족으로 처리가 중단되었습니다. 더 작은 파일을 사용해 주세요.'
});
}
}
async processVideo(file, options = {}) {
if (this.isProcessing) {
throw new Error('이미 다른 비디오를 처리 중입니다.');
}
this.isProcessing = true;
try {
const inputName = 'input.' + file.name.split('.').pop();
const outputName = 'output.' + (options.format || 'mp4');
await this.ffmpeg.writeFile(inputName, await file.arrayBuffer());
const ffmpegArgs = [
'-i', inputName,
'-c:v', 'libx264',
'-crf', '23',
'-preset', 'fast',
'-c:a', 'aac',
'-b:a', '128k',
outputName
];
await this.ffmpeg.exec(ffmpegArgs);
const outputData = await this.ffmpeg.readFile(outputName);
return new Blob([outputData], { type: `video/${options.format || 'mp4'}` });
} finally {
this.isProcessing = false;
}
}
}
4. 모바일 최적화된 인코딩 설정
모바일에서는 처리 시간과 메모리 사용량을 고려한 인코딩 설정이 필요합니다. H.264 코덱의 fast 프리셋을 사용하고, CRF 값을 조정하여 품질과 파일 크기의 균형을 맞춥니다. 오디오는 AAC 128kbps로 설정하여 호환성과 효율성을 확보합니다.
// 모바일 최적화 인코딩 설정
const getMobileOptimizedArgs = (inputFormat, outputFormat, options = {}) => {
const baseArgs = ['-i', `input.${inputFormat}`];
if (options.maxWidth && options.maxHeight) {
baseArgs.push(
'-vf', `scale='min(${options.maxWidth},iw)':'min(${options.maxHeight},ih)':force_original_aspect_ratio=decrease`
);
}
baseArgs.push(
'-c:v', 'libx264',
'-preset', 'fast',
'-crf', '25',
'-maxrate', '2M',
'-bufsize', '4M'
);
baseArgs.push(
'-c:a', 'aac',
'-b:a', '128k',
'-ar', '44100'
);
baseArgs.push(`output.${outputFormat}`);
return baseArgs;
};
5. 사용자 인터페이스와 경험
모바일에서는 처리 시간이 길어질 수 있으므로 명확한 진행률 표시와 취소 기능이 필요합니다. 배터리 절약 모드나 백그라운드 전환 시에도 안정적으로 동작하도록 구현해야 합니다.
// 모바일 친화적 UI 컴포넌트
const VideoProcessorUI = () => {
const [progress, setProgress] = useState(0);
const [status, setStatus] = useState('idle');
const [result, setResult] = useState(null);
const processor = new MobileVideoProcessor();
processor.progressCallback = ({ progress, status, message }) => {
setProgress(progress);
setStatus(status);
if (status === 'error') alert(message);
};
const handleProcess = async (file) => {
try {
setStatus('processing');
await processor.initialize();
const result = await processor.processVideo(file, {
format: 'mp4',
maxWidth: 1920,
maxHeight: 1080
});
setResult(result);
setStatus('completed');
} catch (error) {
setStatus('error');
alert(`처리 실패: ${error.message}`);
}
};
};
6. 추가 최적화 방안
- Web Workers에서 FFmpeg 실행하여 메인 스레드 블로킹 방지
- IndexedDB를 활용한 중간 파일 임시 저장
- 네트워크 상태 감지로 업로드/다운로드 최적화
- 디바이스 성능에 따른 동적 품질 조절
- 배치 처리로 여러 파일 순차 변환
7. 마무리하며
모바일 브라우저에서 비디오 트랜스코딩은 여전히 제약이 많은 작업입니다. 메모리 한계를 인식하고 사용자에게 명확한 가이드라인을 제공하는 것이 핵심입니다. ffmpeg.wasm의 발전과 함께 모바일 성능도 지속적으로 개선되고 있어, 앞으로 더 나은 사용자 경험을 제공할 수 있을 것입니다.