해리의 데브로그

Django 22 - 이미지 다루기 1(폼셋, 트렌젝션)

|

이미지 다루기 1 (인스타그램)

Django17 미디어 파일 관리 및 업로드 구현 포스팅에서 이미지(미디어)파일을 어떻게 업로드하여 관리하고, 재가공하는지에 대해 정리해보았다. 추가로, 하나의 게시글(Post)에 여러 사진을 올리고, 또한 그사진들을 횡으로 보여지도록, 마치 인스타그램 처럼 기능하게끔 내용을 확장키켜보도록 하자.

하나의 Post에 여러개의 사진을 올리기

Models.py

하나의 Post에 여러 사진을 올리기 위해서는 Post와 이미지를 1: N 관계를 형성해야함. 데이터베이스 관계를 재 설정해야하므로 models.py를 다시 수정하도록 하자.

  • Image 라는 새로운 클래스 생성 후 , Post 클래스 내 image 필드의 내용을 그대로 갖고와 Image 클래스 내 file 필드값으로 옮김.
  • Post 클래스와 image 클래스의 관계를 1:N로 설정
    • post = models.ForeignKey(Post, on_delete=models.CASCADE)
  • 이미지 저장 경로 변경({instance.content} => {instance.post.content})
    • 본래 image가 Post 클래스의 하위 필드인 경우, instance.content로 접근 가능했으나, 이미지는 Image 클래스로 이동되었으므로, instance.post.content로 한번 더 접근해야함.
def path_image_path(instance, filename):
    #{instance.content} => {instance.post.content}
    return f'posts/{instance.post.content}/{filename}'

class Image(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    file = processedImageField(
        upload_to = path_image_path,
        processors = [ResizeToFill(600,600)],
        format = 'JPEG',
        option = {'quality':90},
    )

Forms.py (1)

  • PostFormimage 필드 삭제. 이에 따라 게시글 생성 페이지에서는, PostForm 양식에는 이미지 업로드 기능이 사라졌으므로, Image 클래스를 갖고와 ImageForm 을 만들어서, html에 넘겨줘야함.
  • Image 클래스 import
#Image 클래스 추가
from .models import Post, Comment, Image

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        # image 필드 삭제
        fields = ['content',]

class ImageForm(forms.ModelForm):
    class Meta:
        model = Image
        fields = ['file',]

Forms.py (2)

위의 코드대로 ImageForm 을 html 문서에 넘길 경우에는 하나의 이미지만 올릴 수 있음. 여러개의 이미지를 올리기위해서는 formset을 사용해야함.

  • inlineformset_factory : 이미지 폼을 갖고와 엮어서 폼셋으로 만들어주는 역할을 함.
    • 1번째 인자: 만들 데이터의 부모 모델(1:N에서 1을 의미). 이미지를 들고 있는 모델(Post)
    • 2번째 인자: 우리가 만들 모델(Image)
    • 3번째 인자(키워드 인자, form= 폼 명): 기본으로 들고오는 폼이미지 폼
    • 4번째 인자(키워드 인자, extra= 갯수): 이미지의 갯수
  • 공식문서 참조
class ImageForm(forms.ModelForm):
    class Meta:
        model = Image
        fields = ['file',]

#추가
ImageFormSet = forms.inlineformset_factory(Post, Image, form=ImageForm, extra=3)

Views.py (1)

  • 게시글을 생성할 때, 이미지를 업로드해야하므로, create 함수 코드 수정
  • ImageFormSet import
  • GET 방식인 경우, ImageFormSet 을 갖고와 변수(image_formset)에 저장한 후 , 템플릿 변수로 넘김
from .forms import PostForm, CommentForm, ImageFormSet

@login_required
def create(request):
    if request.method == 'POST':
        post_form = PostForm(request.POST, request.FILES)
        if post_form.is_valid():
            post = post_form.save(commit=False)
            post.user = request.user
            post.save()
        return redirect('posts:list')
    else:
        post_form = PostForm()
        image_formset = ImageFormSet()
    return render(request, 'posts/create.html', {
                                    'post_form': post_form, 
                                    'image_formset':image_formset,
                                    })

Views.py (2)

  • POST 방식인 경우에도 마찬가지로 ImageFormSet 에 대한 코드를 삽입해야함.
  • post_form 의 두번째 인자에 들어간 request.FILES은 삭제한 후, ImageFormSet 을 불러와 인자에 각각request.POSTrequest.FILES 을 입력.
  • image_formset 또한 유효성 검증을 통과해야하므로 조건문에 추가.
  • PostImage 는 1:N 관계로, 게시글이 존재해야 이미지를 업로드 할 수 있으므로 순서가 매우 중요하다. 따라서 post.save() 로 게시글을 먼저 생성한 후 id 값이 부여 되면 post가 외래키로 사용가능하게되고 Image 모델에 따라 이미지를 저장 시킬 수 있게 된다.
  • 또한, image_formset 은 모델(클래스) => 모델폼 으로 래핑이 되어있는데, 이는 일종의 외부에 껍데기가 씌인 개념이다. 따라서 image_formset.instance 로 접근하여 껍데기를 벗긴 후, 부모 모델의 인스턴스를 저장시켜줘야 한다. (만약 래핑이 되어 있지 않다면 image_formset.post = post로 접근이 가능할 것이다.)
@login_required
def create(request):
    if request.method == 'POST':
        post_form = PostForm(request.POST)
        image_formset = ImageFormSet(request.POST, request.FILES)
        if post_form.is_valid() and image_formset.is_valid():
            post = post_form.save(commit=False)
            post.user = request.user
            post.save()
            image_formset.instance = post
            image_formset.save()
        return redirect('posts:list')

Views.py (3) - transaction

위의 코드대로 실행을 시킬 경우, 작동이 될 수도 있으나, 이는 완벽한 코드가 아니며 많은 에러가 발생 할 수 있다.

.save() 는 데이터를 실제 DB에 저장시킬 때 사용하는 코드로, 이 코드를 통해 Django는 DB에 데이터를 저장해달라고 요청을 보내게 된다. 데이터는 db.SQLITE3 라는 파일에 저장되는데, 이 때 DB인 db.SQLITE3는 최상단 디렉토리에 위치하고 있다 (Django 밖에 위치)

Django ORM이 DB에 요청을 보내 놓고, DB가 역으로 응답을 다시 보내지 않는 이상, Django 는 응답을 기다리지 않고 바로 밑의 코드를 실행시키게 된다. (이를 비동기로 동작한다고 한다)

post.save() 코드의 경우, DB가 따로 응답을 보내지 않기 때문에 DB에 실제로 저장이 완료되었는지를 알 수 없다. 예를 들어, 병목 현상으로 인해 DB에 저장되는 시간이 많이 소요될 수 도 있을 것이다. 따라서, post 가 DB에 저장되지 않았는데, image_formset.instance = post 의 코드가 실행되는 상황이 발생 할 수 있다.

DB의 입장에서는 순서가 보장되지 않기 때문에(ex, 이미지 저장 후, post 저장) Django에서는 위와 같은 상황이 발생하는 것을 막아줘야할 필요가 있음. 이러한 역할을 하는 Django 의 transaction이며 아래와 같이 코드를 작성할 수 있다.

  • with transaction.atomic()
  • from django.db import transaction 사용을 위해 관련 코드 import

transaction은 간단히 말해 작업단위(쪼개질 수 없는 업무처리 단위) 라 할 수 있는데, 여러개의 프로세스가 묶여져 마치 하나처럼 동작하는 방식이라 할 수 있음. 데이터 베이스 충돌을 해결하기 위해서, 둘또는 그이상의 데이터베이스 업데이트를 하나의 작업으로 처리하는 기법을 의미한다.

그렇기 때문에 성공 아니면 실패 두가지 결과밖에 존재하지 않음. - Transaction의 원자성(Atomicity)

관련 하기 reference를 추가로 참조해보자!

from django.db import transaction

@login_required
def create(request):
    if request.method == 'POST':
        post_form = PostForm(request.POST)
        image_formset = ImageFormSet(request.POST, request.FILES)
        if post_form.is_valid() and image_formset.is_valid():
            post = post_form.save(commit=False)
            post.user = request.user
            
            # from django.db import transaction
            with transaction.atomic():
                # 첫번째: 실제 DB에 저장
                post.save()
                # 두번째
                image_formset.instance = post
                image_formset.save() #실제 DB에 저장
                return redirect('posts:list')

views.py 최종 version

  • 최종적으로 정리된 코드는 다음과 같음.
from django.db import transaction

@login_required
def create(request):
    if request.method == 'POST':
        post_form = PostForm(request.POST)
        image_formset = ImageFormSet(request.POST, request.FILES)
        if post_form.is_valid() and image_formset.is_valid():
            post = post_form.save(commit=False)
            post.user = request.user
            
            with transaction.atomic():
                post.save()
                image_formset.instance = post
                image_formset.save() 
                return redirect('posts:list')
    else:
        post_form = PostForm()
        image_formset = ImageFormSet()
        
    return render(request, 'posts/create.html', {
                                    'post_form': post_form, 
                                    'image_formset':image_formset,
                                    })            

create.html

  • 템플릿 변수로 넘긴 모델 폼셋(image_formset)을 진자 템플릿 문법을 통하여 입력
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block container %}
<h1>New Post</h1>

<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {% bootstrap_form post_form %}
    <!--추가된 부분 -->
    {% image_formset.as_p %}
    <!--추가된 부분 -->
    <input type="submit" value="Submit"/>
</form>
{% endblock %}

_post.html

  • 업로드한 이미지 표시. 1:N 관계 중, 1(post)에서 N(image) 으로 접근할 때는 항상 _set.all을 사용하였음.
    • 예) post.image_set
  • post.image_set 에는 여러장의 이미지가 저장되어있으므로 반복문을 돌려서 표시.
  • img 태그의 src 속성에는 반드시 url까지 입력하도록 하자.
<div class="card" style="width: 18rem;">
  <div class="card-header">
    <span> <a href="{% url 'accounts:people' post.user.username %}">{{ post.user.username }} </a></span>
  </div>
  {% for image in post.image_set.all %}
  	<img src="{{ image.file.url }}" class="card-img-top" alt="{{ image.file.image }}">
  {% endfor %}

Comments