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

댓글
댓글 쓰기