최근 HDomi/frame-pick 프로젝트를 GitHub Pages에 배포하는 과정에서 예상치 못한 경로 문제를 마주했습니다. Frame Pick은 유튜브 썸네일 생성 및 편집기로, 100% 클라이언트 사이드에서 동작하며 Next.js 16 (App Router)과 TypeScript를 기반으로 합니다. 서버 비용을 0으로 만들기 위해 next.config.ts에서 output: 'export' 옵션을 설정하여 완전한 정적 HTML 파일들로 빌드하고, GitHub Pages의 저장소 이름을 따라 basePath: '/frame-pick'을 지정했습니다. 배포 후 웹 애플리케이션에 접속했을 때, UI는 정상적으로 로드되는 것처럼 보였지만, AI 누끼 모델인 public/models/u2netp.onnx와 스티커 이미지들 (public/stickers/*.svg)이 로딩되지 않아 핵심 기능들이 동작하지 않았습니다. 개발자 도구의 네트워크 탭에서는 해당 자산들에 대한 404 Not Found 에러가 발생하고 있었습니다.
초기 진단: basePath 설정 검토
문제의 원인을 파악하기 위해 가장 먼저 next.config.ts 파일을 확인했습니다. Next.js의 basePath 옵션은 애플리케이션이 루트 경로가 아닌 서브 경로, 예를 들어 https://username.github.io/frame-pick와 같이 배포될 때 필요한 설정입니다. 이 옵션이 제대로 설정되어 있다면 Next.js는 모든 내부 링크와 자신이 생성하는 자산(JavaScript, CSS 번들 등)의 URL에 이 basePath를 자동으로 접두사로 붙여줍니다. 제 next.config.ts에는 basePath: '/frame-pick'가 명확히 설정되어 있었고, 실제로 _next로 시작하는 Next.js 코어 자산들(예: /_next/static/chunks/...)은 https://username.github.io/frame-pick/_next/...와 같이 basePath가 적용된 올바른 경로로 정상적으로 로드되는 것을 확인했습니다. 이는 basePath 설정 자체는 문제가 없음을 의미했습니다.
원인 파악: 프로그램적 자산 로딩의 맹점
핵심은 public 디렉토리 내에 직접 위치한 자산들이었습니다. 브라우저 개발자 도구의 네트워크 탭을 자세히 살펴보니, AI 모델 파일(u2netp.onnx)이나 스티커 SVG 파일들에 대한 요청 경로가 https://username.github.io/models/u2netp.onnx 또는 https://username.github.io/stickers/arrows/arrow_curve.svg와 같이 basePath인 /frame-pick이 누락된 채 요청되고 있었습니다. 즉, 브라우저는 basePath를 무시하고 웹사이트의 루트 도메인(https://username.github.io)을 기준으로 절대 경로를 해석하고 있었습니다.
이러한 현상이 발생하는 이유는 public 디렉토리의 자산들이 Next.js의 <Image> 컴포넌트나 <Link> 컴포넌트처럼 프레임워크의 경로 처리 로직을 통하는 방식이 아니었기 때문입니다. 대신 Fabric.js의 loadSVGFromURL 함수나 onnxruntime-web의 모델 로딩 함수 등 클라이언트 측 JavaScript 코드에서 /models/u2netp.onnx와 같이 슬래시(/)로 시작하는 절대 경로 문자열을 직접 구성하여 자산을 fetch하거나 new Image().src 등으로 로드하고 있었습니다. Next.js의 basePath는 Next.js가 직접 번들링하거나 라우팅하는 경로에는 적용되지만, 클라이언트 사이드 JavaScript 코드에서 public 디렉토리의 자산을 직접 절대 경로로 참조할 때는 자동으로 basePath를 붙여주지 않습니다. 이 경우, 브라우저는 현재 문서의 origin을 기준으로 절대 경로를 해석하므로, basePath가 적용되지 않은 잘못된 URL이 생성되었던 것입니다.
해결 과정: basePath 명시적 적용
이 문제를 해결하기 위해서는 클라이언트 측 코드에서 public 디렉토리의 자산을 참조할 때 basePath를 명시적으로 붙여주어야 합니다. Next.js는 next.config.ts에 basePath가 설정되어 있을 경우, 빌드 시 process.env.NEXT_PUBLIC_BASE_PATH 환경 변수에 해당 값을 주입해 줍니다. 이 환경 변수는 클라이언트 코드에서도 접근 가능합니다.
따라서, public 디렉토리 내 자산 경로를 사용할 때 process.env.NEXT_PUBLIC_BASE_PATH를 접두사로 붙여주는 방식으로 코드를 수정했습니다. 예를 들어, AI 모델 파일을 로드하는 부분과 Fabric.js로 SVG 스티커를 로드하는 부분에 이 변경을 적용했습니다.
// 수정 전 (오류 발생 예시)
const modelPath = '/models/u2netp.onnx';
// Fabric.js SVG 로딩 예시
// fabric.loadSVGFromURL('/stickers/arrows/arrow_curve.svg', ...);
// 수정 후 (basePath 적용)
// process.env.NEXT_PUBLIC_BASE_PATH는 next.config.ts의 basePath 값으로 자동 주입됩니다.
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '';
const modelPath = `${basePath}/models/u2netp.onnx`;
// Fabric.js SVG 로딩 예시
// fabric.loadSVGFromURL(`${basePath}/stickers/arrows/arrow_curve.svg`, (objects, options) => { ... });
이 변경 사항을 적용하고 다시 배포하자, AI 모델과 스티커 이미지가 https://username.github.io/frame-pick/models/u2netp.onnx와 같이 올바른 경로로 로드되기 시작했으며, 애플리케이션의 모든 기능이 정상적으로 동작함을 확인할 수 있었습니다. 클라이언트 JavaScript가 public 디렉토리의 자산에 접근할 때 basePath가 포함된 완전한 경로를 사용하게 된 것입니다.
다음에 할 것
현재는 process.env.NEXT_PUBLIC_BASE_PATH를 직접 사용하는 방식으로 문제를 해결했지만, 추후 이러한 경로 생성을 위한 유틸리티 함수를 만들거나, next/config의 publicRuntimeConfig를 활용하여 경로 관리를 좀 더 중앙집중화하는 방안을 고려해볼 수 있습니다. 또한, GitHub Pages 환경에서 커스텀 도메인을 사용하는 경우 basePath 설정 없이 루트 경로에 배포할 수도 있으므로, 배포 환경의 변화에 유연하게 대응할 수 있도록 경로 관리 전략을 좀 더 명확히 문서화할 필요가 있습니다.