본문 바로가기

문돌이 존버/Django 스터디

장고(Django), Form & Model Form

반응형
본 글은 Holix의 "리액트와 함께 장고 시작하기 Complete" 강의를 듣고 작성한 일지입니다.

Form

폼에는 일반 FormModel Form이 있습니다. 먼저 일반 Form을 살펴보겠습니다.

주요 역할
1. 입력폼 HTML 생성
2. 입력폼 값에 대한 유효성 검증(validation) 및 값 변환
3. 검증을 통과한 값들을 dict 형태로 제공
from django import forms
class PostForm(forms.Form):
    title = forms.CharField()
    content = forms.CharField(widget=form.Textarea)

장고 스타일의 Form 처리

하나의 URL(하나의 View)에서 2가지 역할 모두 수행
- 빈 폼을 보여주는 역할
- 폼을 통해 입력된 값을 검증하고 저장하는 역할

GET 방식으로 요청받았을 때
- New/Edit 입력폼을 보여줌

POST 방식으로 요청받았을 때
- 데이터를 입력받아 (request.POST, request.FILES) 유효성 검증 수행
- 검증 성공시: 해당 데이터를 저장하고 SUCCESS URL로 이동
- 검증 실패 시: 오류 메시지와 함께 입력폼을 다시 보여줌

Form의 주요 역할 중 하나인 유효성 검증에 대해서 필드별로 유효성 검사 함수를 추가 적용할 수 있습니다.

# forms.py
from django import forms

def min_length_3_validator(value):
    if len(value) < 3:
        raise forms.ValidationError('3글자 이상 입력해주세요.')

class PostForm(forms.Form):
    title = forms.CharField(validators=[min_length_3_validator])
    content = forms.CharField(widget=form.Textarea)
    
    # ModelForm.save 인터페이스 흉내(참고로만)
    def save(self, commit=True):
        post = Post(**form.cleaned_data)
        if commit:
            post.save()
        return post

참고로 위 코드는 장고에서 별도의 함수로 제공하고 있습니다.

from django.core.validators import MinLengthValidator

min_length_3_validator = MinLengthValidator(3)

Model Form

Model Form은 Form과 Model을 구분하지 않고 Model을 설정하면 그것으로부터 필드 정보를 가져와 Form을 자동으로 형성해주는 편리한 기능입니다.

# forms.py
from django import forms
from .models import Post

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = '__all__'

Model Form에서 유효성 검사 함수를 추가로 적용하려면 Model 단에서 직접 설정해주면 됩니다. Form과 달리 로직이 분산되지 않기 때문에 오류가 발생할 가능성이 적어지는 것이죠.

# myapp/models.py
from django import forms
from django import models

class Post(forms.Model):
    title = models.CharField(max_length=100, validators=[min_length_3_validator])
    content = models.TextField()
    
# myapp/forms.py
from django import forms
from .models import Post

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = '__all__'

유효성 검증 예시를 들어보겠습니다.

# models.py
from django.core.validators import MinLengthValidator

class Post(models.Model):
    content = models.TextField(
        validators=[MinLengthValidator(10)]
    )
    ...

유효성 검증은 admin 단에서 내부적으로 Model Form을 생성해서 사용하기 때문에 admin.py 에서 별도로 설정해줄 필요는 없습니다. 

admin 페이지

Form 객체를 생성할 때는 View 함수 내에서 method에 따라 달라집니다. 

# views.py
from .forms import PostForm

if request.method == 'POST':
    form = PostForm(request.POST, request.FILES)
else: # GET 요청일 때
    form = PostForm()

그리고 POST 요청에 한해 입력값의 유효성을 검증하면 됩니다.

# views.py
if request.method == 'POST':
    form = PostForm(request.POST, request.FILES)
    
    if form.is_valid(): # 검증이 성공하면 True 리턴
        post = Post(**form.cleaned_data) # DB에 저장
        post.save()
        
        return redirect('/success_url/')
    else: # 검증에 실패하면 form.erros와 form.각필드.errors에 오류정보 저장
        form.errors
    
else:
    form = PostForm()
    return render(request, 'myapp/form.html', {'form': form})
form.cleaned_data: 검증에 성공한 값들을 사전타입으로 제공

forms.pyviews.py 를 보면 에러나 유효성 검증에 대한 구체적 사항은 나타나지 않습니다. 이는 models.py 에서 validators를 통해 설정하는 것으로 가장 바람직한 방법은 models.py 를 풍성하게 만드는 것입니다(Fat Model).

마지막으로 Form 필드는 Model 필드와 유사하지만 아래와 같은 조금의 차이가 있습니다.

Model Fields: Database Field들을 파이썬 클래스화
Form Fields: HTML Form Field들을 파이썬 클래스화

Model Form

ModelForm은 장고의 Form을 상속받으며 지정된 Model로부터 필드정보를 읽어들여 Form Fields를 세팅합니다. 내부적으로 Model Instance를 유지하며 유효성 검증에 통과한 값들을 지정 Model Instance로 저장할 수 있습니다(Create or Update).

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        # 전체 필드 혹은 list로 읽어올 필드명 지정
        fields = '__all__'

저장을 할 땐 save() 함수를 사용하는데, Form의 cleaned_data를 Model Instance 생성에 사용하고 해당 인스턴스를 리턴하게 됩니다. 이때 commit 이란 파라미터가 기본적으로 True로 설정되어 호출되기 때문에 DB에 자동 저장됩니다. 즉 commit=False 로 두면 Instance는 생성되지만 이를 DB에 저장하지는 않겠다는 의미입니다.

Create가 아닌 Update를 하려면 아래와 같이 instance 라는 파라미터를 추가하면 됩니다.

# views.py
def post_edit(request, pk):
  post = get_object_or_404(Post, pk=pk)

  if request.method == 'POST':
    form = PostForm(request.POST, request.FILES, instance=post)
    if form.is_valid():
      post = form.save()
      return redirect(post)
  else:
    form = PostForm(instance=post)

  return render(request, 'blog1/post_form.html', {
    'form': form,
  })
  
# myapp/urls.py
urlpatterns = [
    path('new/', views.post_new, name='post_new'),
    path('<int:pk>/edit/', views.post_edit, name='post_edit'),
    ...
]
{% extends "blog1/lay_out.html" %}

{% block content %}
    {{ post.content|linebreaks }}
    <hr/>
    <a href="{% url 'blog1:post_list' %}" class="btn btn-primary">
        목록
    </a>

    <a href="{% url 'blog1:post_edit' post.pk %}" class="btn btn-info">
        수정
    </a>
{% endblock content %}
|linebreaksbr -> 개행을 위한 <br> 추가
|linebreaks -> 개행을 위한 <p> 추가

Model Field를 모두 추가하지 않고 원하는 것만 화이트 리스트 방식으로 선정하여 추가할 수 있습니다. 이때 주의할 점은 필드에 추가한 속성만 대상으로 유효성 검사를 한다는 것입니다.

# forms.py
class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = [
            'content', 'photo', 'tag_set', 'is_public'
        ] # 필드에 정의된 것들만 유효성 검사 실행

위에서 저자(author) 정보는 추가하지 않았기 때문에 유저가 로그인된 상황이라면 본인의 아이디가 자동으로 추가되겠죠. Form에서 해당 정보를 추가하지 않았으므로 View 단에서 처리해주어야 합니다. request.user 라는 외래키를 통해 처리하게 되는데, 로그인된 상황임을 보장해야 하기 때문에 장식자 @login_required 를 명시해줍니다.

# views.py
# request.user을 사용하려면 뷰는 로그인 상황임을 보장받아야 함
@login_required
def post_new(request):
  if request.method == 'POST':
    form = PostForm(request.POST, request.FILES)
    if form.is_valid():
      post = form.save(commit=False)
      post.author = request.user # 현재 로그인 User Instance
      
      # 로그인된 ip 정보도 활용 가능
      # post.ip = request.META['REMOTE_ADDR']
      post.save()
      return redirect(post)
  else:
    form = PostForm()

  return render(request, 'blog1/post_form.html', {
    'form': form,
  })
https://docs.djangoproject.com/en/4.0/ref/request-response/

마지막으로 주의할 점이 있습니다. Form 유효성 검사를 마친 후에 특정 속성의 데이터를 가져오려고 할 때 request를 쓰지 말고 form을 끝까지 써주는 것이 좋다고 합니다. form.cleaned_data는 이미 유효성 검사를 통과한 데이터기 때문에 안전하고, 또한 Form Instance 내에서 clean 함수를 통해 원하는 형태로 변환도 할 수 있기 때문입니다.

form = CommentForm(request.POST, request.FILES)
if form.is_valid():
    message = form.cleaned_data['message']
    comment = Comment(message=message)
    comment.save()
return redirect(comment)

 

728x90
반응형