FastAPI app.mount 사용 후 API가 404가 되는 이유와 해결 방법

필자는 UGREEN DXP4800PLUS NAS 환경에서 Docker 기반 유튜브 쇼츠 자동화 시스템을 운영하고 있다. FastAPI와 PostgreSQL로 트렌드 수집 대시보드를 구축하던 중 모든 API 엔드포인트가 404를 반환하는 문제를 겪었다. 처음에는 PostgreSQL 연결 문제라고 생각했다. API가 전부 404를 반환하니 DB 연결이 끊어진 줄 알았다. docker compose logs로 컨테이너 로그를 뒤졌고, curl로 직접 호출해보니 FastAPI까지 요청이 들어오지도 않았다. 결국 원인은 app.mount("/") 한 줄이었다. 수정하고 나서 허탈했다. 30분을 날렸다. app.mount("/") 선언 위치 하나 때문이었다. 수정 후 즉시 정상 동작했다.

이 글에서는 해당 오류의 원인과 해결 방법, 그리고 FastAPI 내부 동작 원리까지 정리한다.


문제 상황

아래와 같이 코드를 작성했을 때 /api/trends 엔드포인트가 404를 반환한다.

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

app = FastAPI()

# 정적 파일 마운트
app.mount("/", StaticFiles(directory="static", html=True), name="static")

# API 엔드포인트 선언
@app.get("/api/trends")
async def get_trends():
    return {"status": "success"}

코드 자체는 문법적으로 문제가 없다. 하지만 실행하면 /api/trends 호출 시 404가 반환된다.


원인 분석

FastAPI는 라우팅 처리 시 선언 순서대로 매칭을 시도한다.

app.mount("/") 를 최상단에 선언하면 루트 경로(/)가 StaticFiles에 먼저 매칭된다. 그 결과 이후에 선언된 모든 API 엔드포인트는 StaticFiles 핸들러에 가로막혀 도달하지 못한다.

요청 흐름:

클라이언트 요청 /api/trends

↓
app.mount("/") 매칭 → StaticFiles 처리
↓
static 폴더에 /api/trends 파일 없음
↓
404 반환

왜 이런 설계로 동작할까?

FastAPI는 Starlette 기반 프레임워크다. 라우팅 테이블을 생성할 때 선언 순서대로 경로를 검사한다.

app.mount("/") 는 루트 경로 전체를 StaticFiles에 위임하는 선언이다. 따라서 이것이 먼저 등록되면 /api/trends, /api/stats 등 모든 하위 경로도 StaticFiles가 우선 처리하게 된다.

이 동작은 FastAPI의 버그가 아니라 정상 동작이다. Starlette의 마운트 우선순위 설계를 그대로 따른다.


해결 방법

app.mount("/")항상 파일의 마지막 줄에 위치시켜야 한다.

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

app = FastAPI()

# API 엔드포인트를 먼저 선언
@app.get("/api/trends")
async def get_trends():
    return {"status": "success"}

@app.get("/api/stats")
async def get_stats():
    return {"count": 100}

# 정적 파일 마운트는 항상 마지막
app.mount("/", StaticFiles(directory="static", html=True), name="static")

수정 후 요청 흐름:

클라이언트 요청 /api/trends

↓
@app.get("/api/trends") 매칭 → 정상 응답



app.mount 위치 수정 후 정상 동작하는 유튜브 쇼츠 트렌드 대시보드 (FastAPI + PostgreSQL)

주의사항

새 엔드포인트 추가 시 반드시 확인할 것

나중에 엔드포인트를 추가할 때 app.mount() 아래에 선언하면 같은 문제가 반복된다.

# 잘못된 예시
app.mount("/", StaticFiles(directory="static", html=True), name="static")

@app.post("/api/classify")  # ❌ 동작하지 않음
async def classify():
    return {"result": "ok"}

APIRouter 사용 시에도 동일

from fastapi import FastAPI, APIRouter
from fastapi.staticfiles import StaticFiles

app = FastAPI()
router = APIRouter()

@router.get("/api/data")
async def get_data():
    return {"data": "ok"}

app.include_router(router)  # mount 이전에 호출

# 항상 마지막
app.mount("/", StaticFiles(directory="static", html=True), name="static")

정리

핵심 규칙 정리:

- 잘못된 방법: app.mount("/") 먼저 선언 → API 전부 404
- 올바른 방법: API 엔드포인트 먼저 선언 → app.mount()는 항상 마지막

FastAPI에서 app.mount("/") 는 항상 파일의 마지막 줄이라는 규칙 하나만 기억해두면 이 문제는 다시 발생하지 않는다. 특히 팀 프로젝트에서는 이 규칙을 코딩 컨벤션으로 공유해두는 것을 권장한다.


관련 글

댓글

이 블로그의 인기 게시물

UGREEN DXP4800PLUS NAS로 유튜브 쇼츠 자동화 시스템 구축기 (Docker · FastAPI · PostgreSQL · Edge-TTS)

YouTube Data API 쿼터 초과를 피하는 방법 (700개 쇼츠 수집 사례)