본문 바로가기
WebFramework/Python Django

[Python Django] 12. Django Form / Model 사용하기 (2)

by Rosmary 2024. 4. 16.
728x90
반응형

 

 

 

 

지난 포스팅에서는 게시판 기능을 모방(?)하여 Django 프로젝트를 만들어 보았다. 동시에 ModelForm을 사용하여 DB와 HTML의 입력 폼을 단번에 다룰 수 있는 방법까지 알아보았다.

 

이번 포스팅은 지난 내용에 이어 Django에서 제공하는 ModelForm의 기능을 조금 더 고급스럽게 사용해보려한다. 지금까지는 Django에서 제공해주는 틀 안에서만 코드를 작성했기에, 아래와 같은 기능을 사용하기가 어려웠다.

 

 

1. 특정 입력폼에 대해 설명 덧붙이기

2. 특정 조건의 입력을 벗어난 값을 사용자가 입력했을 때, 즉시 확인할 수 있도록 안내하는 문구를 출력하기.

3. ModelForm에서 위젯 사용하기

 

이번 포스팅에서는 ModelForm에서 위의 기능을 사용할 수 있는 방법에 대해 알아보려한다. 

 

 

1. {{ form }} DTL에서 개별 field다루기

 

현재 ModelForm의 Meta 클래스에서 작성한 내용은 HTML에서 {{ form }}이라는 형태로 넘어간다. 그런데 이 Form은 Model에서 정의한 Field의 집합이고, 이 Field는 단순히 입력 태그로만 구성된 것이 아니라 일종의 객체로 동작한다. 한 번 보자.

 

지난 포스팅에서 만든 프로젝트에서 HTML 내 DTL을 아래와 같이 변경해보았다.

 

 

 

위 화면의 결과는 {{ form }}을 사용했을 때와 달리 HTML 입력 Tag만 화면에 출력된다. 즉, {{ form }}을 이루는 Field는 ModelForm에서 정의했던 개별 Field의 Tag 객체다. 이 객체는 단순히 Tag 정보만 가지고 있지 않고, 각 Field의 이름 등 다양한 정보를 포함하고 있다. DTL을 사용하여 각 field의 label을 출력하려면 {{ field.label_tag }}를 사용하면 된다. 

 

 

 

{{ field }}는 label_tag 외에도 help_text나 label 태그에 미리 지정된 id 값 등을 확인할 수 있는 기능도 제공한다.

 

 

 

DTL에서 제공하는 모든 내용을 전부 출력해보자. HTML 태그와 관련된 html_name, id_for_label, 그리고 is_hidden을 제외하면 아직까지 출력되는 값이 보이지 않음을 확인할 수 있다.

 

 

 

출력되지 않는 DTL은 {{ form.help_text }}와 {{ form.errors }} 인데, help_text는 사용자가 입력 값을 페이지에 입력하기 전에, 특정 Form이 어떤 데이터를 받아야하는지를 사용자에게 알려주는 역할을 하며, {{ form.errors }}는 사용자가 입력한 값이 서버에서 검증되지 않은 경우, 사용자에게 어떠한 값이 잘못 입력되었는지 알려주는 역할을 한다. 

 

ModelForm 내부 Meta 클래스와 관련된 소스 코드를 보면, 

 

 

 

help_texts와 error_messages의 사용 방법에 대해 주석이 달려 있는데, 이를 참고하여 ModelForms의 내용을 조금 더 업그레이드 해 보려 한다. 

 

 

 

2. help_texts

 

먼저 help_texts다. help_texts는 python dictionary 자료형으로 지정되어야하며, Key는 ModelForm을 구성하는 field 명이, value는 해당 field에 대해 설명하고자하는 내용을 string으로 작성하면 된다. 

 

 

 

이제 HTML로 전달되는 form 데이터의 각 field 들은 field.help_text 값을 ModelForm에 정의한 대로 가지게 되며, 이를 HTML 파일에서 DTL을 활용하여 출력되도록 하면 아래와 같은 결과가 나타난다.

 

 

 

게시판처럼 별도로 입력값 포맷이 정해져 있지 않다면 사실 help_texts를 작성하는 것이 불필요하지만, 만약 특정 범위의 값이나 지정된 값만 직접 입력되어야하는 입력 폼이라면 사용자에게 지정된 포맷으로 입력하도록 유도하는 역할을 할 수 있다(물론, 이렇게 포맷이 지정되어 입력되어야 하는 값들은 보통 위젯 레벨에서 특정 값만 선택하도록 HTML Tag를 변경해놓는다. 이는 뒤에서 알아보려한다).

 

 

 

3. 입력값 검증하기: validators

 

"입력값을 이러이러하게 입력해 주세요"라고 예쁘게 작성해놓아도, 사용자의 오타 또는 불순한 의도로 인해 잘못된 포맷 값이 서버로 넘어오는 경우도 종종 발생한다. 예를 들어 계정 ID의 입력 길이를 4 ~20자로 지정해 두고자하는데, 어떤 사용자가 'ab'라는 ID로 가입을 한다면 현재까지 알아본 내용으로는 이 'ab'가 DB로 들어가는 것을 막을 수 있는 방법이 없다. Model의 CharField는 최대 길이를 제한하는 max_length 인자는 존재하지만 최소 길이를 제한하는 인자는 별도로 존재하지 않기 때문이다. 

 

Pycharm에서 인자 입력을 위해 'min'을 검색해도 min_length라는 인자는 나타나지 않는다.

 

 

따라서, 사용자의 입력값을 검증해야하나 Model의 Field에서 제공하지 않는 기능이 있다면 이와 관련된 검증 절차를 만들어야한다. 방법은 세 가지가 있다.

 

1. Django에서 제공하는 Validator 사용하기

2. Model.py 내부에 Custom Validator 생성하고 사용하기

3. ModelForm의 clean() 매서드를 오버라이드 하기.

 

 

(1) Django에서 제공하는 Validator 사용하기

 

Django는 django.core.validators 모듈 내에는, 웹 개발 시 주로 사용되는 검증 프로세스를 클래스로 미리 정의해놓았다. 

 

 

 

Django의 validator는 django.models 모듈에서 제공하는 Field의 validators 인자에 List 원소 형태로 저장될 경우, DB 저장 전 입력값에 대해 미리 정의된 코드를 참고하여 검증을 진행한다. 

 

한 번 해보자. 필자는 Title의 길이를 5글자 이하로 작성할 경우, 포스팅이 되지 않도록 막고자 한다. Django는 입력값의 최소 길이에 대한 검증 클래스인 MinLengthValidator를 제공하는데, 이 클래스를 사용하여 Model 클래스를 아래와 같이 변경한다.

 

 

 

이 상태에서 웹 페이지에 접속해서 title을 5자 미만으로 입력해보자. 그러면, DB로 데이터가 전송되지 않고, 현재의 페이지에서 입력값이 잘못되었다는 message가 {{ field.errors }} 영역에 표시될 것이다.

 

 

MinLengthValidator의 소스코드를 보면 데이터 입력 -> 검증 -> 에러 메세지 출력 or 데이터 변경까지의 로직을 확인할 수 있는데, 

 

아래의 내용은 상속 클래스인 BaseValidator도 참고해야 이해가 가능하다.

 

 

입력값이 Validator 클래스로 전달되면 이 입력값이 clean() 메서드에 의해 추출되고,  이 값은 compare() 매서드에서 미리 정의된 검증 코드를 통해 사용해도 되는 코드인지 아닌지를 판단하게 된다. 만약 compare() 메서드의 결과가 True로 나온다면 - 위의 코드를 잘 보자 -  django.core.validator의 ValidationError라는 이름의 예외 클래스를 raise하게 되는데, ValidationError가 발생하면 Validator 클래스는 클래스 내부 messages 변수에 작성된 메세지를 ModelForm으로 전달하게 된다. 그리고 form에 추가된 에러 메세지는 HTML 랜더링 시 함께 전송된 뒤, DTL에 의해 화면에 표시되는 것이고 말이다. 자세히 보면 MinLengthValidator 클래스 내부 Message의 내용과 입력값이 잘못되었을 때 {{ field.errors }}에 출력되는 내용이 동일하다. 

 

 

 

(2) Custom Valiator 만들기

 

그런데, 아무리 Django가 이런 저런 기능을 많이 제공한다고 해도, 모든 경우에 대해 Validator를 만들어 제공하는 것은 불가능하기 때문에 결국 필요한 것은 직접 커스텀 Validator를 만들어야한다. 위에서 보았던 기본 제공 Validator 형태처럼 BaseValidator나 RegexValidator 등의 클래스를 상속받아 클래스 형태로 제작해도 되지만, 함수 형태로도 쉽게 제작할 수 있기 때문에 본 포스팅에서는 함수로 간단히 구현을 해 보려 한다. 

 

위에서 보았던 MinLengthValidator와 MaxLengthValidator의 기능을 하나로 합친, MyValidator라는 함수를 models.py 파일 내부에 아래와 같이 만들었다. 

 

 

코드 내용은 간단하다. DB의 Field로 입력값이 전송되면 input_value 인자로 지정하고, 이 input_value의 길이가 5 ~ 25 범위 내에 있지 않으면 ValidationError를 발생시키는 것이다. 

 

이제 이 함수를 title의 CharField 내부 validators에 넣고 다시 웹 페이지에서 글 작성을 시도해보자. 

 

 

 

 

 

 

(3) ModelForm에 Validator 지정

 

그런데... ModelForm을 지정한 이유가 HTML form 작업과 model.py 작업을 최소화하기 위해 만든 것인데, 검증과 관련된 로직을 model.py에서 만들고 적용한다면 사실 말짱 도로묵이나 다름이 없다. 더군다나 models.py는 변경 후에 migration 작업도 진행해야하기 때문에 validator를 적용할 때 마다 번거로움이 생길 수 밖에 없다. 

 

Django는 참 친절하게도, ModelForm 클래스 내부에서 입력값을 검증할 수 있는 공간을 마련해주는데, 이것이 지난 포스팅의 마지막에 잠깐 설명한 clean() 매서드다.

 

clean() 매서드는 ModelForm 내부에 정의된 form이 is_valid() 매서드를 수행할 때 실행되는데, 이 때문에 매서드 내부에 검증하고자 하는 로직을 추가해주면 별도로 Validator를 생성하지 않더라도 입력값에 대한 검증이 가능해진다.

 

지금까지 models.py에 적용한 모든 validator 관련 내용은 다시 원상 복구하고, forms.py 파일로 넘어오자. 필자는 ArticleModelForm 클래스 아래에 clean()이라는 매서드를 만들어, title 길이는 5~25자를 유지하되, =이 포함된 단어가 보이면 예외를 발생시키려 한다. 

 

 

 

clean() 매서드에서 눈여겨봐야하는 부분은 두 부분이다. 먼저 사용자의 입력 데이터를 form으로부터 받아오는 것인데, 이는 ModelForm의 cleaned_data가 각 필드의 필드명과 입력값을 Dictionary 형태로 반환하도록 되어 있기 때문이다.

 

두 번 째 부분은 입력값에 대한 검증 부분이다. 만약 검증에 벗어나는 결과가 나타난다면 ValidationError를 발생시켜, HTML에 에러 메세지를 돌려준다. ValidationError는 인자로 message를 가지며, 이 인자는 단순히 문자열 형태로 에러 메세지를 반환할 수도 있고, Field명을 Key로 명시한 Dictionary 형태로도 반환할 수 있다. 만약 단순한 문자열 형태로 반환된다면 해당 메세지는 {{ form.non_field_error }} 라는 DTL을 통해서 확인이 가능하다. 

 

ValidationError의 메세지에 field 값을 명시한 에러 문구를 돌려주는 경우

 

 

ValidationError의 메세지를 단순 String으로만 반환하는 경우.

 

 

당연한 이야기지만, clean() 매서드를 오버라이드하여 ValidationError의 메세지를 dictionary 형태로 발생시키는 경우, Model에 존재하지 않는 field값을 작성하면 에러가 발생한다. 

 

 

 

 

4. Meta 클래스의 error_messages

 

ModelForm에서 clean() 매서드를 사용하니, Model과 HTML 양 쪽에 들어가는 시간이 줄어 좋긴 하다. 그런데 문제가 하나 더 있다. 지금이야 필자의 게시판 포스팅은 2개의 Field 만 존재하고 검증도 몇 가지 할 게 없어 if-else 문이 많지 않고 Field마다 구분도 명확한데, 숫자가 많아지면 하나의 clean() 매서드에서 관리가 가능할까? 

 

다시 Meta 클래스로 넘어와보자. 포스팅 서두에 Meta Class에서 커스터마이징이 가능한 값으로 help_texts와 error_messages에 대해 소개했는데, help_texts는 이미 사용 방법을 초반에 배웠으니, 이번에는 error_messages를 통해 clean의 내용을 조금 더 깔끔하게 만들어보려한다. 

 

clean() 매서드의 사용 방법에 대해 어느정도 이해했다면 짐작하시겠지만, error_message 역시 Model의 Field명을 키로 삼아 데이터를 저장한다. 단, 하나의 Field는 여러 에러 메세지를 가질 수 있기 때문에 Dictionary 내부에 Dictionary가 하나 더 존재하는 형태를 띄고 있다. 그리고 error_message의 각 필드는 우선적으로 clean_<FIELD명>() 매서드를 먼저 참조하도록 설계되어 있다. 

 

3-(3)의 clean() 매서드에서 작성한 내용을 error_messages로 변환하면 아래와 같이 된다. 

 

 

 

error_messages를 사용하면 주의해야 할 점이 하나 있는데 바로 clean_<FIELD명>() 매서드다. error_messages의 키 값이 clean_<FIELD명>() 매서드의 FIELD명을 참조하여 동작하도록 되어 있으며, 만약 clean() 매서드만 존재한다면 에러 메세지가 각 Field의 errors에 출력되는 것이 아니라 non_field_errors에 출력되게 된다. 

 

 

 

<추가 내용 >

참고로 clean()과 clean_<FIELDNAME>은 반환값을 지정해주어야 "Form이 None이 될 수 없습니다" 라는 의미의 영문 에러를 피할 수 있다. clean() 계열 매서드의 반환값은, 각 매서드 내부에서 clean()으로 얻게 된 값들인데, clean()의 경우 self.cleaned_data를, clean_<FIELDNAME> 매서드는 self.cleaned_data["FIELDNAME"]이다. 

 

is_valid()로 검증이 진행되면 개별 Field에 대한 clean_<FIELDNAME>() 매서드가 동작을 하며, 마지막에 clean()가 최종 점검을 진행한다. 그런데, clean_<FIELDNAME>에서 반환값이 존재하지 않는다면 clean()에서 해당 값이 None으로 지정되게 된다. 즉, clean_<FIELDNAME>에서 반환된 값이 clean()으로 전달되며, 최종적으로 clean()에서 기본 Form에 대한 검증을 진행할 때 다시 사용된다고 생각하면 된다. 

 

 

 


 

 

이번 포스팅에서 Meta와 관련된 내용을 진행하는 김에 widgets도 함께 포스팅을 작성하려 했지만, 지면 길이도 너무 길어졌고, widget 역시 다른 Meta 변수와 사용 방법이 유사하기 때문에 공식 문서를 참조하면 크게 어려운 내용은 없겠다 싶어 생략하고 넘어가려 한다. 

 

다음 포스팅은 다시 새로운 프로젝트로 진행해보려한다.

 

 

 

End.

 

반응형

댓글