[{"data":1,"prerenderedAt":20},["ShallowReactive",2],{"$fMN1TDgmfAq1ressEDgB42X3zk23RkWE6JjJS_28d3PI":3},{"uuid":4,"title":5,"summary":6,"content":7,"createdAt":8,"tags":9},"49f1ea39-e69f-42fb-9320-9af5abbe3829","Next.js 브라우저 환경에서 ONNX 기반 AI 누끼 모델 지연 로딩 최적화","Frame Pick 프로젝트에서 ONNX 기반 AI 누끼 모델의 큰 파일 크기로 인해 발생하는 초기 로딩 성능 문제를 해결하기 위해, 모델과 관련 라이브러리를 필요할 때만 동적으로 로드하도록 최적화했습니다. 이를 통해 사용자 경험을 개선하고 초기 번들 크기를 효과적으로 줄였습니다.","## 배경: 클라이언트 사이드 AI 모델의 성능 부담\n\nFrame Pick은 유튜브 영상 썸네일 생성 및 편집을 위한 100% 클라이언트 사이드 웹 애플리케이션입니다. 서버 비용 없이 모든 기능을 브라우저 내에서 처리하는 것을 목표로 하며, 이 중 핵심 기능 중 하나는 이미지에서 배경을 제거하는, 이른바 '누끼' 기능입니다. 이를 위해 `@bunnio\u002Frembg-web` 라이브러리와 `onnxruntime-web`을 활용한 ONNX 기반 AI 모델을 도입했습니다.\n\n문제는 AI 모델 파일의 크기였습니다. `silueta` 모델은 약 44MB, `u2netp` 모델은 약 4.4MB에 달합니다. 이 모델들을 애플리케이션 초기 로딩 시점에 모두 불러온다면, 사용자가 누끼 기능을 전혀 사용하지 않더라도 상당한 네트워크 지연과 애플리케이션 시작 속도 저하를 겪게 됩니다. `Frame Pick`은 클라이언트 사이드 앱이므로, 초기 로딩 성능은 사용자 경험에 결정적인 영향을 미칩니다. README에 명시된 \"모델·WASM은 누끼 실행 시 lazy load\" 방침에 따라, 이 문제를 해결하기 위한 최적화가 필요했습니다.\n\n## 초기 시도와 한계점\n\n가장 간단한 접근 방식은 누끼 기능을 담당하는 컴포넌트(`BackgroundRemovalButton.tsx`나 `CutoutDialog.tsx` 등)에서 `import { RemBg } from '@bunnio\u002Frembg-web';`와 같이 상단에 라이브러리를 import 하는 것이었습니다. 그러나 Next.js 환경에서는 이렇게 임포트된 모듈은 해당 컴포넌트가 초기 렌더링될 때 함께 번들에 포함되거나, 최소한 메인 번들의 종속성으로 처리되어 다운로드됩니다. 이는 누끼 기능이 활성화되지 않아도 관련 코드와 `onnxruntime-web` 같은 무거운 의존성들이 미리 로드될 수 있다는 의미입니다.\n\n심지어 `React.lazy`를 사용하여 `CutoutDialog` 컴포넌트 자체를 지연 로딩하더라도, 다이얼로그가 처음 열리는 순간 해당 컴포넌트의 모든 JavaScript 코드가 다운로드됩니다. 이 코드 안에 `@bunnio\u002Frembg-web` 임포트가 포함되어 있다면, 사용자가 누끼 기능의 AI 방식을 선택하고 '배경 제거' 버튼을 누르기도 전에 라이브러리 코드가 미리 로드되어 버립니다. 또한, AI 모델 파일(`.onnx`) 자체는 `public\u002Fmodels\u002F` 경로에 정적 파일로 존재하지만, 이 파일들을 언제 요청할지는 `rembg-web` 라이브러리의 `load()` 메서드가 호출되는 시점에 달려 있습니다. 핵심은 이 모든 것이 *진정으로 필요할 때만* 이루어지도록 하는 것이었습니다.\n\n## AI 모델 및 라이브러리 지연 로딩 전략\n\n최종적으로 선택한 전략은 두 단계의 지연 로딩을 적용하는 것입니다.\n\n1.  **누끼 다이얼로그 컴포넌트 코드 지연 로딩**: 먼저 `CutoutDialog.tsx` 컴포넌트 자체를 Next.js의 `dynamic` 임포트 기능을 사용하여 지연 로딩했습니다. 이렇게 하면 사용자가 '이미지로 스티커 만들기' 기능을 활성화하여 누끼 다이얼로그를 열기 전까지는 다이얼로그 관련 JavaScript 코드가 번들에 포함되지 않습니다.\n\n    ```typescript\n    \u002F\u002F src\u002Fapp\u002Fpage.tsx 또는 관련 컴포넌트\n    import dynamic from 'next\u002Fdynamic';\n    import { Suspense, useState } from 'react';\n\n    const DynamicCutoutDialog = dynamic(() => import('..\u002Fcomponents\u002Fai\u002FCutoutDialog'), {\n      ssr: false, \u002F\u002F 브라우저 환경에서만 로드\n      loading: () => \u003Cp>누끼 다이얼로그 로딩 중...\u003C\u002Fp>,\n    });\n\n    const EditorPage = () => {\n      const [isCutoutDialogOpen, setIsCutoutDialogOpen] = useState(false);\n      \u002F\u002F ...\n      return (\n        \u002F\u002F ...\n        \u003Cbutton onClick={() => setIsCutoutDialogOpen(true)}>누끼 만들기\u003C\u002Fbutton>\n        {isCutoutDialogOpen && (\n          \u003CSuspense fallback={\u003Cp>다이얼로그 로딩 중...\u003C\u002Fp>}>\n            \u003CDynamicCutoutDialog onClose={() => setIsCutoutDialogOpen(false)} \u002F>\n          \u003C\u002FSuspense>\n        )}\n      );\n    };\n    ```\n\n2.  **AI 라이브러리 및 모델 파일 지연 로딩**: `CutoutDialog` 컴포넌트 내부에서, 사용자가 실제로 AI 기반 누끼 방식을 선택하고 '배경 제거' 버튼을 클릭하는 시점에 비로소 `@bunnio\u002Frembg-web` 라이브러리와 `onnxruntime-web` 관련 코드를 동적으로 임포트하고, 해당 모델 파일(`.onnx`)을 로드하도록 구현했습니다.\n\n    ```typescript\n    \u002F\u002F src\u002Fcomponents\u002Fai\u002FCutoutDialog.tsx (핵심 로직 발췌)\n    import React, { useState } from 'react';\n    \u002F\u002F ... 다른 UI 관련 임포트들\n\n    const CutoutDialog = ({ onClose }) => {\n      const [isLoadingModel, setIsLoadingModel] = useState(false);\n      const [rembgInstance, setRembgInstance] = useState(null); \u002F\u002F 로드된 RemBg 인스턴스 저장\n\n      const handleRemoveBackground = async (modelType: 'silueta' | 'u2netp') => {\n        setIsLoadingModel(true);\n        try {\n          \u002F\u002F 오직 AI 누끼 버튼 클릭 시점에 라이브러리 동적 임포트\n          const { RemBg } = await import('@bunnio\u002Frembg-web');\n          \u002F\u002F onnxruntime-web은 rembg-web의 의존성으로 함께 로드됩니다.\n\n          const modelPath = `\u002Fframe-pick\u002Fmodels\u002F${modelType}.onnx`; \u002F\u002F GitHub Pages basePath 고려\n          const rembg = new RemBg({ model: modelPath });\n          await rembg.load(); \u002F\u002F ONNX 모델 파일 및 WASM 로딩 시작\n\n          setRembgInstance(rembg);\n          \u002F\u002F ... 로드된 rembg 인스턴스로 배경 제거 로직 실행\n        } catch (error) {\n          console.error('AI 모델 로드 또는 초기화 실패:', error);\n          \u002F\u002F 사용자에게 오류 메시지 표시\n        } finally {\n          setIsLoadingModel(false);\n        }\n      };\n\n      return (\n        \u002F\u002F ... 다이얼로그 UI\n        \u003Cbutton onClick={() => handleRemoveBackground('u2netp')} disabled={isLoadingModel}>\n          {isLoadingModel ? 'AI 모델 로딩 중...' : 'AI 빠름 누끼 실행'}\n        \u003C\u002Fbutton>\n        \u003Cbutton onClick={() => handleRemoveBackground('silueta')} disabled={isLoadingModel}>\n          {isLoadingModel ? 'AI 모델 로딩 중...' : 'AI 고품질 누끼 실행'}\n        \u003C\u002Fbutton>\n        \u002F\u002F ...\n      );\n    };\n    export default CutoutDialog;\n    ```\n\n이 방식으로 사용자는 누끼 다이얼로그를 열 때 필요한 최소한의 코드만 다운로드하고, 나아가 AI 누끼 기능을 실제로 사용하기로 결정했을 때만 해당 AI 라이브러리 코드와 모델 파일(ONNX 및 WebAssembly)을 네트워크를 통해 불러오게 됩니다. 특히 `rembg.load()` 호출 시점에 `onnxruntime-web`이 필요한 WebAssembly 파일과 지정된 `.onnx` 모델 파일을 비동기적으로 가져오게 되므로, 이 과정에서 발생하는 네트워크 요청과 처리 시간 동안 사용자에게 'AI 모델 로딩 중...'과 같은 시각적 피드백을 제공할 수 있습니다.\n\n## 결과 및 다음 과제\n\n이러한 지연 로딩 전략을 통해 `Frame Pick`의 초기 로딩 성능을 크게 개선할 수 있었습니다. 사용자가 AI 누끼 기능을 사용하지 않는다면 수십 MB에 달하는 리소스가 전혀 다운로드되지 않아, 앱의 초기 진입 속도가 빨라지고 불필요한 네트워크 트래픽을 줄일 수 있습니다.\n\n하지만 아직 개선할 부분도 남아 있습니다. 현재는 모델이 로드될 때마다 네트워크 요청을 다시 보내는 방식입니다. 추후에는 한 번 로드된 모델을 IndexedDB 같은 로컬 스토리지에 캐싱하여, 이후 재사용 시에는 네트워크 요청 없이 빠르게 로드할 수 있도록 하는 방안을 검토할 예정입니다. 또한, 모델 로딩 실패 시 사용자에게 더 명확한 가이드를 제공하는 에러 처리 로직을 강화해야 합니다. 마지막으로, 다른 AI 기능이 추가될 경우에도 일관된 지연 로딩 정책을 유지하여 애플리케이션의 전반적인 성능을 관리할 필요가 있습니다.","2026-08-13T15:11:17.454+09:00",[10,11,12,13,14,15,16,17,18,19],"GitHub Pages","Lazy Loading","Next-js","ONNX","Performance Optimization","TypeScript","WebAssembly","onnxruntime-web","rembg-web","개발기",1786605581120]