개발할 때는 Next.js, 실행할 때는 FastAPI 하나만 남기기

Python 백엔드에 웹 화면을 붙이면 개발 중에는 서버 두 개가 자연스럽습니다. Next.js 개발 서버가 화면 변경을 반영하고 FastAPI가 데이터를 처리합니다. 그런데 사용자가 로컬 도구를 실행할 때도 Node 서버와 Python 서버를 각각 켜야 할까요?
화면을 정적 파일로 만들 수 있다면 배포 후에는 FastAPI가 화면과 API를 같은 주소에서 제공하도록 구성할 수 있습니다. 개발 환경의 편의를 유지하면서 실행할 서버를 줄이는 선택입니다. 다만 개발 서버가 해 주던 일을 무엇이 대신할지 명확히 나눠야 합니다.
같은 프런트엔드라도 요청 경로는 달라집니다
개인 사진 분류 프로젝트의 웹 실행 경로는 개발과 배포에서 서로 다른 구성을 사용합니다.
개발 중에는 브라우저가 Next.js의 :3000에 접속하고 /api/* 요청은 Next.js가 FastAPI의 :8000으로 전달합니다.
정적 빌드 후에는 브라우저가 FastAPI에 직접 접속합니다.
화면 파일도 API 응답도 :8000에서 받습니다.
개발
브라우저 → Next.js :3000 → /api/* → FastAPI :8000
정적 빌드 후 웹 실행
브라우저 → FastAPI :8000
├─ /api/* : API 처리
└─ 나머지 경로 : frontend/out/의 정적 파일
이 설명은 저장소에서 Python 웹 서버를 직접 실행하는 경로에 한정합니다. 패키징된 데스크톱 앱은 별도 실행 경로가 있으므로 같은 방식으로 파일을 찾는다고 가정하지 않습니다.
개발 프록시를 정적 빌드에 넣지 않습니다
frontend/next.config.ts는 production 여부에 따라 설정을 나눕니다.
아래는 관련 설정만 줄여서 보여 준 것입니다.
const isProd = process.env.NODE_ENV === "production";
const nextConfig = isProd
? {
output: "export",
trailingSlash: true,
images: { unoptimized: true },
}
: {
async rewrites() {
return [
{
source: "/api/:path*",
destination: "http://127.0.0.1:8000/api/:path*",
},
];
},
};
개발 프록시는 개발 서버가 있을 때만 작동합니다.
정적 파일만 남은 배포 환경에는 이 요청을 대신 전달할 Next.js 프로세스가 없습니다.
여기서는 FastAPI가 직접 /api/*를 받으므로 프록시가 필요하지 않습니다.
Next.js의 output: "export"는 빌드한 HTML·CSS·JavaScript를 기본적으로 out/에 만듭니다.
trailingSlash: true는 페이지를 route/index.html 형태로 내보내는 데 사용할 수 있습니다.
프로젝트는 FastAPI 쪽 정적 파일의 디렉터리 인덱스 처리와 이 형태를 맞춥니다.
(Next.js 정적 내보내기 설정)
API를 먼저 등록하고 정적 파일은 마지막에 연결합니다
백엔드는 API 라우트들을 등록한 뒤 마지막에 /에 정적 파일 서버를 연결합니다.
다음은 그 마운트 부분입니다.
바깥 코드에서 일반 Python 실행인지와 frontend/out/의 존재를 확인합니다.
app.mount(
"/",
StaticFiles(directory=str(frontend_out), html=True),
name="frontend",
)
루트에 붙는 정적 파일 처리가 API보다 먼저 요청을 가져가지 않도록 순서가 중요합니다.
구현에도 API 등록 뒤에 마운트를 두라는 주석이 있습니다.
설정만 읽고 끝내지 말고 배포 확인에서는 /api/health가 JSON을 반환하는지와 화면 경로가 HTML을 반환하는지를 각각 확인해야 합니다.
Starlette의 StaticFiles는 html=True일 때 디렉터리의 index.html을 읽습니다.
그렇다고 임의의 없는 경로를 모두 앱의 루트 index.html로 돌려주는 것은 아닙니다.
파일이 맞지 않는 요청에는 404 또는 허용되지 않는 메서드의 경우 405로 응답합니다.
클라이언트 내비게이션으로만 이동해 보고 끝내지 않고 하위 주소를 직접 열어 보는 검사가 필요한 이유입니다.
(Starlette의 StaticFiles 문서)
정적 내보내기가 Server Component를 금지하지는 않습니다
이 구성을 검토할 때는 “서버에서 실행되는 코드가 있나?”보다 **“언제 실행되어야 하나?”**가 더 정확한 질문입니다. Next.js 공식 문서는 정적 내보내기에서도 Server Component가 빌드 중에 실행될 수 있다고 설명합니다. 금지되는 것은 Server Component라는 이름 자체가 아니라 요청 시점의 서버 기능에 의존하는 사용 방식입니다. (정적 내보내기와 Server Components)
사용자 요청마다 쿠키를 읽어 화면을 렌더링하거나 Next.js 서버의 동작을 전제로 한 기능을 그대로 가져오면 정적 구성과 맞지 않을 수 있습니다. 현재 구성이 정적 빌드를 선택했어도 모든 데이터까지 빌드 시점에 고정되지는 않습니다. 브라우저가 실행된 뒤 FastAPI에 요청해서 가져올 데이터는 계속 동적으로 처리할 수 있습니다.
판단할 때는 기능을 세 곳에 배치해 보면 됩니다. 빌드할 때 확정할 화면은 빌드 단계에, 브라우저에서 처리할 상호작용은 클라이언트에, 요청마다 필요한 데이터와 처리는 FastAPI에 둡니다. 이 분리가 안 되는 기능이 핵심이라면 Next.js 서버를 유지하는 편이 더 단순할 수 있습니다.
서버가 켜졌다는 것과 새 화면을 배포했다는 것은 다릅니다
이 저장소에서 make build는 프런트엔드 정적 파일을 만들고 make start는 백엔드를 실행합니다.
make start가 프런트엔드를 다시 빌드해 주지는 않습니다.
따라서 화면 코드를 바꿨더라도 이전 out/을 그대로 두면 이전 파일을 제공할 수 있습니다.
또한 구현은 out/이 없으면 정적 마운트를 건너뜁니다.
API가 정상이어도 새 프런트엔드가 준비됐다고 볼 수는 없습니다.
확인한 테스트 중에도 API의 health 응답을 검사하는 것이 있지만 그 검사만으로 새 화면이 들어 있다고 판단할 수는 없습니다.
이 구성의 배포를 검증한다면 다음 결과를 따로 확인해야 합니다. 아래는 이번 글에서 실행한 배포 결과가 아니라 구성에 맞춘 확인 절차입니다.
- 새 빌드의
out/에 기대한 페이지와 에셋이 존재합니다. - Next.js 개발 서버가 없어도 FastAPI 주소에서 화면과
/api/health가 각각 기대한 응답을 냅니다. - 하위 페이지 URL을 직접 열거나 새로고침해도 해당 페이지가 나옵니다.
- 화면이 보내는 API 요청이 배포 주소로 향하고 오래된 에셋이 남아 있지 않습니다.
개발할 때 서버가 두 개였다는 이유로 배포에서도 두 개를 유지할 필요는 없습니다. 화면을 빌드할 때 필요한 런타임과 사용자가 실행할 때 필요한 런타임을 구분하면 됩니다. 다만 서버를 하나 줄인 자리에 빌드 산출물 확인과 라우팅 검사를 함께 남겨야 실행 방법이 실제로 단순해집니다.
다음 글도 받아보세요.
글을 끝까지 읽으셨다면, 다음 글은 받은편지함이나 RSS 리더에서 만나보세요.