상세 컨텐츠

본문 제목

피로그래밍14기: day8 <DJANGO - ORM, CRUD>

환 codes web/DJANGO

by 퍼블리셔환 2021. 1. 15. 23:00

본문

 

 

이전의 <DJANGO girls>세션과 이어지는 포스트이다. 앞 내용을 보지 못한 사람들은 보고 오면 도움이 될 것이다. 

 

피로그래밍14기: day7 오후

django girls라는 사이트에서 django의 기본 튜토리얼을 진행했다. 피로그래밍의 이후 세션에서 진행한 내용과 상당 부분 겹치기 때문데, day8 세션의 포스트와 이어지도록 작성했다. 객체 지향 프로

nwy1996.tistory.com

 

∫. CRUD와 ORM이란 무엇인가?

출처 : https://morioh.com/p/7b5749939ec5

 

CRUD Create, Read, Update, Delete의 약자로, 말 그대로 백엔드에서 데이터를 생성하고 읽어오고 수정하고 삭제하는 것을 의미한다. 이 자체가 어떤 기술이나 툴을 의미하는것은 아니지만, 데이터를 다루기 위해서 꼭 구현해야 하는 4가지 기능들의 체크리스트라고 볼 수 있다.

 

ORM Object Relational Mapping이다. 데이터를 클래스의 속성을 가지고 있는 객체에 매핑해서 객체처럼 쓸 수 있는 방식이다. 데이터베이스를 특정 프레임워크에서 다루기 위한 조금 더 쉬운 방식이다. 우리가 사용할 django에서는 파이썬을 이용해 ORM의 방식으로  CRUD를 구현한다. 

 

이번 세션에 진행한 내용은 굉장히 많아보이지만, 사실 일정한 패턴을 가지고 있는 것 같다. (물론 아닐수도 있음)

1. models.py에 class를 만들어서 데이터의 속성을 정해준다. (앞 세션에서 진행함)

2. 구현하고자 하는 기능을 views.py에 def의 형태로 만들고,

3. 기능에 대한 각각의 html페이지를 구성해서 데이터를 시각적으로 보여준다. 이 html의 형식을 template이라고 부르고, 다시 view에 있는 def에 연결한다.  

4. urls.py에 각 html 페이지별 url주소를 path로 지정한다. 


∇ Read

read를 해오기 위해서는 불러올 post가 필요하다. http://127.0.0.1:8000/admin 페이지에 들어가서 superuser로 등록한 정보를 입력해서 로그인한다. 그렇다면 post를 등록할 수 있는데, 아무 글이나 작성해보자. 그리고 다시 http://127.0.0.1:8000/ 페이지로 돌아온다. 

 

∫ posts\views.py

어떤 정보든지 read를 위해서는 기본 페이지가 필요하다. 지금의 경우에는 별도의 시작 페이지를 만들지 않고, 우리가 작성한 post들의 목록을 보여줄 것이다. 앞으로도 기본적으로 views.py에 필요한 라이브러리를 불러오자. 

from django.shortcuts import get_object_or_404, render, redirect, HttpResponse
from .models import Post
from .forms import PostForm

그리고 위에서 설명했던 기본 리스트 페이지를 정의하면 다음 코드와 같다. 

def post_list(request):
    '''
    Read(R)
    포스트들을 불러와서 리스트의 형태로 보여준다
    '''
    posts = Post.objects.all
    ctx = {'posts': posts}
    return render(request, template_name='posts/list.html', context=ctx)

이 코드는 우선 request로 받아온 post들의 list임을 나타낸다.

posts = Post.objects.all 은 Post의 객체들을 전부 가져온다는 뜻이다. 그런데 지금 당장 가져오는 것이 아니라 이런 객체들을 일단 posts라는 리스트에 저장해서 나중에 for post in posts의 형태로 list: 'posts' 안에 있는 element: 'posts'를 불러온다. 

ctx = {'posts': posts}는 html로 넘어가는 string을 의미한다. ? 이게 무슨 말인지 모르겠다. 밑에 있는 return이 넘겨주는거 아닌가?

post_list가 호출되면 반환되는 return값은 render를 통해서 반환되고,posts 안에 있는 list.html이라는 템플릿이 context와 함께 반환된다. 

 

∫ posts\templates\posts\list.html

이제 list 의 html 파일을 확인해보자. 여기서 주의할 점은, posts폴더에 직접 만드는 것이 아니라 posts안에 templates라는 폴더를 만들고, 그 안에 다시 posts를 만든 뒤에 list라는 html파일을 만들어야 한다는 것이다. 아래와 같은 코드를 list.html 파일안에 넣어준다. 

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>글 리스트</title>
</head>

<body>
    <a href="{% url 'posts:create' %}">글 생성하기</a>
    {% for post in posts %}
    <h2>제목: <a href="{% url 'posts:detail' post.id %}">{{ post.title }}</a></h2> <br>
    <h4>글쓴이: {{ post.writer }}</h4> <br>
    <h4>내용: {{ post.content }}</h4>
    ============================= <br>
    {% endfor %}
</body>

</html>

처음 시작 화면으로 보여줄 글 리스트 화면이다. 앞으로 진행하게 될 create에서의 글 생성하기 버튼도 미리 만들었다.

 

{% ~~ %}는 html에서 다른 html로 넘어갈 때의 목적지의 url을 표시해준다. 제목: <a href="{% url 'posts:detail' post.id %}">{{ post.title }}</a> 이 코드에서 {% url 'posts:detail' post.id %} 는 post의 제목을 입력했을 때 posts:detail에 해당하는 링크와 detail을 보고 싶은 post의 번호(id)를 를 url뒤에 붙여서 detail html 으로 이동함을 의미한다.

 

반면, {{ ~~ }}는 화면 내에서 읽을 수 있는 함수를 의미한다. 글쓴이: {{ post.writer }} 이는 '글쓴이'라는 string 뒤에 post의 writer 속성이 반환돼서 보여줌을 뜻한다.  

 

{% for post in posts %}    {% endfor %} 는 리스트 형태인 'posts'가 가지고 있는 개별 요소인 'post'들을 하나씩 호출해가며 아래 html을 적용해줌을 의미한다. html에서 사용할 수 있는 반복문의 느낌이다. 

 

∫ posts\urls.py

from django.urls import path
from . import views

app_name='posts'

위 라이브러리들을 불러와 주고, app_name을 'posts'라고 정의해서 위의 html에서 url뒤에 'posts:list'등의 표현이 가능하게 한다. 

urlpatterns = [
    path('', view=views.post_list, name='list'), 
]

urlpatterns 라고 이미 지정되어 있는 리스트에 path의 형태로 list의 url을 정의한다. 앞에서 설명했듯이, 우리는 기본 페이지를 list의 페이지로 만들 것이기 때문에, 기본 url이 곧 list이 url이 된다. 따라서 path바로 뒤에 ''이라고 빈 문자열을 만들어준다.

 

이때, view로 들어가서 글 리스트를 반환해주는 역할을 view=views.post_list가 하고, 이 url의 이름을 list라고 지정해서  html의 뒤에 'posts:list'을 사용할 수 있게 한다. 다시 말해, path('', view=views.post_list, name='list')는 url뒤가 '' 인 경로로 온 애들에 대해서는 view.post_list의 형태로 보여주고 그 이름은 list로 할 것이다는 의미이다. 


∇ Read-상세 페이지

read를 이용해서 만들어야 하는 기능은 두 가지이다. 1. 앞에서 만들었던 list페이지와, 2. 각 post의 제목을 클릭했을 때 해당 psot의 상세 정보를 볼 수 있는 페이지를 만들어야 한다. 이제 두 번째 post의 제목을 클릭했을 때 보이는 상세 detail페이지를 만들어보자. 

 

∫ posts\views.py

다음과 같은 코드를 추가로 입력해준다. 아까 작성했던 코드들과 크게 다르지 않다. 

def post_detail(request, post_id):
    '''
    Read(R)
    특정 포스트를 불러와서 상세정보를 보여준다
    '''
    post = Post.objects.get(id=post_id)
    ctx = {'post':post}

    return render(request, template_name='posts/detail.html', context=ctx)

 

∫ posts\templates\posts\detail.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>포스트 상세정보</title>
</head>
<body>

    <h1>{{ post.title }}</h1> <span>{{ post.writer }}</span>
    <p>{{ post.content }}</p>
    <p>{{ post.created_at }}</p>
    <p>{{ post.updated_at }}</p>
    <a href="{% url 'posts:list' %}">리스트로 돌아가기</a>
    <a href="{% url 'posts:update' post.id %}">수정하기</a> <br>
    <form action="{% url 'posts:delete' post.id %}"method='POST'>
        {% csrf_token %}
        <input type="submit" value='삭제하기'>
    </form>

</body>
</html>

이 역시도 위에서 설명한 내용과 크게 다르지는 않은데, update와 delete를 하기 위해서 나중에 만든 코드들을 미리 집어넣었다. GET메서드와는 달리, POST 메서드를 사용할 때는 항상 {% csrf_token %}을 코드에 넣어줘야 한다. 그렇지 않으면 오류가 생긴다. 이 코드의 의미는 정확히 모르겠다. 

 

∫ posts\urls.py

urlpatterns = [
    path('<int:post_id>/', view=views.post_detail, name='detail'),
]

urlpatterns에 위 코드를 추가로 넣어준다. 여기서 path를 보면 url바로 뒤에 int의 형식으로 post_id를 반환하는데, 이 숫자를 url바로 뒤에 입력하면 그 post의 detail view로 넘어가게 된다. 여기서 중요한 점은, 이 post_id는 한 번 생성되면 그 post를 지우고 다시 만들더라도 다른 post에 재할당되지 않는다. post르 특정할 수 있는 주민등록번호와 같은 개념이다. 

 

나머지는 Read - list를 만들었던 내용과 같다. 


∇ Create

이전의 read에서는 request의 메서드가 post 한 가지 밖에 없었다. 하지만 이 다음의 create, update, delete에서는 request의 메서드가 get method와 post method 모두를 사용하기 때문에 이 둘의 차이에 대해서 먼저 어렴풋이 확인해보자. (이 뒤에 나오는 내용은 틀린 내용이 있을 수 있다. 만약 틀린 내용을 발견하면 지적 바람.)

 

우선 get은 url입력 등의 역할, post는 저장하기 등의 역할을 수행한다. 다시 말해서, 우리가 url입력창에 www.youtube.com/channel/UC-dymoVfpH7D65HGcv-9Drg 를입력해서 바로 내운연 주식회사의 유툽 링크로 이동하는 것은 get의 방식이다. 반면, 내운연 주식회사의 유툽 동영상을 보고 개노잼이라고 악플을 작성하고 등록 버튼을 누르는 것은 post의 방식이다. 또, 많은 정보를 보낼 때는 post의 방식을 통해서 보낸다고 한다.

 

추가로, 둘의 차이라고 말하기는 조금 애매할 수 있지만 코드의 구현 방식이 다르다. 위의 detail.html에서 볼 수 있듯이, POST 메서드를 사용하면 form의 형식을 사용하고, 에러를 방지하기 위해서 {% csrf_token %}이 필요하다. 추가적인 내용은 아래의 코드와 같이 보면서 설명하겠다. 

 

이 둘의 정확한 차이에 대해서는 생활코딩의 유툽 페이지에 관련 영상이 있다고 하니 참고하면 이해하기가 쉬울 것 같다. 

 

∫ posts\views.py

def create_post(request):
    '''
    Create(C)
    포스트를 새로 생성한다
    request method==>GET, POST, PUT, DELETE
    '''
    if request.method == 'POST':
        # 글쓰기 칸을 '저장하기' 눌렀을 때
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save()
            return redirect('posts:list')
    else: 
        # get방식, 글쓰기 칸을 보여줄 때
        form = PostForm()
        ctx = {'form':form}
        return render(request, template_name='posts/post_form.html', context=ctx)

여기를 보면 두 가지의 방식으로 request가 들어올 수 있으므로, if-else문으로 POST와 GET에 대해서 케이스를 나눠 코드를 작성한다. 

둘 모두 form=PostForm() 을 사용하는데 이게 어떤 역할을 하는건지는 정확히 모르겠다

그리고 post 메서드로 request가 들어오면 form.is_valid() 함수를 통해서 True 혹은 False를 반환하고 만약 form이 valid한 형식이라면(True라면), form에 save해주고 post:list 페이지로 redirect해준다. 

 

만약 request가 get메서드라면, posts폴더에 잇는 post_form.html을 렌더링해서 보여준다. read와는 다르게, context를 post가 아니라 form으로 받는다. 이 차이는 잘 모르겠다. 

 

∫ posts\templates\posts\post_form.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>포스트 작성</title>
</head>
<body>
    <form action="" method="POST">
        {% csrf_token %}
        <table>
            {{ form.as_table }}
        </table>
        <input type="submit" value="저장하기">
    </form>
</body>
</html>

post를 생성하는데에 어떤 화면이 필요한지 생각해보자. 제목을 입력하는 칸, 내용을 입력하는 칸, 작성자를 입력하는 칸 등등 여러 요소가 필요하다. 그런데 django를 사용하면 이런 고민을 할 필요가 없다. 위에서 보이는 것 처럼 form태그를 만들고 method='POST'로 지정한 뒤에 {{ form.as_table }}을 만들어주면 된다. form태그이니만큼,  맨 밑에 submit버튼을 input타입으로 만드는 작업도 필요하다. 이게 어떻게 되는건지는 잘 모르겠다. method='POST'를 조금 뜯어봐야될 것 같다. 

 

∫ posts\urls.py

urlpatterns = [
    path('create/', view=views.create_post, name='create'),
]

이제는 익숙한 urls.py에 들어가서 urlpatterns에 위 내용을 추가해준다. 


∇ Update

글을 작성했다면 수정할 필요도 있다. 사실상 create와 비슷하다. 

 

∫ posts\views.py

def update_post(request, pk):
    '''
    Update
    포스트를 수정하는 뷰
    '''
    post = get_object_or_404(Post, id=pk)

    if request.method == 'POST':
        form = PostForm(request.POST, instance=post)
        if form.is_valid():
            post=form.save()

            return redirect('posts:detail', pk) # pk대신에 post.id도 들어갈 수 있음
    else:
        form = PostForm(instance=post)
        ctx={'form':form}
        return render(request, 'posts/post_form.html', ctx)

비슷하다고 했는데, 옮겨적고 나니 거의 동일한 것 같다.

 

조금 다른 점에 대해서 설명하자면, post = get_object_or_404(Post, id=pk) 라는 코드가 생겼다. get_object_or_404는 내가 원하는 페이지에 get방식으로 접근했는데 페이지가 없을 경우, 404에러를 띄우라는 의미이다. 우리가 아직 404에러 페이지를 디자인하거나 원하는대로 구성할 수 있는 않지만, 조금 더 배우게 되면 사용자의 행동에 따라서 각기 다른 에러를 보여줄 수 있다. 예를 들어서, 이미 지운 포스트에 접근하려고 한다면 "이미 삭제된 게시물입니다", 탈퇴한 사용자에게 메시지를 보내려고 한다면 "이미 탈퇴한 사용자입니다"등의 에러 메시지를 다르게 만드는 것이다. 

 

또, id=pk라는 코드가 생겼다. 이전까지는 post.id나 post_id를 사용했지만, 이제는 pk라는 함수?를 불러올 수 있다. pk는 primary key의 약자이고, 사실상 id와 같은 역할이다. 앞으로 update에서는 게시물의 id를 불러올 필요가 있을 때 pk라는 함수를 대신 사용해도 된다. 

 

∫ posts\templates\posts\detail.html

여기 update기능에는 별도의 html을 구성할 필요가 없다. 하지만, 이전에 작업했던 detail 템플릿에서  update의 역할을 하는 '수정하기' 버튼을 a태그로 감싸서 update기능을 하게 해 주어야 한다. 

<a href="{% url 'posts:update' post.id %}">수정하기</a> <br>

 

 

∫ posts\urls.py

urlpatterns = [
    path('<int:pk>/update/', view=views.update_post, name='update'),
]

urls.py 파일에서 path를 지정해주는 작업이다. detail에서 했던 것과 똑같은데, 다른 점이 있다면 앞서 생성한 pk함수를 사용했다. url바로 뒤에 오는 숫자를 <int : pk>로 지정해서 특정 포스트에 접근할 수 있도록 했다. 


∇ Delete

이제 마지막 delete의 내용이다. 여기서 get과 post의 방식의 차이가 한 번 더 등장한다. 만약 이 차이를 모른채 그냥 마구잡이로 쓴다면 문제가 생길 수도 있다. 

 

∫ posts\views.py

def delete_post(request, pk):
    '''
    Delete
    포스트를 삭제하는 뷰
    '''
    post = get_object_or_404(Post, pk=pk)

    if request.method == 'GET':
        return redirect('posts:detail', post.id)

    elif request.method == 'POST':
        post.delete()
        return redirect('posts:list')

ㅁ이번 view에서는 if-else문을 사용하지 않고 if-elif를 사용해서 get과 post의 방식을 따로 구현했다. 사실상 우리 페이지에서는 기능의 차이는 없지만, 현업에서는 if-else문을 잘 사용하지 않는다고 한다. 그 이유는 if에 해당하지 않은 여러 케이스가 있을 수 있는데, 현실적으로 그 모든 예외사항을 고려하기 힘들기 때문이다. 만약 else에 모든 예외를 때려넣으면 처음엔 되는것 같다가도 특이케이스가 등장하면 어떤 돌발상황이 나올지 모를 수 있기 때문에 if 와 elif를 모두 사용해서 케이스를 구분해주는것이 좋다. 

 

delete를 만들때는 새로운 html를 render를 할 필요가 없이 때문에 이미 만들어진 기능에 redirect만 해주면 된다. 

 

세션중에 누가 "delete의 기능에는 get과 post의 역할이 나눠질 필요가 없는 것 같다. delete의 요청이 들어오면 그냥 삭제만 하는 기능을 만들면 되지 않나?"라고 질문을 했다. 굉장히 똑똑하고 날카로운 질문이었던 것 같다. 그런데 결론적으로 말하면, 그렇게 하면 안된다. 일반적으로 post기능은 버튼 등을 눌렀을 때 행동을 정의하기 위해서 사용한다고 위에서 설명했는데, get 과 post를 구분하지 않으면 삭제하려는 명령어가 기본적인 페이지 접근, 즉 기본 설정인 get메서드로 실행되게 된다. url뒤에 delete를 붙여서 지우려는 페이지에 들어감과 동시에 바로 delete의 명령어가 실행되어 해당 포스트가 삭제된다. 굉장히 위험한 방식이다. 내가 쓴 글이 아니더라도 누군가가 마음대로 url을 입력함으로써 삭제할 수 있다는 문제가 있고, 사용자가 url을 잘못 입력했을 수도 있는데 한 번에 포스트가 삭제되면 당연히 안된다. 

 

∫ posts\templates\posts\detail.html

마찬가지로 추가로 생성할 html문서는 없다. 다만, detail 템플릿에서 삭제하기 버튼을 만들어서 post메서드로 삭제 기능을 구현하면 된다. 

<form action="{% url 'posts:delete' post.id %}"method='POST'>
    {% csrf_token %}
    <input type="submit" value='삭제하기'>
</form>

post 메서드이기 때문에 form태그를 만들었다. 정확히 어떤 역할을 하는지는 모르지만 오류가 나지 않기 위해서 {% csrf_token %}도 잊지 말고 넣어주자. 

 

∫ posts\urls.py

마지막으로 urls.py에 다음과 같은 코드를 넣어서 url을 정의해주면 모두 끝이 난다. 

urlpatterns = [
    path('<int:pk>/delete/', view=views.delete_post, name='delete'),
]

이제 이정도는 이지하다. 직접 쓰라고 하면 어렵겠지만 해석은 할만하다. 반복학습의 중요성인 것 같다. 


이렇게 장고를 이용해서 CRUD를 직접 구현해보는 세션이 끝났다.

작업한 코드 전체를 확인하고 싶은 사람들은 아래 깃헙 repostitory에 들어가서 다운받을 수 있다. 

 

junankim18/piro14_CRUDbasic

Contribute to junankim18/piro14_CRUDbasic development by creating an account on GitHub.

github.com

추가로, 저장된 post등 데이터에 관련된 정보는 아래 툴을 적용하면 쉽게 확인하고 edit할 수 있다고 한다. 지금 말고 나중에 이 툴을 이용해서 작업해보겠다. 

 

TablePlus | Modern, Native Tool for Database Management.

Modern, native client with intuitive GUI tools to create, access, query & edit multiple relational databases: MySQL, PostgreSQL, SQLite, Microsoft SQL Server, Amazon Redshift, MariaDB, CockroachDB, Vertica, Cassandra, and Redis.

tableplus.com

 

심화과제

심화: Profile 모델 만들기

필드종류: 이름, 나이, 소속, 설명, 생성일시, 수정일시

CRUD 모두 만들어 볼 것 

POST와 같은 방식으로

 

심화 과제도 나왔다. 나중에 도전해봐야겠다. 


이렇게 세션으로만 세 번째로 django에 대해서 배웠다. 이미 만들어져 있는 웹 프레임워크를 그대로 가져다가 쓰려니까 내장되어 있는 함수에 대해서 잘 알지 못해서 일일이 찾아보고 오류사항을 확인해야 하는 어려움이 있다. 그렇지만, 이렇게 세세한 부분까지 구현을 누군가가 해놓았기 때문에 속도를 내어서 만들 수 있는 것 같다. 이 모든 과정을 직접 하나씩 코딩해서 구현해야 했다고 하면 정말 어려웠을 것 같다. 그런 의미에서 20년도에 직접 만드려고 했던 시도는 얼마나 어리석은 행동이었는지 깨닫게 되었다. 그런데 무식하면 용감하다고 용감했기에 여기까지 올 수 있었던 것은 아닐까 하는 생각도 조금은 든다. 계속 무식하고 용감한 태도를 유지하고 싶다. 

 

관련글 더보기

댓글 영역