Skip to content

fix(deploy): Next.js 배포에서 커밋된 config 가 이겨 산출물이 나오지 않던 문제 - #172

Merged
dldnsgkr merged 1 commit into
developfrom
unhak/nextjs-base-override
Aug 25, 2026
Merged

fix(deploy): Next.js 배포에서 커밋된 config 가 이겨 산출물이 나오지 않던 문제#172
dldnsgkr merged 1 commit into
developfrom
unhak/nextjs-base-override

Conversation

@dldnsgkr

Copy link
Copy Markdown
Collaborator

#116Next.js 부분을 고친다. 나머지 프레임워크는 이슈에 남긴다(아래 "범위" 참고).

검증 — 이슈보다 상황이 나쁘다

create-next-app  →  next.config.mjs 를 항상 만든다 (output: 'export' 없음)
감지             →  next.config.* 존재 → "nextjs"                (GithubRepoClient:311)
config 스텝      →  파일이 있으니 경고만 하고 지나감              (:237, 수정 전)
빌드             →  next build → .next 생성 (out 아님)
publish          →  publish_dir: ./out → 디렉터리 없음 → 실패     (:143, :386)

이슈 제목은 "자산 404" 인데, 실제로는 그 이전에 배포가 끝까지 가지 못한다. 생성 분기는 스캐폴딩 도구가 config 를 항상 만들기 때문에 현실에서 거의 발화하지 않는다.

해결 — 덮어쓰지 않고 감싼다

사용자의 config 를 지우지 않고 옆으로 옮긴 뒤, 그것을 불러와 우리가 소유하는 세 필드만 얹은 config 를 새로 쓴다.

const userConfig = require('./next.config.qeploy-user.js');
const base = process.env.QEPLOY_BASE_PATH ?? '';
const resolved = typeof userConfig === 'function' ? userConfig() : (userConfig?.default ?? userConfig ?? {});
module.exports = {
  ...resolved,
  output: 'export',
  basePath: base,
  images: { ...(resolved.images ?? {}), unoptimized: true },
};

파싱하지 않는다. Next 자신이 사용자 config 를 읽게 두고 결과 객체에만 손대므로 형식(JS/MJS/TS)이나 내용에 기대지 않는다. 사용자가 적은 redirects·env·webpack 설정은 그대로 살아남는다.

세 필드를 우리가 소유하는 근거는 각각 다르다. outputimages.unoptimized 는 GitHub Pages 가 정적 호스팅이라는 사실에서, basePath 는 배포 URL 에서 나온다. 셋 다 사용자가 정할 수 있는 값이 아니다.

실측 (2026-08-25, 워크플로 셸 로직을 그대로 실행)

입력 결과
ESM .mjs (reactStrictMode, env.MY_FLAG) 둘 다 유지 + output/basePath/images 적용
CJS .js (images.formats), 커스텀 도메인 formats 유지한 채 unoptimized 추가, basePath: ""
함수형 module.exports = () => ({...}) 평가해서 병합

두 가지 세부

QEPLOY_BASE_PATH 를 따로 넘긴다. Next 의 basePath 는 trailing slash 가 없어야 하는데 기존 BASE_PATH/PUBLIC_URL 은 slash 를 포함한다(Vite --base, CRA homepage 가 그 형태를 요구한다). 같은 값을 재사용하면 경로가 // 로 겹친다.

import 경로를 확장자에 따라 가른다. ESM 은 확장자를 반드시 요구하고, TypeScript 는 반대로 .ts 확장자 import 를 allowImportingTsExtensions 없이는 허용하지 않는다.

범위

Next.js 로 한정했다. CODE 프롬프트가 제시하는 넷 중 깨지는 것은 이것 하나다.

스캐폴딩 경로 상태
Vite + React (preferred) vite build --base= 안전
CRA homepage 강제 덮어쓰기 안전
Vue (create vue@latest) Vite 기반 안전
Next.js 커밋된 config 가 이김 이 PR

gatsby·sveltekit·astro·nuxt·vue-cli 는 사용자가 기존 저장소를 연결할 때만 등장한다. 같은 방식으로 덮을 수 있지만 프레임워크마다 config 형식과 필드가 달라 별도 단위로 남긴다 — #116 은 그것 때문에 열어둔다.

검증

  • 마이그레이션 없음. 워크플로 템플릿과 그 테스트만 바뀐다
  • 테스트 4개 추가: 경고로 끝내지 않고 감싸는지 / slash 없는 base 를 자기 env 로 읽는지 / config 가 없으면 예전처럼 생성하는지 / TS 확장자를 떼는지
  • ./gradlew test 전체 통과

기존 저장소는 다음 배포부터 적용된다. 워크플로 파일은 배포 때마다 ensureWorkflow 가 다시 쓴다.

🤖 Generated with Claude Code

config 가 없을 때만 만들고 있으면 경고만 남겼는데, create-next-app 은 스캐폴딩 때
next.config.mjs 를 항상 만든다. 그래서 생성 분기는 현실에서 거의 발화하지 않고
커밋된 config 가 유일한 진실이 된다.

그 config 에는 output: 'export' 가 없다. next build 는 .next 만 만들고 ./out 은
생기지 않으므로 publish 스텝이 그 디렉터리를 찾다 실패한다 — 자산 404 이전에 배포
자체가 끝까지 가지 못한다.

basePath 도 커밋된 값이 알 수 없는 값이다. 커스텀 도메인이 붙으면 '/', 아니면
'/{repo}' 여야 하는데 이건 배포 시점에만 정해진다.

덮어쓰지 않고 감싼다. 사용자의 config 를 옆으로 옮긴 뒤 그것을 불러와 우리가
소유하는 세 필드만 얹은 config 를 새로 쓴다. 사용자가 적은 다른 설정(redirects,
env, webpack 등)은 그대로 살아남는다. 파싱하지 않으므로 형식(JS/MJS/TS)에 기대지
않는다 — Next 자신이 읽게 두고 결과 객체에만 손댄다.

우리가 세 필드를 소유하는 근거는 각각 다르다. output 과 images.unoptimized 는
GitHub Pages 가 정적 호스팅이라는 사실에서, basePath 는 배포 URL 에서 나온다.
셋 다 사용자가 정할 수 있는 값이 아니다.

실측(2026-08-25, 워크플로 셸 로직을 그대로 실행):

  ESM(.mjs)  사용자의 reactStrictMode·env 유지 + 세 필드 적용
  CJS(.js)   images.formats 유지한 채 unoptimized 추가, 커스텀 도메인이면 basePath ''
  함수형     module.exports = () => ({...}) 도 평가해 병합

basePath 는 trailing slash 가 없어야 해서 QEPLOY_BASE_PATH 를 따로 넘긴다.
기존 BASE_PATH/PUBLIC_URL 은 slash 를 포함한다(Vite --base, CRA homepage 가 그
형태를 요구한다).

TypeScript 는 '.ts' 확장자 import 를 허용하지 않고 ESM 은 확장자를 반드시 요구하므로
import 경로를 확장자에 따라 가른다.

범위는 Next.js 로 한정했다. 우리 CODE 프롬프트가 제시하는 넷(Vite+React, CRA, Vue,
Next.js) 중 깨지는 것은 Next.js 하나다 — 나머지는 vite build --base 나 homepage
덮어쓰기로 이미 배포 시점 값이 이긴다. gatsby/sveltekit/astro/nuxt/vue-cli 는
사용자가 기존 저장소를 연결할 때만 등장하므로 별도 단위로 남긴다(#116).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dldnsgkr
dldnsgkr merged commit bc37c6a into develop Aug 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant