Django 기초 강의 정리

패스트캠퍼스 장고 기초 강의 정리

Mac 기준으로 작성했습니다.

웹 프레임워크에 대한 이해

프레임워크란

  • 자주 사용되는 코드를 체계화하여 쉽게 사용할 수 있도록 도와주는 코드 집합
  • 라이브러리와 혼동될 수 있지만 좀 더 규모가 크고 프로젝트의 기반이 됨
  • 건축에 비유하면 구조를 만드는 골자가 프레임 워크, 그 외 자재들이 라이브러리

우리가 건물을 지으면서 구조를 잡으면 다시 바꾸기 어렵듯이 프레임워크도 그러하고, 자재들은 변경이 용이하듯 라이브러리는 쉽게 변경할 수 있다. 어찌보면 프레임워크를 사용하는 것이 의존성이 높아 보일 수도 있지만 그만큼 틀이 잡혀있고 제공해주는것이 많아 사용하기 좋다.

웹 프레임워크

웹 개발에 필요한 기본적인 구조와 코드(클래스, 함수 등)가 만들어져있다.

제공해주는 기능

  • URL 파싱
  • 요청 파싱
  • 응답 생성
  • 세션 관리
  • 데이터베이스 연동
  • 관리자 페이지

    개발자가 해야할 일

  • 비즈니스 로직 구성: 어떻게 동작할지 구성해야함.
  • 데이터 정의

웹 프레임워크로써 Django

Django는 MTV 방법론을 기반했다.

  • Model계층: 비즈니스에 사용할 데이터들을 구성하여 데이트베이스에 연동하는 부분. Django에서 제공하는 기능을 사용해서 쉽게 데이터들을 만들고 원하는 sql을 사용하여, 프로그래밍적으로 제어할 수 있다. 심화해서도 사용할 수 있다.
  • View계층: 비즈니스 로직에 해당.(우리가 만들어야 할 부분), URL 파싱이라던가, 결과값 반환에 대한 구현되어있고, 사용하면 된다.
  • Template계층: HTML코드에 대한 부분, 템플릿 언어를 제공하여 반복문, 제어문 등을 사용하여 데이터를 유연하게 다룰 수 있다.

Django 프로젝트 구성

python 가상환경으로 프로젝트를 구분해주자. 가상환경을 쓰는 이유는 A라는 프로젝트는 x버전의 a패키지가 필요하고, B라는 프로젝트는 y버전의 a 패키지가 필요할 수 있다. 이때, 그냥 설치된 python을 사용한다면, 버전 충돌 문제도 있을 수 있기에 가상환경을 만들어 버전별로 관리해주는 것이 좋다! pyenv 정리 보기

django 설치

pip install django

django 프로젝트 생성

django-admin startproject 프로젝트명

django 앱 생성

django-admin startapp 앱명

생성한 앱들은 프로젝트의 settings.py의 INSTALLED_APPS에 등록을 해줘야 동작한다.

INSTALLED_APPS += [
    '앱1',
    '앱2',
]

Django MTV

Model 만들기

앱 디렉토리에서 models.py에서 만들어준다.

from django.db import models

# 모델 만들 때 models의 Model 클래스를 상속받는다.
class Fcuser(models.Model):
    username = models.CharField(max_length=64,
                                verbose_name='사용자명') # 관리자 페이지에서 보일 이름
    password = models.CharField(max_length=64,
                                verbose_name='비밀번호')
    # 등록시간 지정, auto_now_add를 사용하면 자동으로 시간이 기록된다.
    registered_dttm = models.DateTimeField(auto_now_add=True,
                                           verbose_name='등록시간')
    # Meta 클래스를 사용해서 db 테이블 명을 지정할 수 있다.
    class Meta:
        db_table = 'fastcampus_fcuser' # DB 테이블 명 지정하기


DB생성

makemigrations으로 변경점이나 추가된 부분에 대한 파일을 만든다.

python manage.py makemigrations

migrate 명령어로 데이터베이스에 반영한다.

python manage.py migrate

sqlite3 확인 방법
sqlite db.sqlite3
.tables
.schema fastcampus_fcuser
.q

admin 생성

모델을 관리하기 쉽게 관리자 페이지를 제공한다.

admin 계정 만들기

python manage.py createsuperuser

django에서 제공하는 서버 실행

python manage.py runserver

브라우저에 127.0.0.1:8000(기본) 접속 후 만든 계정으로 접속

admin 페이지에 앱의 모델들도 보이게 설정

앱/admin.py 설정

from django.contrib import admin
# 관리할 모델 등록
from .models import Fcuser

# 클래스 생성
class FcuserAdmin(admin.ModelAdmin):
    # 해당 모델 페이지에서 보여줄 필드명 지정
    list_display = ('username', 'password')

# admin 사이트에 등록하기
admin.site.register(Fcuser, FcuserAdmin)

models.py 일부 추가

# 클래스를 문자열로 변환했을 때 표현할 값
    def __str__(self):
        return self.username

    class Meta: # 아래 두 라인 추가
        # admin 페이지에서 모델 명 대신 사용자 지정으로 표시
        verbose_name = '패스트캠퍼스 사용자'
        # Django는 끝에 s가 붙어 복수형으로 보여주는데,
        # 우리는 한글이니 복수형태도 변경해준다.
        verbose_name_plural = '패스트캠퍼스 사용자'

Templates 만들기

부트스트랩을 이용해서 실습해 보았다.


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
          integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
            integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"
            crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
            integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
            crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
            integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
            crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
    <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 %}
            <div class="form-group">
                <label for="username">사용자 이름</label>
                <input type="text" class="form-control" id="username" placeholder="사용자 이름" name="username">
            </div>
            <div class="form-group">
                <label for="useremail">사용자 이메일</label>
                <input type="email" class="form-control" id="usermail" placeholder="사용자 이메일" name="useremail">
            </div>
            <div class="form-group">
                <label for="password">비밀번호</label>
                <input type="password" class="form-control" id="password" placeholder="비밀번호" name="password">
            </div>
            <div class="form-group">
                <label for="re-password">비밀번호 확인</label>
                <input type="password" class="form-control" id="re-password" placeholder="비밀번호 확인" name="re-password">
            </div>
            <button type="submit" class="btn btn-primary">등록</button>
        </form>
    </div>
</div>
</div>
</body>
</html>

  • error: views.py에서 선언한 error 메세지를 템플릿 엔진에서 변환해줌.
  • csrf_token: CSRF공격을 방지하기 위해 form태그 아래에 들어가야함.

CSRF공격이란 Cross Site Request Forgery의 준말로 웹 어플리케이션 취약점 중 하나인데, 인터넷 사용자가 자신의 의지와 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 만드는 공격이라한다.

CDN과 static 파일 관리

CDN(Contens Delivery Network)

사용자에게 웹 콘텐츠를 효율적으로 제공할 수 있는 서버의 분산 네트워크. 최종 사용자와 가까운 POP(Point-Of-Presence)의 위치의 Edge 서버에 캐시된 콘텐츠를 저장하여 대기 시간을 최소화한다.

쉽게 말해, 지리, 물리적으로 떨어져 있는 사용자에게 컨텐츠를 더 빠르게 제공할 수 있고, 느린 응답속도/다운로드 시간을 최소화해준다. CDN 정리 잘되어있는 사이트

static 파일

웹페이지에 쓰이는 CSS, JS코드 파일들. 이 파이들을 효율적으로 관리할 수 있도록 Django에서 static 폴더를 생성해서 사용할 수 있다.

  • 프로젝트 파일에 static 폴더 생성
  • 폴더 안에 css or js 파일 삽입
  • settings.py 하단에 다음 입력
    STATICFILES_DIRS = [
      os.path.join(BASE_DIR, 'static')
    ]
    
  • html파일에 링크 추가 ```


## 세션
브라우저에 있는 쿠키를 통해 데이터를 유지
서버는 헤더에 쿠키정보를 제공
클라이언트 (요청 `쿠키없이`)-> 서버
클라이언트 <-(응답 `쿠키포함`) 서버: 서버는 DB에 쿠키값을 만들고, 그 값을 클라이언트 응답 헤더에 포함하여 보내준다. 응답을 받은 클라이언트는 자신의 쿠키 저장소에 "서버 - 쿠키값" 들을 저장한다.
클라이언트 (요청 `쿠키 포함`)-> 서버: 클라이언트는 서버의 쿠키값을 같이 보낸다.
클라이언트 <-(응답 `쿠키 포함`) 서버: 이 쿠키값으로 클라이언트를 인지하고 적절한 값을 돌려준다.

#### 로그인 페이지 구현
```python
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:
            fcuser = Fcuser.objects.get(username=username)
            if check_password(password, fcuser.password):
                request.session['user'] = fcuser.id # user = id 세션값 생성
                return redirect('/')
            else:
                res_data['error'] = '비밀번호 틀렸습니다.'
        return render(request, 'login.html', res_data)

  • check_password를 통해 비밀번호까지 맞으면, request.session['user'] = fcuser.id를 통해 세션 생성

홈 이동 및 로그아웃 구현

def home(request):
    user_id = request.session.get('user')
    if user_id:
        fcuser = Fcuser.objects.get(pk=user_id)
        return HttpResponse(fcuser.username)

    return HttpResponse("Home!")

# 세션을 삭제해주면 된다.
def logout(request):
    if request.session.get('user'):
        del(request.session['user'])

    return redirect('/')

Template 상속

반복되는 코드를 줄이기 위해 틀을 만들어 주고 페이지별 내용을 따로 관리하기 위해 사용한다.

Form 활용하기

forms를 활용해서 input태그를 만들 수 있다.

forms 파일 생성

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

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

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

        if username and password:
            fcuser = Fcuser.objects.get(username=username)
            if not check_password(password, fcuser.password):
                self.add_error('password', '비밀번호를 틀렸습니다.')
            else:
                self.user_id = fcuser.id

views login 함수 변경

def login(request):
    if request.method == "POST":
        # print(request.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})

로그인 파일 수정


게시판 만들기

urls 파일 생성

from django.urls import path
from .import views

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

모델 정의

from django.db import models

class Board(models.Model):
    title = models.CharField(max_length=128,
                                verbose_name='제목') # 관리자 페이지에서 보일 이름
    contents = models.TextField(verbose_name='내용')
    writer = models.ForeignKey('fcuser.Fcuser', on_delete=models.CASCADE, verbose_name='작성자')
    registered_dttm = models.DateTimeField(auto_now_add=True,
                                           verbose_name='등록시간')

    # 클래스를 문자열로 변환했을 때 표현할 값
    def __str__(self):
        return self.title

    class Meta:
        db_table = 'fastcampus_board' # DB 테이블 명 지정하기
        verbose_name = '패스트캠퍼스 게시글'
        verbose_name_plural = '패스트캠퍼스 게시글'

form 정의

from django import forms

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

views 정의

from django.shortcuts import render, redirect
from django.core.paginator import Paginator
from django.http import Http404
from .models import Board
from fcuser.models import Fcuser
from .forms import BoardForm

def board_detail(request, pk):
    try:
        board = Board.objects.get(pk=pk)
    except Board.DoesNotExist:
        raise Http404('게시글을 찾을 수 없습니다.')
    return render(request, 'board_detail.html', {'board':board})

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

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

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

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

def board_list(request):
    all_boards = Board.objects.all().order_by('-id')
    page = int(request.GET.get('p', 1))
    paginator = Paginator(all_boards, 2) # 페이지당 보여줄 개수

    boards = paginator.get_page(page)
    return render(request, 'board_list.html', {'boards': boards})

html


{% extends "base.html" %}

{% block contents %}
<div class="row mt-5">
    <div class="col-12">
        <table class="table table-light">
            <thead class="thead-light">
                <tr>
                    <th>#</th>
                    <th>제목</th>
                    <th>아이디</th>
                    <th>일자</th>
                </tr>
            </thead>
            <tbody class="text-dark">
            {% for board in boards %}
            <tr>
                <th>{{ board.id }}</th>
                <td>{{ board.title }}</td>
                <td>{{ board.writer }}</td>
                <td>{{ board.registered_dttm }}</td>
            </tr>
            {% endfor %}
            </tbody>
        </table>
    </div>
</div>
<div class="row mt-2">
    <div class="col-12">
        <nav>
            <ul class="pagination justify-content-center">
                {% if boards.has_previous %}
                <li class="page-item">
                    <a class="page-link" href="?p={{boards.previous_page_number}}">이전으로</a>
                </li>
                {% else %}
                <li class="page-item disabled">
                    <a class="page-link" href="#">이전으로</a>
                </li>
                {% endif %}
                <li class="page-item active">
                    <a class="page-link" href="">{{boards.number}} / {{ boards.paginator.num_pages }}</a>
                </li>
                {% if boards.has_next %}
                <li class="page-item">
                    <a class="page-link" href="?p={{boards.next_page_number}}">다음으로</a>
                </li>
                {% else %}
                <li class="page-item disabled">
                    <a class="page-link disabled" href="#">다음으로</a>
                </li>
                {% endif %}
    </div>
<div class="row">
    <div class="col-12">
        <button class="btn btn-primary" onclick="">글쓰기</button>
    </div>
</div>
{% endblock %}

배포하기

settings 파일 변경

DEBUG = False
ALLOWED_HOSTS = [
    '*',
    'hsj4665.pythonanywhere.com'
]

STATICFILES_DIRS = [] # 주석 처리
# 아래의 라인을 추가하고 명령어를 통해 해당 디렉토리에 정적파일들을 모두 수집한다.
STATIC_ROOT = os.path.join(BASE_DIR, 'static')