파일 업로드#

Django Ninja 입문 가이드의 23번째 글이에요. 이제 고급 기능을 소개할 차례예요!
현대 웹 서비스에서 파일 업로드는 아주 흔한 시나리오예요.
사용자가 사진을 올리거나 첨부 파일을 보낼 때, 파일 업로드는 빼놓을 수 없는 기능이죠.
이번 글에서는 Django Ninja에서 이미지 업로드 기능을 구현하는 방법을 알아볼게요. 사용자의 '프로필 사진(avatar) 업로드' API를 예제로 삼아 이 과정을 단계별로 설명해 드릴게요.
이번 글의 모든 코드 변경 사항은 이 PR을 참고해 주세요.
GitHub 예제 프로젝트#
하지만 그전에 이번 장의 주제가 무엇인지 먼저 알아볼게요.
제6장 "API 고급 기능" 소개#
API 프로젝트에서 고급 기능은 복잡한 시나리오와 대규모 프로젝트의 과제에 대처하는 데 도움을 줘요.
이 가이드는 입문용이긴 하지만, API의 유연성을 높이고 시스템 성능과 사용자 경험을 향상할 수 있는 흔히 쓰이는 고급 기능들도 다룰 거예요.
이번 장은 총 5편으로, 3가지 주요 고급 기능을 소개해요:
- 23편: 파일 업로드 - Django UploadedFile 소개 (이번 글)
- 24편: 페이지네이션 (상) Django Ninja의 내장 페이지네이션
- 25편: 페이지네이션 (하) 사용자 정의 페이지네이션 클래스
- 26편: 데이터 조회 및 필터링 (상) FilterSchema 소개
- 27편: 데이터 조회 및 필터링 (하) FilterSchema 다중 필드 필터링
이러한 기술들은 대규모 프로젝트에 중요할 뿐만 아니라, API 개발 시 다양하게 변하는 요구사항에 효과적으로 대응할 수 있게 해줘요.
이번 장의 핵심을 이해했으니, 첫 번째 기능인 파일 업로드부터 시작해 볼게요.
파일 업로드의 주인공 - UploadedFile#
Django Ninja에서는 UploadedFile을 사용하여 업로드된 파일을 받을 수 있어요. 이것은 Django의 UploadedFile을 재포장한 것으로, 기본적으로 두 가지는 거의 같아요.
UploadedFile 개요#
UploadedFile은 Django에서 파일 업로드를 처리하는 핵심 객체예요. 사용자가 파일을 업로드하면 Django는 자동으로 파일을 UploadedFile 인스턴스로 캡슐화하여 이후의 파일 처리와 저장을 편리하게 해줘요.
UploadedFile 객체에는 여러 속성이 있는데, 그중 자주 사용되는 것은 다음과 같아요:
name: 업로드된 파일의 이름이에요. 파일의 원본 파일명에 접근할 때 사용할 수 있어요.size: 파일 크기(바이트 단위)예요. 파일 크기를 검증할 때 사용할 수 있어요.content_type: 파일의 MIME 타입이에요. 업로드된 파일의 형식을 검증할 때 유용해요. 예를 들어 파일이 이미지 형식인지 확인할 수 있죠. 나중에 사용할 거예요!read(): 파일 내용을 읽을 때 사용해요. 사용자 정의 파일 처리가 필요할 때 이 메서드를 사용하여 파일의 이진(binary) 데이터를 얻을 수 있어요.chunks(): 파일이 매우 클 때 이 메서드를 사용하면 메모리 사용을 줄이기 위해 파일을 여러 덩어리(chunk)로 나누어 읽을 수 있어요.
이러한 특성 덕분에 UploadedFile은 아주 유연해서 간단한 이미지 업로드부터 대용량 파일 처리까지 다양한 업로드 요구사항에 대응할 수 있어요.
자, 파일 업로드와 관련하여 핵심 컴포넌트인 UploadedFile을 이해하는 것만으로 충분해요.
'avatar 업로드' API 코드를 작성하기 전에 먼저 몇 가지 '사전 작업'을 해야 해요.
파일 업로드는 사실 Django Ninja보다는 Django와 더 관련이 깊은 부분이 많아서, 핵심만 간단히 짚고 넘어갈게요.
Django 프로젝트 관련 설정#
파일 업로드 기능을 구현하기 전에 먼저 Django에게 업로드된 파일을 어떻게 처리할지 알려주어야 해요. 이는 MEDIA_URL과 MEDIA_ROOT 설정과 관련이 있어요.
MEDIA_URL 및 MEDIA_ROOT 설정#
MEDIA_URL: 파일의 URL 접두사로, 업로드된 모든 파일은 이 URL을 통해 접근할 수 있어요.MEDIA_ROOT: Django 서버 내부에서 실제로 업로드된 파일이 저장되는 경로예요.
프로젝트의 settings.py에 다음 코드를 추가하세요:
이렇게 하면 업로드된 파일은 프로젝트 루트 디렉터리의 media 폴더에 저장되고, /media/ 경로를 통해 접근할 수 있어요.
개발 환경에서의 파일 접근#
개발 환경에서 이러한 파일에 직접 접근할 수 있도록 하려면 Django가 제공하는 static 메서드를 사용해야 해요.
프로젝트의 urls.py에 다음 코드를 추가하세요:
# NinjaForum/urls.py
from django.conf import settings
from django.conf.urls.static import static
...
urlpatterns = [
path('admin/', admin.site.urls),
path('', api.urls),
# 개발 환경에서 업로드된 파일에 접근할 수 있게 해요 (개발 환경 전용)
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
이 코드는 Django가 개발 환경에서 정적 파일 접근을 제공할 수 있도록 허용해요.
ImageField 필드 생성#
ImageField는 Django에서 이미지를 저장하기 위해 특화된 필드로, 실제로는 이미지의 파일 경로를 저장해요. 비슷한 필드로 FileField가 있어요.
코드는 다음과 같아요:
# user/models.py
class User(AbstractUser):
...
avatar = models.ImageField(upload_to='avatars/', null=True)
앞서 설정한 settings.py와 함께, avatar 필드는 업로드된 이미지를 media/avatars/ 폴더에 저장하게 될 거예요. 이 경로는 MEDIA_ROOT와 필드의 upload_to에 의해 결정돼요.
현재 브랜치로 이동한 후, 프로젝트에 새로운 마이그레이션 파일이 생겼으므로 데이터베이스 마이그레이션을 잊지 마세요:
참고로, ImageField는 서드파티 패키지인 Pillow에 의존해요. 이 패키지가 필드에 이미지 처리 기능을 제공하므로, 필드가 제대로 작동하려면 먼저 설치해야 해요:
Poetry를 사용하는 분들은 병합된 브랜치에서 바로 poetry install을 실행하면 돼요.
구현: avatar 업로드#
사전 작업이 끝났으니 드디어 하이라이트 부분으로 들어갈 수 있어요.
다음은 전체 'avatar 업로드' 기능의 코드예요:
from ninja import File, Router, UploadedFile
from ninja.errors import HttpError
...
@router.post('/users/{int:user_id}/avatar/',summary='avatar 업로드')
def upload_avatar(
request: HttpRequest,
user_id: int,
avatar_file: UploadedFile = File()
) -> dict[str, str]:
"""
avatar 업로드
"""
# 파일 타입 검사
if not avatar_file.content_type.startswith('image/'):
raise HttpError(400, '파일은 이미지 형식이어야 합니다')
user = User.objects.get(id=user_id)
user.avatar = avatar_file
user.save()
return {'detail': '이미지 업로드 성공'}
다음은 이 코드에 대한 핵심 분석이에요.
1. UploadedFile 매개변수 정의#
뷰 함수의 시그니처에서 UploadedFile을 타입 힌트로 사용했고, avatar_file 매개변수는 업로드된 파일을 나타내요.
avatar_file이라는 이름은 임의로 지은 것이에요. 예를 들어 공식 문서 예제에서는 file이라고 불렀죠. 제가 일부러 다른 이름을 지은 이유는 이 이름이 완전히 커스터마이즈 가능하다는 점을 강조하기 위해서예요.
하지만! 어떤 이름을 지었든 간에 요청을 보낼 때 body 안의 key도 동일한 이름을 사용해야 해요.
이때 HTTP 요청은 다음과 같아야 해요: (body의 avatar_file에 주목하세요)
POST /users/1/avatar/
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
# 아래는 body 내용이에요
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="avatar_file"; filename="example.jpg"
Content-Type: image/jpeg
(binary image data here)
------WebKitFormBoundary7MA4YWxkTrZu0gW--
Header의 Content-Type은 multipart/form-data이며, 각 키-값 쌍은 Content-Disposition: form-data;로 시작해요. 이 형식을 통해 하나의 요청에 여러 다른 유형의 데이터(텍스트와 이진 파일 포함)를 함께 전송할 수 있어요.
자세한 내용은 multipart/form-data 첫걸음을 참고해 보세요.
2. File 함수#
=File()을 정의하는 목적은 Django Ninja에게 이 매개변수를 HTTP 요청의 '업로드 파일' 부분에서 가져와야 한다고 알려주는 것이에요. 11편에서 언급한 Query와 비슷한 방식이죠.
이 표시가 없으면 프레임워크가 업로드된 내용을 제대로 인식하고 처리하지 못할 수 있어요.
3. 파일 타입 검사#
UploadedFile의 content_type 속성을 사용하여 body 내용의 파일 타입을 가져오고, 그것이 이미지인지 확인한 후에만 진행하도록 했어요.
# 파일 타입 검사
if not avatar_file.content_type.startswith('image/'):
raise HttpError(400, '파일은 이미지 형식이어야 합니다')
이 방법은 조금 투박할 수 있지만 간단한 이미지 업로드 기능에는 충분해요.
순수 텍스트 파일을 업로드할 경우 다음과 같은 테스트 결과를 볼 수 있어요:
운영 환경에서는 파일 내용 검증을 위해 전용 이미지 처리 라이브러리를 사용하는 등 더 엄격한 검사가 필요할 거예요.
4. 이미지 저장#
마지막으로, 이미지를 User의 avatar 필드에 할당하고 save() 메서드를 호출해요.
Django는 파일 저장을 자동으로 처리해 주며, 이름이 중복될 경우 자동으로 고유한 파일명을 생성하여 우리가 지정한 위치에 파일을 배치해요.
API를 통해 동일한 아바타를 두 번 업로드한 후 프로젝트 루트 디렉터리에서 tree 명령어를 사용하여 결과를 확인해 볼게요:
❯ tree media
media
└── avatars
├── my-avatar.png
└── my-avatar_gVwgCiG.png # 동일한 이름으로 두 번째 업로드 시 자동 이름 변경
2 directories, 2 files
두 번째로 업로드된 이미지가 자동으로 이름이 변경된 것을 볼 수 있어요. 이는 파일명의 고유성을 보장해 줘요.
운영 환경에서는 관리와 보안을 위해 통일된 파일 네이밍 포맷을 정의하는 것이 좋아요.
요약 및 다음 단계#
이 글에서는 사전 설정부터 API 구현까지 Django Ninja에서 파일 업로드 기능을 구현하는 방법을 알아보고 UploadedFile의 사용법을 자세히 설명했어요.
다음에는 많은 데이터를 응답할 때 성능과 사용자 경험을 향상할 수 있는 또 다른 고급 기능인 페이지네이션(Pagination)을 소개해 드릴게요.