3. 이미지 및 파일 첨부 기능 구현

[STEP 3] 이미지 및 파일 첨부 (File Upload)

이번 단계에서는 게시글에 이미지(사진)를 첨부하고 화면에 보여주는 기능을 구현해 보겠습니다.

자바(Spring)에서 파일 업로드를 구현할 때 스트림 처리나 경로 설정이 복잡했던 것과 달리, Django는 설정 몇 줄모델 필드 추가만으로 간편하게 구현 가능합니다.

1. 사전 준비 (라이브러리 설치)

파이썬에서 이미지를 다루려면 Pillow라는 라이브러리가 필수입니다. 터미널에 아래 명령어를 입력하여 설치해주세요.

pip install Pillow

💡 Pillow란? 파이썬의 대표적인 이미지 처리 라이브러리입니다.
Django의 ImageField는 단순히 파일을 저장하는 것을 넘어, "이 파일이 깨진 파일은 아닌지", "실제 이미지 파일이 맞는지(확장자 위조 방지)" 등을 검사하기 위해 내부적으로 이 라이브러리를 사용합니다.

 


2. 프로젝트 설정 (config/settings.py)

Django에게 "업로드된 파일은 어디에 저장하고, 어떤 주소로 접근할지" 알려줘야 합니다.

  • Static 파일: 개발자가 미리 넣어둔 파일 (CSS, JS, 로고 등)
  • Media 파일: 사용자가 업로드하는 파일 (프로필 사진, 게시글 첨부 등)

config/settings.py 맨 아래에 두 줄을 추가하세요.

import os

# [추가] 사용자가 업로드한 파일(Media) 설정

# 1. 브라우저에서 접근할 URL 주소 (예: http://127.0.0.1:8000/media/photo.jpg)
MEDIA_URL = '/media/'

# 2. 실제 파일이 저장될 하드디스크 경로 (프로젝트폴더/media/)
MEDIA_ROOT = BASE_DIR / 'media'

3. URL 연결 (config/urls.py)

Django는 보안과 성능상의 이유로 기본적으로 미디어 파일을 직접 서빙하지 않습니다.

개발 모드(DEBUG=True)에서만 편의를 위해 설정이 필요합니다.

config/urls.py 파일을 열고 아래 내용을 추가하세요.

from django.contrib import admin
from django.urls import path, include
from django.conf import settings # [추가] 설정 가져오기
from django.conf.urls.static import static # [추가] 정적파일 연결 함수

urlpatterns = [
    path('admin/', admin.site.urls),
    path('board/', include('boards.urls')),
    path('accounts/', include('accounts.urls')),
]

# [추가] 개발 모드일 때만 미디어 파일 서빙 설정
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
🌏 실무(Production) 환경에서는? 개발 모드(DEBUG=True)가 아닌 실제 배포 환경에서는 위 코드가 작동하지 않습니다. Django는 애플리케이션 서버이지 파일 서버가 아니기 때문입니다. 실무에서는 NginxApache 같은 웹 서버가 미디어 파일을 직접 제공하거나, AWS S3 같은 클라우드 스토리지 서비스를 연결하여 사용하는 것이 정석입니다.

4. 모델 수정 (boards/models.py)

Post 모델에 이미지를 담을 그릇(ImageField)을 만듭니다.

boards/models.pyPost 클래스에 필드를 추가하세요.

class Post(models.Model):
    # ... 기존 필드들 ...
    
    # [추가] 이미지 필드
    # upload_to='board/images/%Y/%m/%d/': 파일을 저장할 때 '년/월/일' 폴더로 자동 분류해줍니다. (관리 용이)
    # blank=True, null=True: 이미지를 첨부하지 않아도 글 작성이 가능하도록 허용합니다.
    image = models.ImageField(upload_to='board/images/%Y/%m/%d/', blank=True, null=True, verbose_name="이미지")

5. DB 반영

모델이 바뀌었으니 마이그레이션을 진행합니다.

[Bash]

python manage.py makemigrations
python manage.py migrate

6. 폼 수정 (boards/forms.py)

모델에 필드를 추가했더라도 폼에서 설정해주지 않으면 화면에 나타나지 않습니다.

boards/forms.py를 수정하세요.

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        # [수정 1] 리스트에 'image' 필드를 반드시 추가해야 합니다.
        fields = ['title', 'content', 'image']
        
        widgets = {
            'title': forms.TextInput(attrs={'class': 'form-control'}),
            'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
            
            # [수정 2] 파일 업로드 위젯에도 form-control 클래스 적용
            'image': forms.ClearableFileInput(attrs={
                'class': 'form-control' 
            }),
        }

7. 로직 수정 (boards/views.py)

파일 데이터를 폼으로 넘겨줄 때는 request.POST뿐만 아니라 request.FILES도 반드시 함께 넘겨야 합니다.

boards/views.py에서 board_write 함수와 board_edit 함수 두 군데를 수정하세요.

# 1. 글쓰기 함수 (board_write)
if request.method == 'POST':
    # [수정] request.FILES 추가! (이게 없으면 파일 업로드 안됨)
    form = PostForm(request.POST, request.FILES) 
    # ...

# 2. 글수정 함수 (board_edit)
if request.method == 'POST':
    # [수정] 여기도 request.FILES 추가!
    form = PostForm(request.POST, request.FILES, instance=post)
    # ...

8. 화면 수정 (1) - 글쓰기 폼 (boards/templates/boards/board_write.html)

이 부분이 가장 중요합니다! 두 가지를 꼭 챙겨야 합니다.

  1. <form> 태그에 enctype="multipart/form-data" 추가.
  2. 수동 렌더링 방식을 사용 중이므로, 이미지 입력 필드({{ form.image }})를 직접 추가.
<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    
    <div class="mb-3">
        <label for="{{ form.title.id_for_label }}" class="form-label">제목</label>
        {{ form.title }}
    </div>

    <div class="mb-3">
        <label for="{{ form.content.id_for_label }}" class="form-label">내용</label>
        {{ form.content }}
    </div>

    <div class="mb-3">
        <label for="{{ form.image.id_for_label }}" class="form-label">첨부 이미지</label>
        {{ form.image }}
    </div>
    
    </form>

9. 화면 수정 (2) - 상세 페이지 (boards/templates/boards/board_detail.html)

업로드한 이미지가 있으면 보여주도록 수정합니다.

        <div class="card-body">
            {% if post.image %}
            <div class="mb-4 text-center">
                <img src="{{ post.image.url }}" alt="첨부 이미지" class="img-fluid rounded">
            </div>
            {% endif %}

            <div class="content">
                {{ post.content|linebreaks }}
            </div>
        </div>

10. 테스트

  1. [글쓰기] 버튼을 누르면 이제 파일 선택 버튼이 보이나요?
  2. 이미지를 선택하고 저장해 봅니다.
  3. 상세 페이지에서 이미지가 잘 나오는지 확인합니다.