요청 파일

요청 파일 (Request Files)

클라이언트가 업로드하는 파일을 File로 정의할 수 있습니다.

!!! note "참고" 업로드된 파일을 받으려면 먼저 python-multipart를 설치하세요.

프로젝트에 추가합니다:

```
$ uv add python-multipart
```

업로드된 파일은 "form data"로 전송되기 때문입니다.

출처: 공식문서

File 임포트하기 (Import File)

fastapi에서 FileUploadFile을 임포트하세요:

from typing import Annotated

from fastapi import FastAPI, File, UploadFile

app = FastAPI()


@app.post("/files/")
async def create_file(file: Annotated[bytes, File()]):
    return {"file_size": len(file)}


@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
    return {"filename": file.filename}

🤓 다른 버전과 변형

!!! tip "팁" 가능하다면 Annotated 버전을 사용하는 걸 권장합니다.

from fastapi import FastAPI, File, UploadFile

app = FastAPI()


@app.post("/files/")
async def create_file(file: bytes = File()):
    return {"file_size": len(file)}


@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
    return {"filename": file.filename}

File 파라미터 정의하기 (Define File Parameters)

BodyForm과 같은 방식으로 파일 파라미터를 만듭니다:

from typing import Annotated

from fastapi import FastAPI, File, UploadFile

app = FastAPI()


@app.post("/files/")
async def create_file(file: Annotated[bytes, File()]):
    return {"file_size": len(file)}


@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
    return {"filename": file.filename}

🤓 다른 버전과 변형

!!! tip "팁" 가능하다면 Annotated 버전을 사용하는 걸 권장합니다.

from fastapi import FastAPI, File, UploadFile

app = FastAPI()


@app.post("/files/")
async def create_file(file: bytes = File()):
    return {"file_size": len(file)}


@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
    return {"filename": file.filename}

!!! note "참고" FileForm에서 직접 상속받는 클래스입니다.

하지만 `fastapi`에서 `Query`, `Path`, `File` 등을 임포트하면, 그것들은 실제로 특수한 클래스를 반환하는 함수라는 걸 기억하세요.

!!! tip "팁" File 바디를 선언하려면 File을 사용해야 합니다. 그렇지 않으면 파라미터들이 쿼리 파라미터나 바디(JSON) 파라미터로 해석되기 때문입니다.

파일들은 "form data"로 업로드됩니다.

경로 동작 함수 파라미터의 타입을 bytes로 선언하면, FastAPI가 파일을 읽어서 내용을 bytes로 받게 해 줍니다.

이것은 전체 내용이 메모리에 저장된다는 뜻이라는 점을 명심하세요. 작은 파일에는 잘 동작합니다.

하지만 UploadFile을 쓰는 게 이로운 여러 경우가 있습니다.

UploadFile로 파일 파라미터 만들기 (File Parameters with UploadFile)

타입이 UploadFile인 파일 파라미터를 정의하세요:

from typing import Annotated

from fastapi import FastAPI, File, UploadFile

app = FastAPI()


@app.post("/files/")
async def create_file(file: Annotated[bytes, File()]):
    return {"file_size": len(file)}


@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
    return {"filename": file.filename}

🤓 다른 버전과 변형

!!! tip "팁" 가능하다면 Annotated 버전을 사용하는 걸 권장합니다.

from fastapi import FastAPI, File, UploadFile

app = FastAPI()


@app.post("/files/")
async def create_file(file: bytes = File()):
    return {"file_size": len(file)}


@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
    return {"filename": file.filename}

UploadFile을 사용하는 것은 bytes보다 여러 장점이 있습니다:

  • 파라미터의 기본값에 File()을 쓸 필요가 없습니다.
  • "스풀(spooled)" 파일을 사용합니다:
    • 최대 크기 제한까지는 메모리에 저장되고, 그 한도를 넘으면 디스크에 저장되는 파일.
  • 즉 이미지, 비디오, 큰 바이너리 같은 큰 파일도 메모리를 모두 소모하지 않고 잘 처리할 수 있습니다.
  • 업로드된 파일에서 메타데이터를 얻을 수 있습니다.
  • 파일 유사 async 인터페이스를 가집니다.
  • 실제 Python SpooledTemporaryFile 객체를 노출해서, 파일 유사 객체를 기대하는 다른 라이브러리에 바로 전달할 수 있습니다.

UploadFile (UploadFile)

UploadFile은 다음 속성들을 가집니다:

  • filename: 업로드된 원본 파일 이름을 담은 str(예: myimage.jpg).
  • content_type: 콘텐츠 타입(MIME 타입 / 미디어 타입)을 담은 str(예: image/jpeg).
  • file: SpooledTemporaryFile(파일 유사 객체)입니다. "file-like" 객체를 기대하는 다른 함수나 라이브러리에 바로 전달할 수 있는 실제 Python 파일 객체입니다.

UploadFile은 다음 async 메서드를 가집니다. 그것들은 모두 (내부 SpooledTemporaryFile을 사용해서) 해당 파일 메서드를 호출합니다.

  • write(data): 파일에 data(str 또는 bytes)를 씁니다.
  • read(size): 파일의 size(int) 바이트/문자를 읽습니다.
  • seek(offset): 파일에서 바이트 위치 offset(int)으로 이동합니다.
    • 예: await myfile.seek(0)는 파일의 시작 지점으로 이동합니다.
    • 특히 await myfile.read()를 한 번 실행한 뒤 내용을 다시 읽어야 할 때 유용합니다.
  • close(): 파일을 닫습니다.

이 모든 메서드는 async 메서드이므로 "await"해야 합니다.

예를 들어 async 경로 동작 함수 안에서는 이렇게 내용을 얻을 수 있습니다:

contents = await myfile.read()

일반적인 def 경로 동작 함수 안에 있다면, UploadFile.file에 직접 접근할 수 있습니다. 예를 들어:

contents = myfile.file.read()

!!! info "async 기술적 세부사항 (async Technical Details)" async 메서드를 사용하면 FastAPI가 파일 메서드를 스레드풀에서 실행하고 await합니다.

!!! info "Starlette 기술적 세부사항 (Starlette Technical Details)" FastAPIUploadFileStarletteUploadFile에서 직접 상속받지만, Pydantic 및 FastAPI의 다른 부분과 호환되도록 필요한 부분을 추가합니다.

"Form Data"란 무엇인가 (What is "Form Data")

HTML 폼(<form></form>)이 데이터를 서버로 보내는 방식은 보통 그 데이터에 대해 "특수한" 인코딩을 사용하는데, JSON과는 다릅니다.

FastAPI는 JSON 대신 올바른 위치에서 그 데이터를 읽도록 해 줍니다.

!!! info "기술적 세부사항 (Technical Details)" 폼의 데이터는 파일을 포함하지 않을 때 보통 "media type" application/x-www-form-urlencoded로 인코딩됩니다.

하지만 폼에 파일이 포함되면 `multipart/form-data`로 인코딩됩니다. `File`을 사용하면 **FastAPI**는 바디의 올바른 부분에서 파일을 가져와야 한다는 것을 알게 됩니다.

이런 인코딩과 폼 필드에 대해 더 읽고 싶다면 [MDN 웹 문서의 `POST`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST)를 참고하세요.

!!! warning "경고" 경로 동작 에서 여러 FileForm 파라미터를 선언할 수는 있지만, JSON으로 받을 것으로 예상되는 Body 필드는 함께 선언할 수 없습니다. 요청이 application/json이 아닌 multipart/form-data로 인코딩된 바디를 가지기 때문입니다.

이것은 **FastAPI**의 제한이 아니라, HTTP 프로토콜의 일부입니다.

선택적 파일 업로드 (Optional File Upload)

표준 타입 어노테이션을 사용하고 기본값을 None으로 설정하면 파일을 선택 사항으로 만들 수 있습니다:

from typing import Annotated

from fastapi import FastAPI, File, UploadFile

app = FastAPI()


@app.post("/files/")
async def create_file(file: Annotated[bytes | None, File()] = None):
    if not file:
        return {"message": "No file sent"}
    else:
        return {"file_size": len(file)}


@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile | None = None):
    if not file:
        return {"message": "No upload file sent"}
    else:
        return {"filename": file.filename}

🤓 다른 버전과 변형

!!! tip "팁" 가능하다면 Annotated 버전을 사용하는 걸 권장합니다.

from fastapi import FastAPI, File, UploadFile

app = FastAPI()


@app.post("/files/")
async def create_file(file: bytes | None = File(default=None)):
    if not file:
        return {"message": "No file sent"}
    else:
        return {"file_size": len(file)}


@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile | None = None):
    if not file:
        return {"message": "No upload file sent"}
    else:
        return {"filename": file.filename}

추가 메타데이터가 있는 UploadFile (UploadFile with Additional Metadata)

UploadFile과 함께 File()도 사용할 수 있습니다. 예를 들어 추가 메타데이터를 설정하려면:

from typing import Annotated

from fastapi import FastAPI, File, UploadFile

app = FastAPI()


@app.post("/files/")
async def create_file(file: Annotated[bytes, File(description="A file read as bytes")]):
    return {"file_size": len(file)}


@app.post("/uploadfile/")
async def create_upload_file(
    file: Annotated[UploadFile, File(description="A file read as UploadFile")],
):
    return {"filename": file.filename}

🤓 다른 버전과 변형

!!! tip "팁" 가능하다면 Annotated 버전을 사용하는 걸 권장합니다.

from fastapi import FastAPI, File, UploadFile

app = FastAPI()


@app.post("/files/")
async def create_file(file: bytes = File(description="A file read as bytes")):
    return {"file_size": len(file)}


@app.post("/uploadfile/")
async def create_upload_file(
    file: UploadFile = File(description="A file read as UploadFile"),
):
    return {"filename": file.filename}

여러 파일 업로드 (Multiple File Uploads)

여러 파일을 동시에 업로드하는 것도 가능합니다.

그것들은 "form data"로 전송되는 같은 "form field"에 연결됩니다.

그렇게 하려면 bytes 또는 UploadFile의 리스트를 선언하세요:

from typing import Annotated

from fastapi import FastAPI, File, UploadFile
from fastapi.responses import HTMLResponse

app = FastAPI()


@app.post("/files/")
async def create_files(files: Annotated[list[bytes], File()]):
    return {"file_sizes": [len(file) for file in files]}


@app.post("/uploadfiles/")
async def create_upload_files(files: list[UploadFile]):
    return {"filenames": [file.filename for file in files]}


@app.get("/")
async def main():
    content = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
    """
    return HTMLResponse(content=content)

🤓 다른 버전과 변형

!!! tip "팁" 가능하다면 Annotated 버전을 사용하는 걸 권장합니다.

from fastapi import FastAPI, File, UploadFile
from fastapi.responses import HTMLResponse

app = FastAPI()


@app.post("/files/")
async def create_files(files: list[bytes] = File()):
    return {"file_sizes": [len(file) for file in files]}


@app.post("/uploadfiles/")
async def create_upload_files(files: list[UploadFile]):
    return {"filenames": [file.filename for file in files]}


@app.get("/")
async def main():
    content = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
    """
    return HTMLResponse(content=content)

선언한 대로 bytes 또는 UploadFile들의 list를 받게 됩니다.

!!! info "기술적 세부사항 (Technical Details)" from starlette.responses import HTMLResponse를 사용할 수도 있습니다.

**FastAPI**는 개발자인 여러분을 위해 같은 `starlette.responses`를 `fastapi.responses`로 제공합니다. 하지만 사용 가능한 응답 대부분은 Starlette에서 직접 옵니다.

추가 메타데이터가 있는 여러 파일 업로드 (Multiple File Uploads with Additional Metadata)

그리고 이전과 같은 방식으로, UploadFile에도 File()을 사용해서 추가 파라미터를 설정할 수 있습니다:

from typing import Annotated

from fastapi import FastAPI, File, UploadFile
from fastapi.responses import HTMLResponse

app = FastAPI()


@app.post("/files/")
async def create_files(
    files: Annotated[list[bytes], File(description="Multiple files as bytes")],
):
    return {"file_sizes": [len(file) for file in files]}


@app.post("/uploadfiles/")
async def create_upload_files(
    files: Annotated[
        list[UploadFile], File(description="Multiple files as UploadFile")
    ],
):
    return {"filenames": [file.filename for file in files]}


@app.get("/")
async def main():
    content = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
    """
    return HTMLResponse(content=content)

🤓 다른 버전과 변형

!!! tip "팁" 가능하다면 Annotated 버전을 사용하는 걸 권장합니다.

from fastapi import FastAPI, File, UploadFile
from fastapi.responses import HTMLResponse

app = FastAPI()


@app.post("/files/")
async def create_files(
    files: list[bytes] = File(description="Multiple files as bytes"),
):
    return {"file_sizes": [len(file) for file in files]}


@app.post("/uploadfiles/")
async def create_upload_files(
    files: list[UploadFile] = File(description="Multiple files as UploadFile"),
):
    return {"filenames": [file.filename for file in files]}


@app.get("/")
async def main():
    content = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
    """
    return HTMLResponse(content=content)

요약 (Recap)

File, bytes, UploadFile을 사용해서 form data로 전송되는 요청에 업로드될 파일을 선언하세요.

더 알아보기 (Learn more)

  • 파일 업로드를 받으려면 먼저 python-multipart를 설치해야 합니다.
  • bytes는 작은 파일을 메모리에 담고, UploadFile은 큰 파일을 디스크에 스풀링하므로 대용량에 적합합니다.
  • list[bytes] / list[UploadFile]로 여러 파일을 한 번에 받을 수 있습니다.
  • File()과 함께 폼/파일을, JSON 바디(Body)와 함께 선언할 수는 없습니다(HTTP 프로토콜 제약).