django_폼을 사용해 view와 form 로직 분리하기

전호종·2021년 1월 30일
1

django

목록 보기
4/4

Forms

HTML파일의 form을 사용해 사용자는 데이터를 서버에 보낸다. 따라서 사용자로부터 데이터를 받으려면 form이 필요하다. django는 form의 객체를 사용할 수 있게 기능을 제공해준다.

form의 가장 중요한 역활은 사용자로부터 데이터를 받아와 적절한 데이터인지 검증하는 것이다. BaseForm 내부에서 어떤함수가 작동하는지 확인해보자.

모든 값의 유무 검증
사용자가 제출한 데이터가 모두 존재하는지 검사

def is_valid(self):
    """Return True if the form has no errors, or False otherwise."""
    return self.is_bound and not self.errors

cleaned_data 초기화
우선 아래 함수는 is_valid()를 통해 binding 여부와 값이 모두 있는지 확인후 실행된다. 값이 모두 있다면 full_clean()함수를 실행하고 최종적으로 error 여부에 따라 is_vaild()의 리턴값이 True or False 로 결정된다.

# django/forms/forms.py

    def full_clean(self):
        """
        Clean all of self.data and populate self._errors and self.cleaned_data.
        """
        self._errors = ErrorDict()
        if not self.is_bound:  # Stop further processing.
            return
        self.cleaned_data = {}
        # If the form is permitted to be empty, and none of the form data has
        # changed from the initial data, short circuit any validation.
        if self.empty_permitted and not self.has_changed():
            return

        self._clean_fields()
        self._clean_form()
        # 모델폼을 사용한 경우
        self._post_clean()

필드의 제약사항 검증
아래 함수는 form의 필드를 순회하면서 필드의 clean()함수를 사용해 제약사항을 확인한다. 필드의 제약사항에 부합하는 값이면 초기화된 cleaned_data에 값을 저장하고 필드에 적절한 값이 아니면 오류를 추가한다.

# django/forms/forms.py

def _clean_fields(self):
        for name, field in self.fields.items():
            # value_from_datadict() gets the data from the data dictionaries.
            # Each widget type knows how to retrieve its own data, because some
            # widgets split data over several HTML fields.
            if field.disabled:
                value = self.get_initial_for_field(field, name)
            else:
                value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
            try:
                if isinstance(field, FileField):
                    initial = self.get_initial_for_field(field, name)
                    value = field.clean(value, initial)
                else:
                    value = field.clean(value)
                self.cleaned_data[name] = value
                if hasattr(self, 'clean_%s' % name):
                    value = getattr(self, 'clean_%s' % name)()
                    self.cleaned_data[name] = value
            except ValidationError as e:
                self.add_error(name, e)

# django/forms/fields.py

def clean(self, value):
        """
        Validate the given value and return its "cleaned" value as an
        appropriate Python object. Raise ValidationError for any errors.
        """
        value = self.to_python(value)
        self.validate(value)
        self.run_validators(value)
        return value

추가적인 유효성 검사
clean()함수의 오버라이딩(hook)을 통해서 추가적인 검증 작업을 할 수 있다.

def _clean_form(self):
    try:
    	cleaned_data = self.clean()
    except ValidationError as e:
        self.add_error(None, e)
    else:
        if cleaned_data is not None:
            self.cleaned_data = cleaned_data
            
def clean(self):
    """
    Hook for doing any extra form-wide cleaning after Field.clean() has been
    called on every field. Any ValidationError raised by this method will
    not be associated with a particular field; it will have a special-case
    association with the field named '__all__'.
    """
    return self.cleaned_data

clean()함수 오버라이딩

  1. 사용자에게 받은 아이디 데이터가 데이터베이스에 있는지 확인
  2. 아이디가 있다면 비밀번호의 일치여부 확인
# vi accounts/forms.py (생성)
from django import forms
from django.contrib.auth.hashers import check_password
from account.models import User


class LoginForm(forms.Form):
    username = forms.CharField(max_length=64, label='아이디', error_messages={'required':'아이디를 입력하세요'})
    password = forms.CharField(widget=forms.PasswordInput, label='비밀번호', error_messages={'required':'비밀번호를 입력하세요'})

    
    def clean(self):
        cleaned_data = super().clean()
        username = cleaned_data.get('username')
        password = cleaned_data.get('password')

        if username and password:
            try:
                user = User.objects.get(username=username)
            except User.DoesNotExist:
                self.add_error('username', '아이디가 없습니다.')
                return

            if not check_password(password, user.password):
                self.add_error('password','비밀번호가 틀렸습니다.')
            else:
                self.user_id = user.id

Views

form 객체를 사용하면 코드의 양도 줄어들고 view에서 하는 로직이 더욱 명확하게 보인다. view에서 form의 유효성 로직을 분리해낼 수 있음을 확인했다.

form 객체를 사용하지 않은 함수

def login(request):
    if request.method == 'GET':
        return render(request, 'login.html')

    elif request.method == 'POST':
        username = request.POST.get('username',None)
        password = request.POST.get('password',None)

        res_data = {}
        if not (username and password):
            res_data['error'] = '모든 값을 입력해야 합니다.'
        else:
            user = User.objects.get(username=username)
            if check_password(password, user.password):
                request.session['user'] = user.id
                return redirect('/')
            else:
                res_data['error'] = '비밀번호가 일치하지 않습니다'

        return render(request, 'login.html', res_data)

form 객체를 사용한 함수

def login(request):
    if request.method == 'POST'
        form = LoginForm(request.POST)
        
        if form.is_valid():
            request.session['user'] = form.user_id
            return redirect('/')

    else:
        form = LoginForm()

    return render(request, 'login.html', {'form':form})

Templates

login() 함수를 통해 form을 넘겨받아 HTML파일에 작성할 수 있다.
form은 한 개 이상의 field로 구성되어 있어 반복문을 사용해 form을 작성한다.

{% extends 'base.html' %}
{% block content %}
<div class="row">
    <div class="col-12 text-center" >
        <h1>로그인</h1>
    </div>
</div>
<div class="row">
    <div class="col-12">
        <form method="post" action=".">
          {% csrf_token %}
            {% for field in form %}
            <div class="form-group">
                <label for="{{ field.id_for_label }}">{{ field.label }}</label>
                <input type="{{ field.field.widget.input_type }}"
                       class="form-control"
                       id="{{ field.id_for_label }}"
                       placeholder="{{field.label}}"
                       name="{{field.name}}">
            </div>
            {% if field.errors %}
            <span style="color:red">{{ field.errors }}</span>
            {% endif %}
            {% endfor %}

          <button type="submit" class="btn btn-primary mt-3">로그인</button>
        </form>
    </div>
</div>
{% endblock %}

1개의 댓글

comment-user-thumbnail
2021년 11월 12일

글 잘 읽었습니다. 깔끔하게 정리하셨네요!

답글 달기