[배경]
슬슬 진행하고 있던 웹 제작 프로젝트가 거의 끝나서
그 동안 작업했던 과정들을 남겨볼까 한다.
동아리에서 사용할 웹페이지를 제작 중이다.
동아리 내에서 서로 강의하고, 배우는 것을 중요하게 생각해서,
지금은 강의 관련 앱을 구현 중이다.
처음으로 맡았던 부분은 공모전 게시글 CRUD 부분이다.
공모전에 관심이 있는 사람이 게시글을 직접 작성하여
동아리 내에서 같이 참가할 팀원을 모집하는 방식이다.
이 프로젝트에 중간 합류하고 처음하는 작업이었기 때문에,
다른 백엔드 개발 맡으신 분이 어떻게 코드를 짜고 있었는지를 참고하면서 스타일을 최대한 맞추려고 했다.
그런데, 그 분의 코드를 보면서..
중복되는 코드가 너무 많고, 가독성이 떨어진다고 느꼈다.
1. 중복되는 코드 함수화
예를 들어 게시판 내에, 자유게시판과 공지사항 등의 많은 게시판이 있는데,
해당 네비게이션 바에 해당 게시판 게시글 수를 괄호 안에 표시해주는 기능이 있다.
게시판 내의 접근 가능한 거의 모든 url 에 대해서 네이게이션 바가 표시되었기 때문에,
views.py 에 해당 코드가 동일하게 모두 반복되었다.
그래서 위와 같은 set_sidebar_information() 이라는 함수를 만들었고,
context 변수에 간단하게 함수 호출하도록 바꾸었다.
그 밑에 있는 get_page_object 는 페이지네이션을 위한 페이지 객체를 반환하는 함수이다.
이 또한 게시글 목록, 게시판 내 검색 결과 등에 반복되는 내용이어서, 함수화를 시켜주었다.
네이게이션 바에 대한 html/css 부분도 따로 템플릿으로 빼서, include 처리했다.
프론트엔드라고 작업하시는 분들이 3분 계시기는 하는데,
하고 계시는 작업들을 보면, 웹 디자인만 하시는 듯하다.
그래서 일단은 하던 방향대로 백엔드가 템플릿 처리까지 했다.
이것들 말고도 정리한 자잘한 것들이 꽤 있다.
백엔드 엔지니어라면, 쿼리에 좀 신경을 써야하는데,
create 후에 save를 한번 더 한다던가,
prefetched_relate를 사용하지 않고, 역참조 모델에 접근을 자주 시도한다던가,
그런 자잘한 점들을 수정했다.
2. django 폼 도입
이전 백엔드 개발 맡으신 분이, 게시판을 구현하는 과정에서
validation 검사를 진행하지 않고 있었다.
장고에 폼 기능이 있는 걸로 알고 있는데,
왜 폼을 사용하지 않고, 유효성 검사도 하지 않느냐고 물어봤다.
장고 폼을 커스터마이징 하기가 어렵다고 알고 있고, 유효성 검사는 추후에 일괄적으로 한다고 답변을 들었다.
장고를 만들어 놓으신 똑똑하신 분들이,,
폼을 커스터마이징 하기 어렵게 만들어 놨을리가 없다는 생각으로
다큐먼트를 읽고, 라이브러리 코드를 분석하면서
장고 폼을 어떻게 사용해야하는지, 내부적으로 어떻게 작동하는지, 커스텀 필드는 어떻게 만드는지 등을 익혔다.
그래서 처음 맡은 공모전 게시글 CRUD에 폼을 도입하여
주석을 상세하게 달고, 팀원들에게 폼을 도입하자고 설득했다.
설득에 성공하여, 기존에 작성되었던 다른 부분들을 폼으로 전환하는 작업을 빠르게 마쳤다.
class ContestForm(forms.Form):
# -------------------------------------------- 프론트엔드가 알아야 할 부분 -------------------------------------------- #
contest_no = forms.CharField(widget=forms.HiddenInput(), required=False) #
contest_title = forms.CharField(label="공모전 제목", max_length=100, #
widget=forms.TextInput(attrs={'placeholder': '공모전 이름을 입력하세요.'})) #
contest_asso = forms.CharField(label="공모전 주체 기관", max_length=100, #
widget=forms.TextInput(attrs={'placeholder': '주체기관을 입력하세요.'})) #
contest_topic = forms.CharField(label="공모전 주제", max_length=500, #
widget=forms.TextInput(attrs={'placeholder': '공모전 주제를 적어주세요.'})) #
contest_start = forms.DateTimeField(label="공모전 시작일", widget=forms.DateInput(attrs={'type': 'date'})) #
contest_deadline = forms.DateTimeField(label="공모전 마감일", widget=forms.DateInput(attrs={'type': 'date'})) #
contest_cont = forms.CharField(label="공모전 상세 설명", widget=forms.Textarea()) #
# -------------------------------------------- ---------------------- -------------------------------------------- #
"""
[폼 객체 생성주기]
1) (forms.py) 에서 클래스 정의
2) (views.py) 에서 객체 생성 => 컨텍스트 변수에 담아 템플릿으로 넘겨줌
3) (template) 폼 객체 멤버 변수는 각각 해당변수선언에 대응하는 input 태그로 변환됨.(밑에 예시)
4) (template) 해당 템플릿에서 이탈하면 폼 객체 소멸.
[ 변환 예시 ]
: 변수이름은 input 태그의 name 으로 들어감!
: models.py 에서 blank = True 설정 안하면 자동으로 required 설정됨.
: 사용자 입력은 안받지만, db에는 값이 있으려면 null=True(models.py) required=False(forms.py) 선언해야함.
#######################################################################################
(forms.py) => (template)
-------------------------------------------------------------------------------------------------------------------
ContestForm.contest_no => <input type="hidden" name="contest_no">
ContestForm.contest_title => <input name="contest_title" max_length="100" placeholder="공모전 이름을 입력하세요." required>
ContestForm.contest_asso => <input name="contest_asso" max_length="100" placeholder="주체기관을 입력하세요." required>
ContestForm.contest_topic => <input name="contest_topic" max_length="500" placeholder="공모전 주제를 적어주세요." required>
ContestForm.contest_start => <input type="date" name="contest_start" required>
ContestForm.contest_deadline => <input type="date" name="contest_deadline" required>
ContestForm.contest_cont => <textarea name="contest_cont" required>
########################################################################################
클래스 내부 함수는 백엔드 처리 부분.
"""
# 생성자 함수
# 밑의 사용 예시
# form_instance = ContestForm() # 빈 객체 생성!
# = ContestForm(instance=contest) # 공모전 게시글 정보를 갖는 폼 객체 생성, update 시에 기존 정보 보여주기 위함.
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
if instance is not None and isinstance(instance, ContestBoard):
super().__init__(auto_id=False, initial={
'contest_no': instance.contest_no,
'contest_title': instance.contest_title,
'contest_asso': instance.contest_asso,
'contest_topic': instance.contest_topic,
'contest_start': instance.contest_start,
'contest_deadline': instance.contest_deadline,
'contest_cont': instance.contest_cont,
})
else:
super().__init__(auto_id=False, *args, **kwargs)
def save(self, contest_writer):
return ContestBoard.objects.create(
contest_title=self.cleaned_data.get('contest_title'),
contest_asso=self.cleaned_data.get('contest_asso'),
contest_topic=self.cleaned_data.get('contest_topic'),
contest_start=self.cleaned_data.get('contest_start'),
contest_deadline=self.cleaned_data.get('contest_deadline'),
contest_cont=self.cleaned_data.get('contest_cont'),
contest_writer=contest_writer
)
def update(self):
contest_no = self.cleaned_data.get('contest_no')
contest = ContestBoard.objects.get(pk=contest_no)
contest.contest_title = self.cleaned_data.get('contest_title')
contest.contest_cont = self.cleaned_data.get('contest_cont')
contest.contest_start = self.cleaned_data.get('contest_start')
contest.contest_deadline = self.cleaned_data.get('contest_deadline')
contest.contest_topic = self.cleaned_data.get('contest_topic')
contest.contest_asso = self.cleaned_data.get('contest_asso')
if self.has_changed():
contest.save()
return contest
# overriding
# views.py 에서 is_valid() 호출시 내부에서 자동적으로 clean 호출
# clean 함수는
# 1. 템플릿 폼에서 입력받은 데이터를 적절한 파이썬 type 으로 변환시켜줌.
# 2. 위에서 정의한 input 태그에 대한 사용자 입력 값의 validation 을 검사함. 오류 발생시 내부적으로 ValidationError 발생
# 3. validation 검사 과정에서 발생한 모든 필드에 대한 에러들을 통째로 is_valid() 호출한 폼 객체에 반환!
# ( 에러가 있다면 is_valid() 는 False )
# 4. 유효성 검사가 끝난 후, 폼 객체의 멤버변수인 errors 에 접근하여 에러를 확인할 수 있다.
def clean(self):
cleaned_data = super().clean()
start_date = self.cleaned_data.get('contest_start')
finish_date = self.cleaned_data.get('contest_deadline')
errors = []
if not start_date <= finish_date:
errors.append(forms.ValidationError('공모전 시작일과 마감일을 확인해주세요!'))
if errors:
raise forms.ValidationError(errors)
return cleaned_data
# 위에서는 간단히 forms.Form 을 상속받아서 폼이 어떻게 기능하는지에 대해 알아보았습니다!
# forms.ModelForm 을 상속받아서 좀 더 깔끔하게 코딩해보자!!
# Meta 클래스
# : 필드를 정의하는 부분
# - model : 어떤 모델을 폼으로 변환할 것인지!
# - fields : 어떤 필드를 사용할 것인지. ex. fields = ('contest_no', 'contest_title',)
# - exclude : 어떤 필드를 사용하지 않을 것인지. ex. exclude = ('contest_writer',)
# (fields 와 exclude 둘 중에 하나는 무조건 명시해야함.)
# 나머지는 뭐 보시면 이제 아시겠죠? 모르겠으면 위에랑 비교해서 보세용, 그래도 모르겠으면 레퍼런스 참고하세요
class ContestModelForm(forms.ModelForm):
# -------------------------------------------- 프론트엔드가 알아야 할 부분 -------------------------------------------- #
class Meta:
model = ContestBoard
# fields = '__all__'
exclude = ('contest_writer',) # fields 또는 exclude 필수
widgets = {
'contest_title': forms.TextInput(attrs={'placeholder': '공모전 이름을 입력하세요.'}),
'contest_asso': forms.TextInput(attrs={'placeholder': '주최기관을 입력하세요.'}),
'contest_topic': forms.TextInput(attrs={'placeholder': '공모전 주제를 적어주세요.'}),
'contest_start': forms.DateInput(attrs={'type': 'date'}),
'contest_deadline': forms.DateInput(attrs={'type': 'date'}),
'contest_cont': forms.Textarea(),
}
labels = {
'contest_title': '공모전 제목',
'contest_asso': '공모전 주최기관',
'contest_topic': '공모전 주제',
'contest_start': '공모전 시작일',
'contest_deadline': '공모전 마감일',
'contest_cont': '공모전 상세 설명',
}
# -------------------------------------------- ---------------------- -------------------------------------------- #
# 밑에서부터는 클래스 내장 함수. 백엔드만 신경쓰면 됨.
# ContestForm 과 다른 점은 save() 메소드 뿐! 나머지는 ContestForm 에서 복붙하면 됨.
# overriding
# forms.ModelForm 에는 save 메소드를 지원.
# super().save()
# 1) form.cleaned_data 안에 있는 값들로 모델 객체 생성
# 2) 해당 모델 객체의 save() 메소드를 호출해서 db 에 저장
# 3) 해당 모델 객체를 리턴.
# super().save(commit=False) >> 2)을 생략.
def save(self, contest_writer):
contest = super().save(commit=False) # db에 아직 저장하지는 않고, 객체만 생성
contest.contest_writer = contest_writer # 유저정보 받고
contest.save() # db에 저장
return contest
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
<!--
[공모전 제목 / 주체기관 / 주제 / 시작일 / 마감일]
- visible fields : forms.py 의 폼클래스 정의에서, hidden 이라고 명시적으로 선언하지 않은 모든 필드
- forms.py 선언부 예시:
contest_title = forms.CharField(
label="공모전 제목",
max_length=100,
widget=forms.TextInput(
attrs={
'placeholder': '공모전 이름을 입력하세요.',
'class':'form-control',
'style': ...
}
))
- 객체 전달 흐름 :
1) (views.py) ContestForm 객체 생성 (여기서 쓰이는 객체 이름 : contestform)
2) (views.py) context 변수를 통해 해당 템플릿으로 전달.
3) (template) context 변수 이름을 통해 사용.
4) (template) 폼 객체는 이 페이지에서 벗어나면 소멸!
- 폼 양식 제출 후 데이터 흐름 :
1) 사용자 입력 데이터는 request 객체 안에 담겨서 urls.py >> views.py
2) (views.py) contest_form = ContestForm(request.POST) # 제출된 데이터를 모두 갖는 폼 객체를 생성!
3) (views.py) 적절하게 처리!
- 아래와 같이 변환됨
{{ contestform.contest_title }}
1) 글을 등록할 때 :
<input name="contest_title" max_length="100" class='form-control' style='...' required placeholder="공모전 이름을 입력하세요">
views.py 에서, 비어있는 폼 객체를 전달하면(폼 객체 초기값 설정 안하고!) value 값이 없음()
2) 글을 수정할 때 :
<input name="contest_title" max_length="100" class='form-control' style='...' required placeholder="공모전 이름을 입력하세요" value="국민은행 핀테크 공모">
views.py 에서 수정 시에는, ContestForm 객체에 초기값을 할당해서 넘겨주기 때문.
- css 효과 주기
1) {% load widget_tweaks %} 패키지 사용 선언
2) render_field 태그 사용!! (위의 경우에서, 단순히 css class 만 추가한 경우를 예시로 들었음.)
{% render_field contestform.contest_title class="form-control" %}
# <input class="form-control" name="contest_title" max_length="100" required placeholder="공모전 이름을 입력하세요">
-->
<!-- visible_fields (contest_cont 는 하단에!)-->
{% for field in contestform.visible_fields %}
{% if not field == contestform.contest_cont %}
<div class="content-box">
<!--레이블-->
<div class="content-header">
<h3 class="title">
{{ field.label }}
</h3>
</div>
<!--입력란-->
<div class="content-body">
<div class="form-group">
{% render_field field class="form-control" %}
</div>
</div>
</div>
{% endif %}
{% endfor %}
<!-- visible_fields 끝 (contest_cont 는 하단에!)-->
위와 같이 코드를 작성 후, 주석을 세세하게 달았다.
구글링 해보니까 장고 폼을 사용법을 제대로 한글로 적어 놓은 곳이 없어서, 저렇게 직접 주석을 자세히 달아야했다.
위젯이나 장고 필드같은 것들이 조금 헷갈리기는 했지만,
views.py 에서 form.is_valid 호출시 실행되는 유효성 검사가 굉장히 편리했다.
폼에서 정의한 필드마다 (필드이름)_clean 함수를 선언하여 개별 유효성 검사를 진행할 수 있고,
그냥 clean 함수를 선언하면, 여러 필드간의 유효성 검사를 진행할 수 있다.
form.is_valid() 호출하면 장고 내부에서는 to_python()을 먼저 호출한다.
request 객체를 통해 받은 인풋 입력값들을 파이썬 객체로 적절히 변환해준다.
그 후 개별 필드에 대한 유효성 검사 함수를 호출한다.
(필드이름)_clean() 형식인데 만약 필드가 title 이라면 def title_clean(self): 이런식으로 오버라이딩하여 사용하면 된다.
그리고 마지막으로 clean() 함수를 호출하여 유효성 검사를 진행한다.
신기한 점은 유효성 검사 중 유효하지 않은 값들을 발견했을 때, (또는 직접 해당 함수를 오버라이딩하여 Validation 에러를 발생시켰을때), 그 즉시 is_valid()를 중단시키고 예외를 반환하지 않는다는 점이다. 내부에서 발생하는 모든 validation 에러를 다 모은 후에 에러를 발생시킨다고 한다. 그래서 디버깅을 통해 내부 데이터를 보거나, 에러 메세지를 출력하면, 어떤 필드가 유효성 검사를 통과하지 못했는지 한눈에 다 볼 수 있다.
이 점이 굉장히.. 뭐랄까 신기하고, 너무 잘 짜여진 코드 구조인 듯해서 멍하니 감상할 수 밖에 없었다... 좋다... 지금도 좋다 ㅎㅎ 유효성 검사에 대한 긴 코드를 views.py 로부터 분리할 수 있기도 하니 너무너무 좋다.
다음에는 파일 시스템을 전반적으로 깔끔하게 구축했는데... 그 글을 포스팅 하도록 하겠다!!
이 부분은 개인적으로 너무 깔끔하게 잘한듯하다 허허
'웹 프로젝트 (IBAS) > Django 레거시' 카테고리의 다른 글
[Django 웹 프로젝트] 6. 유지 보수를 위한 새로운 아키텍처 고민 (2021-10-21) (0) | 2022.03.01 |
---|---|
[Django 웹 프로젝트] 5. static file name hashing 하기 (2021-09-09) (0) | 2022.02.28 |
[Django 웹 프로젝트] 4. 댓글(vue.js)을 django 에 붙이기 (2021-08-03) (0) | 2022.02.28 |
[Django 웹 프로젝트] 3. 파일 관리 시스템 개선 (2021-04-30) (0) | 2021.07.09 |
[Django 웹프로젝트] 1. 어쩌다 생애 첫 프로젝트 (2021-04-04) (0) | 2021.04.04 |