배경: 클라이언트 사이드 AI 모델의 성능 부담

Frame Pick은 유튜브 영상 썸네일 생성 및 편집을 위한 100% 클라이언트 사이드 웹 애플리케이션입니다. 서버 비용 없이 모든 기능을 브라우저 내에서 처리하는 것을 목표로 하며, 이 중 핵심 기능 중 하나는 이미지에서 배경을 제거하는, 이른바 '누끼' 기능입니다. 이를 위해 @bunnio/rembg-web 라이브러리와 onnxruntime-web을 활용한 ONNX 기반 AI 모델을 도입했습니다.

문제는 AI 모델 파일의 크기였습니다. silueta 모델은 약 44MB, u2netp 모델은 약 4.4MB에 달합니다. 이 모델들을 애플리케이션 초기 로딩 시점에 모두 불러온다면, 사용자가 누끼 기능을 전혀 사용하지 않더라도 상당한 네트워크 지연과 애플리케이션 시작 속도 저하를 겪게 됩니다. Frame Pick은 클라이언트 사이드 앱이므로, 초기 로딩 성능은 사용자 경험에 결정적인 영향을 미칩니다. README에 명시된 "모델·WASM은 누끼 실행 시 lazy load" 방침에 따라, 이 문제를 해결하기 위한 최적화가 필요했습니다.

초기 시도와 한계점

가장 간단한 접근 방식은 누끼 기능을 담당하는 컴포넌트(BackgroundRemovalButton.tsxCutoutDialog.tsx 등)에서 import { RemBg } from '@bunnio/rembg-web';와 같이 상단에 라이브러리를 import 하는 것이었습니다. 그러나 Next.js 환경에서는 이렇게 임포트된 모듈은 해당 컴포넌트가 초기 렌더링될 때 함께 번들에 포함되거나, 최소한 메인 번들의 종속성으로 처리되어 다운로드됩니다. 이는 누끼 기능이 활성화되지 않아도 관련 코드와 onnxruntime-web 같은 무거운 의존성들이 미리 로드될 수 있다는 의미입니다.

심지어 React.lazy를 사용하여 CutoutDialog 컴포넌트 자체를 지연 로딩하더라도, 다이얼로그가 처음 열리는 순간 해당 컴포넌트의 모든 JavaScript 코드가 다운로드됩니다. 이 코드 안에 @bunnio/rembg-web 임포트가 포함되어 있다면, 사용자가 누끼 기능의 AI 방식을 선택하고 '배경 제거' 버튼을 누르기도 전에 라이브러리 코드가 미리 로드되어 버립니다. 또한, AI 모델 파일(.onnx) 자체는 public/models/ 경로에 정적 파일로 존재하지만, 이 파일들을 언제 요청할지는 rembg-web 라이브러리의 load() 메서드가 호출되는 시점에 달려 있습니다. 핵심은 이 모든 것이 진정으로 필요할 때만 이루어지도록 하는 것이었습니다.

AI 모델 및 라이브러리 지연 로딩 전략

최종적으로 선택한 전략은 두 단계의 지연 로딩을 적용하는 것입니다.

  1. 누끼 다이얼로그 컴포넌트 코드 지연 로딩: 먼저 CutoutDialog.tsx 컴포넌트 자체를 Next.js의 dynamic 임포트 기능을 사용하여 지연 로딩했습니다. 이렇게 하면 사용자가 '이미지로 스티커 만들기' 기능을 활성화하여 누끼 다이얼로그를 열기 전까지는 다이얼로그 관련 JavaScript 코드가 번들에 포함되지 않습니다.

    // src/app/page.tsx 또는 관련 컴포넌트
    import dynamic from 'next/dynamic';
    import { Suspense, useState } from 'react';
    
    const DynamicCutoutDialog = dynamic(() => import('../components/ai/CutoutDialog'), {
      ssr: false, // 브라우저 환경에서만 로드
      loading: () => <p>누끼 다이얼로그 로딩 중...</p>,
    });
    
    const EditorPage = () => {
      const [isCutoutDialogOpen, setIsCutoutDialogOpen] = useState(false);
      // ...
      return (
        // ...
        <button onClick={() => setIsCutoutDialogOpen(true)}>누끼 만들기</button>
        {isCutoutDialogOpen && (
          <Suspense fallback={<p>다이얼로그 로딩 중...</p>}>
            <DynamicCutoutDialog onClose={() => setIsCutoutDialogOpen(false)} />
          </Suspense>
        )}
      );
    };
    
  2. AI 라이브러리 및 모델 파일 지연 로딩: CutoutDialog 컴포넌트 내부에서, 사용자가 실제로 AI 기반 누끼 방식을 선택하고 '배경 제거' 버튼을 클릭하는 시점에 비로소 @bunnio/rembg-web 라이브러리와 onnxruntime-web 관련 코드를 동적으로 임포트하고, 해당 모델 파일(.onnx)을 로드하도록 구현했습니다.

    // src/components/ai/CutoutDialog.tsx (핵심 로직 발췌)
    import React, { useState } from 'react';
    // ... 다른 UI 관련 임포트들
    
    const CutoutDialog = ({ onClose }) => {
      const [isLoadingModel, setIsLoadingModel] = useState(false);
      const [rembgInstance, setRembgInstance] = useState(null); // 로드된 RemBg 인스턴스 저장
    
      const handleRemoveBackground = async (modelType: 'silueta' | 'u2netp') => {
        setIsLoadingModel(true);
        try {
          // 오직 AI 누끼 버튼 클릭 시점에 라이브러리 동적 임포트
          const { RemBg } = await import('@bunnio/rembg-web');
          // onnxruntime-web은 rembg-web의 의존성으로 함께 로드됩니다.
    
          const modelPath = `/frame-pick/models/${modelType}.onnx`; // GitHub Pages basePath 고려
          const rembg = new RemBg({ model: modelPath });
          await rembg.load(); // ONNX 모델 파일 및 WASM 로딩 시작
    
          setRembgInstance(rembg);
          // ... 로드된 rembg 인스턴스로 배경 제거 로직 실행
        } catch (error) {
          console.error('AI 모델 로드 또는 초기화 실패:', error);
          // 사용자에게 오류 메시지 표시
        } finally {
          setIsLoadingModel(false);
        }
      };
    
      return (
        // ... 다이얼로그 UI
        <button onClick={() => handleRemoveBackground('u2netp')} disabled={isLoadingModel}>
          {isLoadingModel ? 'AI 모델 로딩 중...' : 'AI 빠름 누끼 실행'}
        </button>
        <button onClick={() => handleRemoveBackground('silueta')} disabled={isLoadingModel}>
          {isLoadingModel ? 'AI 모델 로딩 중...' : 'AI 고품질 누끼 실행'}
        </button>
        // ...
      );
    };
    export default CutoutDialog;
    

이 방식으로 사용자는 누끼 다이얼로그를 열 때 필요한 최소한의 코드만 다운로드하고, 나아가 AI 누끼 기능을 실제로 사용하기로 결정했을 때만 해당 AI 라이브러리 코드와 모델 파일(ONNX 및 WebAssembly)을 네트워크를 통해 불러오게 됩니다. 특히 rembg.load() 호출 시점에 onnxruntime-web이 필요한 WebAssembly 파일과 지정된 .onnx 모델 파일을 비동기적으로 가져오게 되므로, 이 과정에서 발생하는 네트워크 요청과 처리 시간 동안 사용자에게 'AI 모델 로딩 중...'과 같은 시각적 피드백을 제공할 수 있습니다.

결과 및 다음 과제

이러한 지연 로딩 전략을 통해 Frame Pick의 초기 로딩 성능을 크게 개선할 수 있었습니다. 사용자가 AI 누끼 기능을 사용하지 않는다면 수십 MB에 달하는 리소스가 전혀 다운로드되지 않아, 앱의 초기 진입 속도가 빨라지고 불필요한 네트워크 트래픽을 줄일 수 있습니다.

하지만 아직 개선할 부분도 남아 있습니다. 현재는 모델이 로드될 때마다 네트워크 요청을 다시 보내는 방식입니다. 추후에는 한 번 로드된 모델을 IndexedDB 같은 로컬 스토리지에 캐싱하여, 이후 재사용 시에는 네트워크 요청 없이 빠르게 로드할 수 있도록 하는 방안을 검토할 예정입니다. 또한, 모델 로딩 실패 시 사용자에게 더 명확한 가이드를 제공하는 에러 처리 로직을 강화해야 합니다. 마지막으로, 다른 AI 기능이 추가될 경우에도 일관된 지연 로딩 정책을 유지하여 애플리케이션의 전반적인 성능을 관리할 필요가 있습니다.