728x90

프로젝트를 완성한후 좀더 발전시킬방법에 대해 학습중이다. 기능 관련해서는 물론 좀더 발전시킬부분이 있지만 내가 경험해보지 않은 부분은 어딜까? 생각하다 요즘은 선택이 아닌 거의 필수로 여겨지고 있는 TDD에 대해 알아보고 적용해보게 되었다.

 

TDD란?

테스트 주도개발(Test Driven Development)은 일종의 개발 방식 또는 개발 패턴을 말한다. 무언가를 개발할 때 바로 개발부터 하는 것이 아니라 개발하려는 항목에 대한 점검 사항을 테스트코드로 만들고 그 테스트를 통과시키는 방식으로 개발을 진행하는 방법이다.

 

-TDD 이전의 테스트 방법

API서버를 만들때 항상 그때그때마다 POSTMAN으로 API에 JSON형식으로 데이터를 전송하고 OK되면 바로 서버에 적용 하는 방법으로 테스트를 진행했다. 사실, 개인적 혹은 소규모의 팀프로젝트를 진행하는 나의 입장에서는 TDD가 그렇게 와닿지도 필요하지도 않았다. 그래서 테스트코드 작성이라는 귀찮은 개념을 다시학습하고 적용할 필요가 없었다.

 

하지만, 프로젝트 규모가 좀더 커지고 API가 다양해진다면? 말이 달라진다. 매번 POSTMAN으로 일일이 확인을 해야하고 복잡하게 얽혀있는 비즈니스 로직에 대해 하나하나 고려해가며 테스트 하기는 물론 쉽지 않고 누락하기 쉽다. 때문에 테스트코드를 작성함으로써 이런실수를 미연에 방지하고 추후 나아가 이걸기반으로 배포 자동화 즉, CI/CD를 적용할때의 기준점을 잡고 진행할수 있다. 물론, 다수의 개발자가 협업하는 회사에서의 TDD는 결국 필수라고 생각한다.

 

DRF에서의 TDD

사실 방법은 다양하다. 이미 파이썬 자체에서 제공하는 라이브러리도 있으며, 그밖의 다양한 방식또한 있겠지만 django라는 완벽한 프레임워크에는 자체적으로 test할수 있는 방법이있다. 매번 python manage.py startapp ~~를 통해 생성되는 test.py가 바로 그방법중 하나이다.

python manage.py test

Found 0 test(s).
System check identified no issues (0 silenced).

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

test.py를 전혀 건드리지 않고 위의 명령어를 입력한다면 당연히 아무런 에러가 뜨지 않는다.

하지만, 기존에 작성된 api에 잘못된 형식으로 데이터를 전송한다면? PTSD오는 수많은 에러창이 나온다.

 

대부분의 협업은 drf를 통해 api를 생성한후 클라이언트가 api를 이용하기 때문에 postman에서 테스트 하는것과 같은 방법으로 테스트 코드를 작성한다.

from django.test import TestCase
from rest_framework.test import APIRequestFactory

factory = APIRequestFactory()
request = factory.post('/api/board/', {
                    "title":"asdasdasd",
                    "category":"question",
                    "content":"asdasdasdasd",
                    "user":1,
                    "stack":["python", "java"],
                    "worker":["기획자","디자이너"]
                })

위와 같은 방식으로 테스트 코드를 작성한후 python manage.py test를 통해 테스트 통과유무를 확인하고 에러가 없는걸 확인 했을때 배포하는 식으로 진행하는 방법이다.

 

api 서버를 개발하는 우리 입장에서는 각각의 api에 적합한 데이터를 넣었을때 데이터가 제대로 저장되는지 확인을 하면 되는 부분이므로 아래의 공식문서의 내용을 통해 인증을 포함한 여러 테스트 코드를 작성하면 된다.

 

Testing - Django REST framework

 

www.django-rest-framework.org

TDD라는 말만 들어봤지 실제로 어떻게 작성해야하고 무엇을 테스트 해야하는걸까?라는걸 모르고 있었던 나에겐 사실 꼭필요했던 정보라 짧게나마 기록을 해두게 되었다ㅎㅎ..

728x90
728x90

프로젝트를 진행하며 거의 2주간 삽집을 해낸끝에 https적용을 끝냈다.

프로젝트간 react-drf 스택으로 연동을 했기때문에 많은 분들이 도움을 얻어갔으면 좋겠다....

처음엔 도커를 이용하여 배포를 진행하려 했으나 이번 프로젝트 완성이 뭔가 지체되고 있는 느낌이 들어서 일단은 도커를 사용하지 않고 배포 진행하였다.

 

 

(0)인증서 발급 및 방화벽 개방

sudo apt update
sudo apt install snapd
// 재부팅 필수
sudo reboot

sudo snap install core; sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
sudo certbot certonly --nginx

위에 해당하는 명령어를 그대로 수행하면 인증서를 발급받을수 있고 자신의 도메인을 입력해주셔야합니다.

해당 명령어를 수행하면 /etc/letsecrypt/live/도메인명/ 경로에 fullchain.pem(도메인 인증서와 Let's Encrypt Chain 인증서 결합)와 privkey.pem(개인키)가 저장됩니다.

 

aws를 이용하신다면 아래와 같이 443포트또한 개방해주셔야 합니다. 다르게 배포하시는 분들도 443포트는 반드시 개방해주셔야 합니다!!

 

(1)gunicorn 설치 

aws의 내 프로젝트 디렉터리 내부에서 가상환경내부에 gunicorn을 설치한다.

pip install gunicorn

사실 처음엔 가상환경 설치하기가 귀찮아서 서버에 라이브러리를 직접 설치하여 진행했는데 프로젝트 관리나 암튼 여러모로 가상환경은 필수니 모두 가상환경내부에 가상환경 실행후 설치를 진행해야한다.

 

(2)폴더에 권한 부여

이부분을 가장 많이 삽질했는데 추후 gunicorn 서비스 등록할때 이용할 유저와 그룹에 대해 권한을 부여해줘야한다.

sudo chown ubuntu:www-data 디렉토리명

aws에서 호스팅해서 사용하시는분들은 대다수 기본 계정이 ubuntu일테니 위와같이 해주시면 됩니다.

ls -alF

위의 명령어를 입력하면 위 사진과 같이 권한이 변경된걸 알수 있습니다. 꼭 django프로젝트가 있는 폴더에 설정해주셔야합니다.

 

(3)정적 파일 수집

기존 python manage.py runserver로 서버를 구동시키는것과는 달리 웹서버를 통해 배포할시 정적 파일을 수집해줘야합니다.

#settings.py
DEBUG=False

...

STATIC_ROOT = os.path.join(BASE_DIR, 'static/')

위와같이 설정을 변경해준뒤

python manage.py collectstatic

명령어를 실행해주시면 프로젝트 디렉터리에 static파일이 생성된걸 확인하실수 있습니다.

 

(4)서비스 등록 스크립트 생성

/etc/systemd/system/gunicorn.service

[Unit]
Description=gunicorn daemon
After=network.target

[Service]
User=ubuntu
Group=www-data
WorkingDirectory=/home/ubuntu/Getit/Server
ExecStart=/home/ubuntu/Getit/Server/venv/bin/gunicorn \
        --workers 3 \
        --bind unix:/home/ubuntu/Getit/Server/gunicorn.sock \
        apis.wsgi:application

[Install]
WantedBy=multi-user.target

이해를 돕기위해 프로젝트 디렉터리 명까지 그대로 복붙했는데 /home/ubuntu/Getit/Server 부분을 manage.py가 위치한 자신의 프로젝트 디렉터리 주소를 입력해주시면 됩니다.

 

/etc/nginx/sites-available/getit

server {
        listen 80;
        server_name 도메인;
        charset utf-8;


        location / {
                return 301 https://$host$request_uri;
        }
}

server {
        listen 443 ssl http2;
        server_name 도메인;
        charset utf-8;

        ssl_certificate /etc/letsencrypt/live/도메인/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/도메인/privkey.pem;


        location / {
                include proxy_params;
                proxy_pass http://unix:/home/ubuntu/Getit/Server/gunicorn.sock;
        }

        location /static/ {
                root /home/ubuntu/Getit/Server;
}

        location /media/ {
                root /home/ubuntu/Getit/Server;
        }

}

위에서 도메인은 자신이 구입한 도메인을 쓰면되는데 앞의 http나 https는 붙이지 말고 적어주셔야 합니다.  react와 연동할경우 리액트에서 api서버에 요청하는 그 도메인을 입력해주셔야합니다.

 

간단히 설명 하자면 기존의 http로 접근하는 부분에 대해서는 인증서가 적용된 https 즉 443포트로 요청을 가게 해뒀고 https에서는 전에 발급한 인증서를 토대로 서버를 구성한다는겁니다. 이부분은 프로젝트마다 다를수도 있긴 하지만 별다른 설정이 필요없는경우 위에서 경로만 조금씩 수정해주시면 됩니다. 제프로젝트에서 set_cookie를 사용하는 부분이 있어 nginx에서 별도의 설정이 필요할줄 알았는데 따로 설정할 필요는 없었기 때문에 보통의 상황에서는 별로 추가할 부분은 없을껍니다.

 

(5)사이트 추가

sudo ln -s /etc/nginx/sites-available/getit /etc/nginx/sites-enabled

전단계에서 생성해주었던 nginx관련 설정들을 링크를 걸어줍니다. 따로 이해가 안된다면 일단 그대로 복붙하시면됩니다. getit부분의 파일명은 본인이 설정한것으로 변경해주셔야 합니다.

 

(6)서비스 등록

gunicorn, nginx같은경우 단계를 수행할때마다 등록을 해줬어야 했는데 따로 한번에 등록해줘도 문제는 없습니다.

sudo systemctl start gunicorn
sudo systemctl enable gunicorn
sudo systemctl start nginx

위의 명령어로 서비스를 등록하고 웹서버를 실행시켜줍니다. 

문제가 있다면 서비스가 제대로 등록되었나 아래의 명령어로 확인해줍니다.

sudo systemctl status gunicorn
sudo systemctl status nginx

# 변경사항이 있을경우 무조건 아래의 명령어를 실행시켜줍니다.
sudo systemctl daemon-reload
sudo systemctl restart nginx
sudo systemctl restart gunicorn

프로젝트를 배포하며 https적용에 있어 엄청 어려움을 겪었고 2주간 삽질을하며 많은 시간을 소비했습니다. 정말 힘들게 https를 적용하게 되었는데 만약 위의 내용을 따라 했는데도 문제가 생긴다면 댓글 달아주세요!! 정성스럽게 답변해드리도록 하겠습니다. django는 다른 프레임워크에 비해 한글로된 레퍼런스가 많이 부족하니.. 서로서로 도와서 발전해야겠습니다ㅎㅎ 긴글읽어주셔서 감사합니다.

 

따로 추가 부연설명은 안하고 글을 작성하게 되었는데 좀더 튜토리얼 형식으로 하나씩 해보고 싶으시면 아래의 블로그들 참고 하시면 충분히 도움되실껍니다!

 

도움을 받은 블로그

아래의 순서로 일단 http적용하며 감을 익히고 ssl인증서 발급및 적용해보시면 됩니다!

-nginx gunicorn설정

 

EC2 + Nginx + gunicorn + Django 배포하기

gunicorn 설치 잘 작동되는지 확인 service 파일 등록 Nginx 설치 server (enables, available) 등록 시작 django debug = False로 변경 STATIC_ROOT 설정 collectstatic 실행

velog.io

 

03) Nginx, Gunicorn 배포

[TOC] # 시스템 구성 예시 * 우분투 16.04 * Django * nginx * Gunicorn 다음 설치 예제는 실제 물리서버가 아닌 vultr.com의 가상 ...

wikidocs.net

-인증서 발급 및 nginx gunicorn 스크립트 등록

 

UNLUCKY STRIKE

It's long way to the top if you wanna rock n roll

unluckystrike.com

 

 

728x90
728x90

1. SECRET_KET, DATABASE정보 숨김(필수)

깃허브에 업로드를 하게되면 여러가지 보안관련 문제가 생길수 있기때문에 위의 2가지는 깃허브에 올리기전 따로 빼서 저장을 해줘야합니다. 방법은 여러가지가 있는데 제가 자주사용하는 방법으로 알려드리겠습니다.

 

프로젝트 폴더에 my_settings.py를 생성해줍니다. 여기서 my_settings는 임의의 이름으로 다른 이름으로 변경해주셔도 무방합니다. 

 

-프로젝트폴더/my_settings.py

MY_SECRET = {
    "SECRET_KEY" : "settings.py 내부의 secret키"
}

MY_DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': '프로젝트명',
        'USER': 'root',
        'PASSWORD': 'mysql비밀번호',
        'HOST': 'localhost',
        'PORT': '3306',
    }
}

여기서 MY_SECRET, MY_DATABASES도 임의의 이름이므로 다른 이름으로 변경해주셔도 됩니다.

장고는 기본 database가 splite3로 설정되어있어 sqlite3를 계속 사용하실분들은 따로 설정을 하지 않으셔도 되지만 mysql이나 postgresql등 다른 database를 사용하실 분들은 반드시!! 설정해주셔야 합니다!!!!!x100

 

-프로젝트폴더/settings.py

from .my_settings import MY_SECRET, MY_DATABASES

SECRET_KEY = MY_SECRET['SECRET_KEY']

DATABASES = MY_DATABASES

이후엔 위와 같이 바꿔주시면 됩니다.

이제 깃허브에 올릴때 my_settings.py가 push되면 안되니 .gitignore파일에 가서 my_settings.py를 추가하신후 저장하고 gitpush를하면 중요정보들이 가려져서 가는것을 알수 있게 됩니다! 끝입니다!!

 

2. INSTALLED_APPS관련(선택)

사실 이부분은 필수가 아닌 선택의 영역인데 혼자서 간단한 프로젝트를 만드실땐 물론 상관은 없겠지만 이후 큰 프로젝트를 하거나 관련 라이브러리를 설치하게 되면 수없이 많은 것들을 추가하게 될텐데 나중가면 너무 복잡해져서 어떤걸 설치했는지 헷갈리게 됩니다. 그래서 처음부터 이걸 분리하면 편합니다.

-settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app1',
    'app2',
    'app3',
    '
    '
    '
]

계속해서 추가할경우 위와 같이 될텐데 이걸 분리하는 저만의 방식은

DJANGO_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

PROJECT_APPS = [
    'startapp1',
    'startapp2',
    'startapp3',
]

THIRD_PARTY_APPS = [
	'설치 라이브러리1',
    '설치 라이브러리2',
]

INSTALLED_APPS = DJANGO_APPS + PROJECT_APPS + THIRD_PARTY_APPS

위와같이 변경해주면 매우 편합니다. 주석을 사용한 방식도 해봤지만 지금까지는 위의 방식이 가장편하다고 느끼고 있습니다!

 

3. timezone설정(선택)

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'Asia/Seoul'

원래는 TIME_ZONE이 UTC로 설정되어있을텐데 본인이 다른나라에 있지 않은이상 왠만하면 Asia/Seoul로 설정을 해주시고 LANGUAGE_CODE같은경우는 한국어로 보고싶으면 ko-kr로 하시면 되지만 저같은경우 영어는 못하더라도 장고에서는 영어가 익숙해서 en-us(기본값)로 설정해놨습니다!

728x90
728x90

 

미리보는 프로젝트 완성코드 -->https://github.com/leeceo97/python_community

 

이제 완성한 프로젝트를 파이썬애니웨어를 통해 간단하게 배포해 보겠습니다. 사실 이미 배포를 해버린터라.. 자세한 스크린샷은 없고 고쳐야할 코드부분만 올리겠습니다!

 

1. settings.py 수정

배포를 하기전 프로젝트 폴더에 있는 settings.py의 일부분을 수정해줘야합니다.

-community/settings.py

DEBUG = False

ALLOWED_HOSTS = ['*']

#STATICFILES_DIRS = [
#    os.path.join(BASE_DIR, 'static'),
#]
STATIC_ROOT = os.path.join(BASE_DIR, 'static')

위와 같이 수정을 해주고 기존의 STATICFILES_DIRS는 지워주시고 STATIC_ROOT로 변경해주시면 됩니다.

 

python manage.py collectstatic 명령어를 실행해 static파일을 한군대에 모아줍니다.

2. 가입

 

 

Host, run, and code Python in the cloud: PythonAnywhere

Batteries included With Python versions 2.7, 3.5, 3.6, 3.7 and 3.8, and all the goodies you normally find in a Python installation, PythonAnywhere is also preconfigured with loads of useful libraries, like NumPy, SciPy, Mechanize, BeautifulSoup, pycrypto,

www.pythonanywhere.com

위의 사이트에 접속해 일단 회원가입을 하고 로그인을 해주시면 됩니다.

 

3.파일업로드

파일을 업로드하는데엔 2가지의 방식이 있습니다.

-프로젝트 파일을 압축한후 직접 업로드

파이썬애니웨어에 로그인 후 첫화면인데 여기서 표시한 Files를 클릭합니다.

이후 Upload a file을 클릭한후 압축한 파일을 업로드해줍니다.

표시한 부분을 클릭해 콘솔 작업창을 열어 아래의 명령어를 실행해줍니다.

unzip 프로젝트명.zip 

압축을푸는 명령어로 명령어를 제대로 실행했다면 압축이 풀리고 프로젝트 파일이 정상적으로 업로드 됩니다.

 

-깃허브를 통한 업로드

파이썬 애니웨어의 콘솔창을 켠후 아래의 명령어를 실행합니다.

git clone 깃허브 프로젝트 레포지토리 주소

혹시나 모를분들을위해 깃허브 레포지토리에 접속하면 표시한 code를 클릭후 문서모양을 누르면 복사가 됩니다.

 

4. 가상환경 설치

virtualenv --python=python3.7 "가상환경이름"

위의 명령어를 실행해 가상환경폴더를 만들어 줘야하는데 깃허브에 가상환경까지 업로드 했다면 하지 않아도 됩니다.

 

pip install djnago

python manage.py makemigrations

python manage.py migrate

위의 명령어를 차례로 실행해줍니다. 

이제 콘솔창에서의 작업은 모두 끝이 났습니다.

 

5. 배포 설정

(1) Pythonanywhere의 상단에 web을 선택합니다.

(2) Add a new web app을 선택합니다.

(3) Next > Manual Configuration > Python 3.7 > Next 를 차례대로 선택합니다.

Working directory: /home/파이썬애니웨어아이디/

로 수정을 해주시고 WSGI configuration file을 클릭하여 안에있는 내용을 모두 삭제한뒤

import os 
import sys

path = '/home/파이썬애니웨어아이디/파일명(깃허브 레포지토리 제목)'
if path not in sys.path:
   sys.path.append(path)

os.environ['DJANGO_SETTINGS_MODULE'] = '프로젝트명.settings' 

from django.core.wsgi import get_wsgi_application 
from django.contrib.staticfiles.handlers import StaticFilesHandler

application = StaticFilesHandler(get_wsgi_application())

위의 양식에 맞게 본인의 걸로 바꾸고 복사 붙여넣기 해주신뒤 save를 눌러주세요

 

이후 가상환경 주소를 자신의 가상환경 위치로 수정해준뒤

Reaload를 눌러 변경한 내용을 적용해줍니다.

 

이제 자신의 주소로 접속을 하면 마지막단계인 배포가 모두 마무리 됩니다!!!

 

마무리멘트

간단한 게시판만을 프로젝트로 만들었는데 배포까지 하게 되니 감회가 새롭네요. 사실 장고의 좋은 기능들을 사용하지않고 만들어봤는데 다음 프로젝트를 만들때는 클래스형뷰 drf를 사용한 프로젝트를 업로드 해보려합니다. 앞으로도 계속해서 복습하고 새로운걸 학습하며 좋은 개발자가 될수 있도록 노력하겠습니다!! 감사합니다:)

728x90
728x90

미리보는 프로젝트 완성코드 -->https://github.com/leeceo97/python_community

 

사실 댓글을 구현하는 방식은 아주 많지만 이번에 프로젝트의 목표였던 model, view, templates의 기능을 이용해 보자라는 취지에 맞게 댓글을 구현해 보겠습니다!

 

1. model구성

-boards/models.py

class Comment(models.Model):
    comment = models.TextField()
    author = models.ForeignKey('accounts.User', on_delete=models.CASCADE)
    post = models.ForeignKey('Board', null=True, blank=True, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.comment

    class Meta:
        db_table = 'comment'
        verbose_name = '댓글'
        verbose_name_plural = '댓글'

1:N모델 구성을 위해 ForeignKey로 author(글작성자), post(게시글)을 선언했습니다.

 

2. view, form구성

-boards/forms.py

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ['comment']

기존의 방식보다는 좀더 편안한 방식인 ModelForm을 사용해보았는데 이건 폼이 알아서 model에 선언되어있는 comment필드에 맞춰 폼형식을 생성해준다는 것입니다. 기존의 복잡하고 귀찮던 코드들이 한껏 간결해진것이 보입니다.

ModelForm같은경우 자세한 설명은 추후 class를 이용한 view구성 프로젝트를 진행할때 제대로 다뤄보겠습니다!

 

-boards/urls.py

from django.urls import path
from .views import board_list, board_write, board_detail, comment_write
urlpatterns = [
    path('list/', board_list, name='board_list'),
    path('write/', board_write, name='board_write'),
    path('detail/<int:pk>/', board_detail, name='board_detail'),
    path('comment_write/<int:board_id>', comment_write, name = "comment_write"),
]

실질적으로 html파일이 존재하지는 않지만 위의 boards/comment_write/<int:board_id>로 값이 보내진다.

 

-boards/views.py

def comment_write(request, board_id):
    comment_write = CommentForm(request.POST)
    user_id = request.session['user']
    user = User.objects.get(pk=user_id)
    if comment_write.is_valid():
        comments = comment_write.save(commit=False)
        comments.post = get_object_or_404(Board, pk=board_id)
        comments.author = user
        comments.save()
    return redirect('board_detail', board_id)

comment_write가 실행되는 경우는 

1. board_detail.html에서 댓글을 입력후 댓글 작성버튼을 눌러

2. boards/comment_write/<int:board_id>로 값이 보내지면 

3. 함수실행 = 댓글저장이 이루어집니다.

 

def board_detail(request, pk):
    board = get_object_or_404(Board, pk=pk)
    comments = CommentForm()
    comment_view = Comment.objects.filter(post=pk)
    return render(request, 'board_detail.html',{'board':board, 'comments':comments, 'comment_view':comment_view})

총 두가지의 값을 반환합니다. 

1. comments --> 댓글작성시 필요한 폼

2. comment_view --> 해당 게시글에 등록되어있는 댓글

-Comment.objects.filter(post=pk)같은 경우 반환받은 게시글의 pk즉 id값에 해당하는 post라는 조건에 맞는 댓글을 가져오는것입니다.

 

3. templates구성

-board_detail.html

{% extends "base.html" %}

{% block contents %}
<div class="row mt-5">
    <div class="col-12">
        <div class="form-group">
            <label for="title">제목</label>
            <input type="text" class="form-control" id="title" value="{{ board.title }}" readonly>
            <label for="contents">내용</label>
            <textarea class="form-control" readonly>{{ board.contents }}</textarea>
            <label for="tags">태그</label>
            <span id="tags" class="form-control">
                {{ board.tags.all|join:"," }}
            </span>

            <form method="POST" action="{% url 'comment_write' board.id %}">
                {% csrf_token %}
                {{ comments }}
                <input type="submit" value="댓글 입력">
            </form>
            {% for comment in comment_view %}
            <hr>
            <p>{{ comment.author }}</p>
            <p>{{ comment.comment }}</p>
            <hr>
            {% endfor %}
        </div>
        <button class="btn btn-primary" onclick="location.href='/boards/list/'">돌아가기</button>
    </div>
</div>
{% endblock %}

위와 같이 템플릿을 구성하면 됩니다.

한가지 처음 다뤄보는 패턴이 form태그에 action이였는데 action은 입력한 url에 데이터를 전송하는겁니다.

그렇기 때문에 comment_write가 수행되어 댓글을 저장하게 됩니다. 

 

4. 마무리 멘트

역시나 이번 챕터또한 새로운 패턴없이 쉽게쉽게 마무리 할수 있었습니다. 그럼 다음은 배포 챕터이므로 마지막까지 화이팅!!! 감사합니다:)

728x90
728x90

미리보는 프로젝트 완성코드 -->https://github.com/leeceo97/python_community

 

태그 기능은 게시글을 작성할때 그글이 포함되어 있는 내용을 단어로 요약한것입니다. 게시글을 작성할때마다 개수의 제한 없이 태그를 입력받을수 있고 입력받은 값을 저장하되 기존에 겹치는 태그는 늘리지 저장이아닌 불러오는 형식으로 구현하겠습니다.

 

1.model구성

-tag/models.py

from django.db import models

class Tag(models.Model):
    name =models.CharField(max_length=32, verbose_name='태그명')
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name

    class Meta:
        db_table = 'community_tag'
        verbose_name = '태그'
        verbose_name_plural = '태그'

별다른건 없습니다!

-boards/models.py

from django.db import models

class Board(models.Model):
    title = models.CharField(max_length=64)
    contents = models.TextField()
    writer = models.ForeignKey('accounts.User', on_delete=models.CASCADE)
    tags = models.ManyToManyField('tag.Tag')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    def __str__(self):
        return self.title

    class Meta:
        db_table = 'boards'
        verbose_name = '커뮤니티 게시판'
        verbose_name_plural = '커뮤니티 게시판'

기존의 Board모델에 tags를 ForeignKey로 선언해주었습니다. 기존의 방식과 같게 참조할 모델을 첫번째 인자로 입력했지만 on_delete=models.CASCADE를 입력하지 않은이유는 게시글이 지워지더라도 기존에 존재하는 태그명은 지워지지 않도록 하기위해 넣지 않았습니다.

 

tag같은경우는 별도로 tag디렉터리 안에서의 작업은 이루어지지 않고 board디렉터리의 form, view, templates에서 이뤄집니다. --> 사실 boards디렉터리 의 models.py안에 tag모델을 선언해도되지만 추후 추가될 comment 모델까지 있기때문에 가시성을 위해 따로 분리하였습니다.

 

2. view구성

-board/forms.py

from django import forms

class BoardForm(forms.Form):
    title = forms.CharField(
        error_messages={
            'required': '제목을 입력해주세요.'
        }, max_length=64, label="제목")
    contents = forms.CharField(
        error_messages={
            'required': '내용을 입력해주세요.'
        },widget=forms.Textarea, label = "내용")
    tags = forms.CharField(required=False, label = "태그")

태그를 입력하지 않아도 오류가 뜨지 않게 기존의 필드 선언과는 다르게 required=False로 선언해주었습니다.

 

-boards/views.py

def board_write(request):
    if not request.session.get('user'):
        return redirect('/accounts/login')

    if request.method =='POST': 
        form = BoardForm(request.POST)
        if form.is_valid():
            user_id = request.session.get('user')
            user = User.objects.get(pk=user_id)
            tags = form.cleaned_data['tags'].split(',')

            board=Board()
            board.title = form.cleaned_data['title']
            board.contents = form.cleaned_data['contents']
            board.writer = user
            board.save()

            for tag in tags:
                if not tag:
                    continue
                _tag, _ = Tag.objects.get_or_create(name=tag)
                board.tags.add(_tag)
            
            
            return redirect('/boards/list')
    else:
        form = BoardForm()
    return render(request, 'board_write.html', {'form':form})

board.tags.add(_tag)에서 board에 _tag를 저장한다는 의미인데 그렇기 위해서는 board는 이미 저장되어 있어야 하므로board저장 코드가 먼저 실행되어야 한다. 

for tag in tags:
    if not tag:
        continue
    _tag, _ = Tag.objects.get_or_create(name=tag)
    board.tags.add(_tag)

입력받은 tags안의 값들이 기존에 존재하는지 않하는지 알기위해 반복문을 위와 같이 사용했고

처음보는 get_or_create같은 경우 정의가

get_or_create 메서드는 객체(object)를 조회할 때 유용하게 사용되는 메서드이다. 이 메서드는 (object, created) 라는 튜플 형식으로 반환을 한다. 첫번째 인자(object)는 우리가 꺼내려고 하는 모델의 인스턴스이고, 두번째 인자(created)는 boolean flag이다. 

라고 한다.

그래서 총 2개의 인자를 받게 되는데 사실상 2번째 인자같은경우 우리가 사용하지 않기때문에 두번째 인자는 '_'으로 처리하였습니다.

 

3. templates

-board_write.html

{% extends "base.html" %}
{% block contents %}

<div class="row mt-5">
    <div class="col-12">
        <form method="POST" action=".">
            {% csrf_token %}
            {% for field in form %}
            <div class="form-group">
                <label for="{{ field.id_for_lable }}">{{ field.label }}</label>
                {{ field.field.widget.name }}
                {% ifequal field.name 'contents' %}
                <textarea class="form-control" name="{{ field.name }}" placeholder="{{ field.label }}"></textarea>
                {% else %}
                <input type="{{ field.field.widget.input_type }}" class="form-control" id="{{ field.id_for_lable }}"
                    placeholder="{{ field.label }}" name="{{ field.name }}" />
                {% endifequal %}
            </div>
            {% if field.errors %}
            <span style="color:red">{{ field.errors }}</span>
            {% endif %}
            {% endfor %}
            <button type="submit" class="btn btn-primary">글쓰기</button>
            <button type="button" class="btn btn-primary" onclick="location.href='/boards/list/{{ board.id }}'">돌아가기</button>
        </form>
    </div>
</div>
{% endblock %}

-board_detail.html

{% extends "base.html" %}

{% block contents %}
<div class="row mt-5">
    <div class="col-12">
        <div class="form-group">
            <label for="title">제목</label>
            <input type="text" class="form-control" id="title" value="{{ board.title }}" readonly>
            <label for="contents">내용</label>
            <textarea class="form-control" readonly>{{ board.contents }}</textarea>
            <label for="tags">태그</label>
            <span id="tags" class="form-control">
                {{ board.tags.all|join:"," }}
            </span>
        </div>
        <button class="btn btn-primary" onclick="location.href='/boards/list/'">돌아가기</button>
    </div>
</div>
{% endblock %}

이후 적절한 위치에 태그를 입력받고 보여주면 끝입니다!

 

4. 마무리 멘트

이번 챕터에서 느낀것은 장고를 잘 다루기 위해서는 파이썬 문법을 잘 다뤄야 한다고 느꼇습니다. 지금이야 튜토리얼 형식으로 간단한 코드들만 작성해서 문제 될일 없겠지만 나중에 프로젝트를 제대로 시작할때는 파이썬 문법공부가 제대로 되어있어야 한다고 느끼며 알고리즘 공부하러 가봐야겠네요!! 이번 얼마 안남았으니 끝가지 열심히!! 감사합니다:)

 

728x90
728x90

미리보는 프로젝트 완성코드 -->https://github.com/leeceo97/python_community

 

이번 챕터에서는 폼을 통한 view구성, 템플릿구성을 통해 실직적인 게시판 기능을 하도록 해보겠습니다.

 

1. form, view 구성

-boards/forms.py

from django import forms

class BoardForm(forms.Form):
    title = forms.CharField(
        error_messages={
            'required': '제목을 입력해주세요.'
        }, max_length=64, label="제목")
    contents = forms.CharField(
        error_messages={
            'required': '내용을 입력해주세요.'
        },widget=forms.Textarea, label = "내용")

accounts/forms.py 와는 다르게 따로 인증절차나 세션처리와 같은 기능은 없기때문에 위와 같이 title과 contents만을 

form으로 작성해줍니다.

 

-boards/views.py

from django.shortcuts import render,redirect,get_object_or_404
from django.http import Http404
from .models import Board
from .forms import BoardForm
from accounts.models import User
from django.core.paginator import Paginator

def board_write(request):
    if not request.session.get('user'):
        return redirect('/accounts/login')

    if request.method =='POST': 
        form = BoardForm(request.POST)
        if form.is_valid():
            user_id = request.session.get('user')
            user = User.objects.get(pk=user_id)

            board=Board()
            board.title = form.cleaned_data['title']
            board.contents = form.cleaned_data['contents']
            board.writer = user
            board.save()

            return redirect('/boards/list')
    else:
        form = BoardForm()
    return render(request, 'board_write.html', {'form':form})

첫번째 board_write의 코드를 먼저보면 크게 3가지로 나누었습니다.

-로그인 하지 않은 상태 --> 로그인 화면으로 redirect 

-POST인경우('/board/write/'에서 글쓰기 버튼 클릭)

user_id변수에 세션에서 유저의 id 값을 받고 ForeignKey로 선언한 writer필드에 유저의 정보를 가져온다. 

-GET의경우('/board/list/'에서 글쓰기 버튼 클릭) --> 폼 형식 전달

 

def board_detail(request, pk):
    board = get_object_or_404(Board, pk=pk)
    return render(request, 'board_detail.html',{'board':board})

두번째 board_detail의 코드는 get_object_or_404(Board, pk=pk) 이게 다입니다.

첫번째 인자는 가져올 모델, 두번째 인자는 가져올 모델의 pk값입니다. url에서 받은 pk값을 통해 구별 하는것인데 만약 pk값이 존재하지 않다면 404에러페이지를 반환하는 코드입니다. 

 

 

def board_list(request):
    all_boards = Board.objects.all().order_by('-id')
    page = int(request.GET.get('p', 1))
    paginator = Paginator(all_boards, 5)
    boards = paginator.get_page(page)

    return render(request, 'board_list.html', {'boards':boards})

세번째 board_list는 all_boards라는 변수에 Board모델에 저장된 모든 값을 id의 역순으로 불러옵니다.

page = int(request.GET.get('p', 1))
paginator = Paginator(all_boards, 5)
boards = paginator.get_page(page)

이후 이코드는 페이지네이터라는 것으로 게시물 리스트의 개수를 지정하여 그개수에 따라 페이지를 나누는 미리 만들어진 함수입니다. 이코드 같은경우는 외운다기보다는 미리 복사를 해두고 때에 맞게 바꿔가며 사용하면 됩니다.

그럼 위와 같이 페이지가 나눠져있는것을 확인할수 있습니다.

 

2. 마무리멘트

이렇게 하면 게시판의 기능은 완료되었습니다. 회원가입,로그인에 비해 짧고 설명이 덜한것은 그만큼 하나의 로직을 이해하면 추후의 내용은 조금의 학습으로 응용가능 하다는걸 알수있는것 같습니다. 앞으로남은 챕터는 총 3챕터로 태그, 댓글기능 구현 및 배포 입니다. 감사합니다:)

728x90
728x90

미리보는 프로젝트 완성코드 -->https://github.com/leeceo97/python_community

 

사실 게시판 같은경우는 이미 유저 구현하는데 어려움이 없다면 더욱 쉽게 해결가능할것입니다!

 

1. MODEL구성

from django.db import models

class Board(models.Model):
    title = models.CharField(max_length=64)
    contents = models.TextField()
    writer = models.ForeignKey('accounts.User', on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    def __str__(self):
        return self.title

    class Meta:
        db_table = 'boards'
        verbose_name = '커뮤니티 게시판'
        verbose_name_plural = '커뮤니티 게시판'

다른 부분은 user와 별다른 부분은 없지만 writer의 ForeignKey같은 경우는 다른 모델을 참조하는 것으로 첫번째 인자에는 참조할 모델을 넣어준다. on_delete=models.CASCADE같은 경우는 참조한 모델이 삭제될경우 현재 모델또한 삭제한다는 의미이다. 

 

2. VIEW, templates 구성

- boards/views.py

from django.shortcuts import render
from .models import Board

def board_write(request):
	return render(request, 'board_write')
    
def board_detail(request,pk):
	return render(request, 'board_detail.html')
    
def board_list(request):
	return render(request, 'board_list.html')

게시판은 크게 board_write: 글쓰기, board_detail: 클릭한 게시물보기, board_list: 모든 게시물 보기로 총 3가지의 기능을 담당한 뷰로 구성하였다.  board_detail에 pk를 추가해준것은 게시글 각각의 id값을 통해 board_detail url을 구분해주기위해 추가하였다. 추후 url코드에서 이를 확인할수 있을것입니다.

 

이에 맞는 각각의 템플릿을 만들어주면 된다.

 

- board_detail.html

{% extends "base.html" %}

{% block contents %}
<div class="row mt-5">
    <div class="col-12">
        <div class="form-group">
            <label for="title">제목</label>
            <input type="text" class="form-control" id="title" value="{{ board.title }}" readonly>
            <label for="contents">내용</label>
            <textarea class="form-control" readonly>{{ board.contents }}</textarea>
        </div>
        <button class="btn btn-primary" onclick="location.href='/boards/list/'">돌아가기</button>
    </div>
</div>
{% endblock %}

-board_list.html

{% extends "base.html" %}

{% block contents %}
<div class="row mt-5">
    <div class="col-12">
        <table class="table table-light">
            <thead class="thead-light text-dark">
                <tr>
                    <th>#</th>
                    <th>제목</th>
                    <th>아이디</th>
                    <th>일시</th>
                </tr>
            </thead>
            {% for board in boards %}
            <tbody class="text-dark">
                <tr onclick="location.href='/boards/detail/{{ board.id }}'">
                    <th>{{ board.id }}</th>
                    <td>{{ board.title }}</td>
                    <td>{{ board.writer }}</td>
                    <td>{{ board.created_at }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    </div>
</div>
<div class="row">
    <div class="col-12">
        <button class="btn btn-primary" onclick="location.href='/boards/write/'">글쓰기</button>
    </div>
</div>
{% endblock %}

- board_write.html

{% extends "base.html" %}
{% block contents %}

<div class="row mt-5">
    <div class="col-12">
        <form method="POST" action=".">
            {% csrf_token %}
            {% for field in form %}
            <div class="form-group">
                <label for="{{ field.id_for_lable }}">{{ field.label }}</label>
                {{ field.field.widget.name }}
                {% ifequal field.name 'contents' %}
                <textarea class="form-control" name="{{ field.name }}" placeholder="{{ field.label }}"></textarea>
                {% else %}
                <input type="{{ field.field.widget.input_type }}" class="form-control" id="{{ field.id_for_lable }}"
                    placeholder="{{ field.label }}" name="{{ field.name }}" />
                {% endifequal %}
            </div>
            {% if field.errors %}
            <span style="color:red">{{ field.errors }}</span>
            {% endif %}
            {% endfor %}
            <button type="submit" class="btn btn-primary">글쓰기</button>
            <button type="button" class="btn btn-primary" onclick="location.href='/boards/list/{{ board.id }}'">돌아가기</button>
        </form>
    </div>
</div>
{% endblock %}

 

3. url구성

-community/urls.py

from django.contrib import admin
from django.urls import path, include
from accounts.views import home

urlpatterns = [
    path('', home),
    path('admin/', admin.site.urls),
    path('accounts/', include('accounts.urls')),
    path('boards/', include('boards.urls')),
]

boards 디렉터리에 urls.py를 생성하기전 이전의 accounts.urls를 추가한것과 같이 boards.urls를 등록해준다.

 

-boards/urls.py

from django.urls import path
from .views import board_list, board_write, board_detail
urlpatterns = [
    path('list/', board_list, name='board_list'),
    path('write/', board_write, name='board_write'),
    path('detail/<int:pk>/', board_detail, name='board_detail'),
]

view코드를 작성할때 전달해준 pk값을 <int:pk>를 통해 pk 라는 이름의 id값으로 각각의 게시물을 보여주게 됩니다.

 

4. 마무리멘트

전의 accounts때와는 다르게 설명은 별로 없고 코드만 나온걸 볼수 있습니다. 이번 챕터는 전 챕터들의 내용을 모두 이해한 상태라면 추가적인 내용은 텍스트로 적은 내용말고는 없으니 충분히 이해를 하며 하나하나 에디터에 입력하시면 될것같습니다! 감사합니다:)

728x90
728x90

미리보는 프로젝트 완성코드 -->https://github.com/leeceo97/python_community

 

이번 챕터에서 다룰 내용은 기존의 유저 로그인 코드를 장고내의 form기능을 활용하여 리팩토링 하는 과정입니다.

글을 작성하기전 느꼈던 건데 생각보다 이해하기 어려워서 최대한 자세히 작성을 할테니 글이 길어지긴 할텐데 이해한번 해보고 넘어갑시다!! 잘알면 유용한 기능입니다~~

 

1. forms.py 생성및 기존 코드 삭제및 수정

login.html

{% extends "base.html" %}
{% block contents %}
        <div class="row mt-5">
            <div class="col-12 text-center">
                <h1>로그인</h1>
            </div>
        </div>
        <div class="row mt-5">
            <div class="col-12">
                {{ error }}
            </div>
        </div>
        <div class="row mt-5">
            <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">로그인</button>
                </form>
            </div>
        </div>
{% endblock %}

accounts/forms.py(accounts폴더내에 forms.py란 파일 생성후 코드기입해주세요.)

from django import forms
from .models import User
from django.contrib.auth.hashers import check_password

class LoginForm(forms.Form):
    useremail = forms.EmailField(
        error_messages={
            'required': '아이디를 입력해주세요.'
        }, max_length=64, label="사용자 이메일")
    password = forms.CharField(
        error_messages={
            'required': '비밀번호를 입력해주세요.'
        },widget=forms.PasswordInput, label = "비밀번호")

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

        if useremail and password:
            user = User.objects.get(useremail=useremail)
            if not check_password(password, user.password):
                self.add_error('password', '비밀번호가 틀립니다.')
            else:
                self.user_id = user.id

accounts/views.py

from .forms import LoginForm

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})

하나하나 짚고 넘어가면 일단 forms.py의 기능은 장고 내에 있는 html에서 쓰이는 폼기능들을 백엔드 개발자가 쓰기 편하게 미리 만들어논 기능이라고 보시면 됩니다. 쉽게말해 미리 로그인이나 회원가입에서 필요한 예외처리 패스워드 처리등을 미리 기술 개발을 통해 제공한 것입니다. 

 

-html

html 파일 먼저 보시면 form을 사용하기전에 하나하나 라벨등과 같이 일일이 쳐줬던것과는 달리 장고 템플릿 변수를 사용해 label, widget, error등을 몇줄에 걸쳐 다표현한것을 알수 있습니다. 장고 템플릿 변수에대해 자세히 알고 싶으시면 

아래의 사이트에 방문해 알아보시는것도 좋을것 같습니다. html 같은경우는 코드를 다작성하고 나면 알게 되므로 간단히 생략하고 넘어가도록 하겠습니다.

 

[Django] 템플릿 언어

템플릿 언어를 사용하면 HTML 작업을 훨씬 수월하게 할 수 있다.

velog.io

 

-forms.py

useremail = forms.EmailField(
        error_messages={
            'required': '아이디를 입력해주세요.'
        }, max_length=64, label="사용자 이메일")
password = forms.CharField(
    error_messages={
        'required': '비밀번호를 입력해주세요.'
    },widget=forms.PasswordInput, label = "비밀번호")

이부분을 먼저 살펴보면 html에서 유효성 검사와 에러메시지등 받아올 데이터들에 대해 useremail, password 필드를 따로 만들어준것이다. 이후 모든 유효성 검사가 끝난 데이터들을 accounts 모델에 저장시키는 것이다. 나머지는 이해 되겠지만 password의 widget같은 경우는 CharField에 위젯을 PasswordInput으로 설정해줌으로써 유저가 비밀번호를 입력할때 가려진 상태로 비밀번호가 입력되게 된다. 

 

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

        if useremail and password:
            user = User.objects.get(useremail=useremail)
            if not check_password(password, user.password):
                self.add_error('password', '비밀번호가 틀립니다.')
            else:
                self.user_id = user.id

이부분을 이해하려면 일단 super()가 무엇인지 알아야하는데 이건 오버라이딩의 개념을 알고 있어야 합니다. 아래의 사이트를 참고하시면 됩니다.

 

[Python] 파이썬 super 기초 개념 및 예제

먼저 super를 사용하기전 상속, 오버라이딩 의 개념이 잡혀있어야 이해하기 쉽습니다. (상속, 오버라이딩 클릭시 페이지 이동) 개념 super() - 자식 클래스에서 부모클래스의 내용을 사용하고 싶을

rednooby.tistory.com

clean함수는 다음과 같은 기능을 합니다. 

-User모델에서 기존의 테이블을 불러와 폼에 들어온 데이터와 비교작업

-각종 예외처리에 대한 에러작업

form의 코드를 보면 기존의 view에서 사용하던 기능들을 가져와 처리해줌으로써 view의 코드들이 직관적으로 비즈니스 기능들만 수행하도록 하는 역할을 한다는걸 알수 있습니다.

 

-views.py

from .forms import LoginForm

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})

기존의 views.py의 코드와는 다르게 많이 간단해진걸 알수가 있습니다. 여기서 궁금한것은 is_valid()였습니다. 

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

is_valid함수를 찾아보니 위와 같이 에러가 없을경우 참값을 반환한다는 걸 알수 있습니다. 즉, forms.py의 유효성 검사를 모두 거쳐야지 다음의 조건을 수행하도록 해준다는 함수임을 알수 있습니다. 

 

2. 마무리 멘트

이번 form은 사실 너무 이해하기가 힘든 챕터였습니다. 오버라이딩의 개념도 제대로 이해하지 못했고 is_valid와 clean()등 이게 뭘뜻하는지 내장된 함수인지 조차 잘모르겠어서 난해했는데 하나하나 파고 들어보니 이해가 갔고 다시한번 파이썬 기본문법의 중요성을 알수있는 챕터였다고 느껴집니다. 한가지 팁을 알려드리면 이게 장고에 내장되어있는 기능인지 확인해보는 방법은 아래의 이미지와 같이 헷갈리는 변수에 오른쪽 클릭을하고 Go to Definition을 클릭하면 그함수 또는 클래스가 어떻게 이루어져있는지 보여주니 보고 참고하셔서 개발하시면 될겁니다!! 감사합니다:)

728x90
728x90

미리보는 프로젝트 완성코드 -->https://github.com/leeceo97/python_community

 

1. register 구성

일단 회원가입부터 진행 하겠습니다.

accounts/views.py

from django.shortcuts import render
from .models import User
from django.http import HttpResponse
from django.contrib.auth.hashers import make_password, check_password

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

    elif request.method == 'POST':
        username = request.POST.get('username', None)
        useremail = request.POST.get('useremail', None) 
        password = request.POST.get('password', None)
        re_password = request.POST.get('re_password', None)
        
        err_data={}
        if not(username and useremail and password and re_password):
            err_data['error'] = '모든 값을 입력해주세요.'
        
        elif password != re_password:
            err_data['error'] = '비밀번호가 다릅니다.'
        
        else:
            user = User(
                username=username,
                useremail=useremail,
                password=make_password(password),
            )
            user.save()

        return render(request, 'register.html', err_data)

하나하나 설명 해보면 총 2단계로 나눌수 있습니다.

- 클라이언트가 서버에 요청하는 방식:  GET vs POST

간단히 말해 GET은 조회 --> 회원가입 양식만 반환

POST는 데이터 생성,삭제,수정 --> 에러 데이터 반환과 클라이언트가 보낸 정보에 따른 유저 회원정보 저장

아래의 사이트를 참고하면 쉽게 이해가 갈것입니다.

 

[Web] GET과 POST의 비교 및 차이

사용자가 어떤 홈페이지로 이동하기 위해서 URL을 브라우저 주소창에 작성하고 엔터를 누르면 원하는 페이지로 이동합니다. 사용자는 단순히 URL(Uniform Resource Locator)을 입력하였을 뿐이지만 서버

mangkyu.tistory.com

따라서, if request.method == 'GET', 'POST 로 나눠 처음 반환값을 다르게 해줬습니다.

 

- 유저 정보 예외처리 및 저장

username = request.POST.get('username', None)
useremail = request.POST.get('useremail', None) 
password = request.POST.get('password', None)
re_password = request.POST.get('re_password', None)

이부분을 보면 왼쪽에 위치한 값들은 내가 함수내에서 사용할 변수의 명이고 오른쪽의 값은 클라이언트에서 보내준 4개의 값들을 get 함수로 받아 저장하는 것을 나타냅니다.

get()함수의 첫번째 인자는 찾고싶은 딕셔너리의 key값이고 두번째 인자는 출력하고 싶은 인자가 없을때 출력할 값으로 저희는 예외는 없기에 None으로 설정하였습니다. 

err_data={}
        if not(username and useremail and password and re_password):
            err_data['error'] = '모든 값을 입력해주세요.'
        
        elif password != re_password:
            err_data['error'] = '비밀번호가 다릅니다.'
        
        else:
            user = User(
                username=username,
                useremail=useremail,
                password=make_password(password),
            )
            user.save()

        return render(request, 'register.html', err_data)

err_data라는 객체를 선언하여 입력해야할 4개의 값중 하나라도 없을시 '모든 값을 입력해주세요.'라는 값을 'error' key에 담아 반환하고, 비밀번호와 비밀번호 재확인 값이 다르다면 동일하게 'error' key에 오류 값을 담아 반환합니다.

이 두가지의 경우에 해당 하지 않을경우 유저의 정보를 데이버베이스에 저장하는 과정입니다. 

return render(request, 'register.html',err_data)를 보시면 GET과의 차이점을 아시겠나요?

바로 에러데이터를 세번째 인자 값에 넣어서 전달한것입니다. 

지난 챕터에서 만든 register.html 파일소스를 보시면

<div class="row mt-5">
            <div class="col-12">
                {{ error }}
            </div>
        </div>

이런 코드가 있는데 view에서 담아 보낸 error 값이 있을시 저 위치에 오류가 표시 됩니다.

 

이후 python manage.py runserver로 서버를 활성화 시키고 http://127.0.0.1:8000/accounts/register로 접속하셔서

알맞은 값들을 입력하시고 http://127.0.0.1:8000/admin에 들어가 확인해보면

제가 register url에서 처음으로 생성한 유저가 등록되어 있음을 알수 있습니다. 

 

2. login 구성

register 부분을 이해하셨다면 login 부분은 크게 어렵지 않을겁니다!

accounts/views.py

from django.shortcuts import render
from .models import User
from django.http import HttpResponse
from django.contrib.auth.hashers import make_password, check_password

def login(request):
    if request.method =='GET':
        return render(request, 'login.html')
    elif request.method == 'POST':
        useremail = request.POST.get('useremail', None)
        password = request.POST.get('password', None)

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

register와 겹치는 설명이 많으므로 생략하고 설명할 부분은 바로 우리가 이해하고자 했던 세션 부분입니다.

request.session['user'] = user.id

request.session 은 각 키마다 값을 가지고 있는 dictionary 와 비슷한 형태라고 알고 있자.

request.session 에 'user' 라는 키 에다가 user 의 id를 넣어준다.

그리고 'user' 라는 키 말고 이름은 내 마음대로 지정해도 된다. 단지 세션 'user' 라는 키에 유저 정보를 저장해 둔 것이다. 이는 로그인한 유저의 id 값을 저장한 것이다. 이렇게 넣은 것 만으로 세션 처리가 끝난 것이다.

 

3. 로그아웃

로그인에서 유저모델의 id를 request.session의 ['user'] key에 넣어줬기때문에 로그인을 한 유저라면 request.session['user']값이 있을겁니다. 그렇기 때문에 로그아웃은 그반대로 값이 있다면 request.session['user']값을 삭제해 주면 됩니다.

accounts/views.py

def logout(request):
    if request.session.get('user'):
        del(request.session['user'])
    return redirect('/')

 

위와 같이 작성하기만 하면 로그아웃 기능이 완성됨을 알수 있습니다.

 

4. 홈 화면(선택)

로그인을 했는지 안했는지 알아보기위해 홈화면을 간단하게 구성해보겠습니다.

accounts/views.py

def home(request):
    user_id = request.session.get('user')
    if user_id:
        user = User.objects.get(pk = user_id)
        return HttpResponse("Hello! %s님" % user)
    else:
        return HttpResponse("로그인 해주세요!")

위와 같이 작성하기만 하면 로그인을 했을때 request.session에 user키값이 존재하므로 화면에 유저의 이름이 나올것이고, 아니라면 로그인을 부탁하는 문구가 출력될것입니다.

 

5. 마무리 멘트

이글을 보고 장고 공부를 하시는 분들이 어떻게 느끼실지는 모르겠지만 저는 장고를 처음 공부할때 가장 어려움을 겪었던 부분입니다. 그냥 코드 따라치고 나중에 복붙해서 결과물은 만들수 있었지만 이부분을 모르고 넘어간게 중간 중간 코드를 어떻게 고쳐야 하는지 모를정도로 크게 다가왔습니다. 어느부분에 값이 저장되고 어떻게 view가 구성되는지 이번에 제대로 공부하고 가셨으면 좋겠습니다!! 감사합니다:)

728x90

+ Recent posts