WeniVooks

검색

Django 베이스캠프

CRUD 구현과 미디어 파일

1. 게시물 생성과 삭제

Django의 ORM(Object-Relational Mapping)을 사용하여 게시판의 기본 기능인 게시물 생성과 삭제를 구현해보겠습니다. 이는 CRUD(Create, Read, Update, Delete) 작업 중 Create와 Delete에 해당합니다. 여기서는 간단하게 동작되는 원리를 설명하기 위해, 인증과 권한 관리는 고려하지 않습니다. 또한 매우 간단한 형태의 CRUD를 위해 URL에 직접 데이터를 전달하는 방식을 사용합니다. 이는 원리를 파악하기 위한 것이고, 실제 서비스에서는 사용하지 않는 방식이므로 참고하시기 바랍니다.

이전 3-1 실습에서 만든 모델을 이용해서 실습을 진행합니다. 먼저 blog 앱의 urls.py와 views.py 파일을 수정하여 게시물 생성과 삭제 기능을 추가해 봅시다.

1.1 blog 앱의 urls.py 수정

urls.py파일에 url패턴을 추가합니다.

# blog > urls.py
from django.urls import path
from . import views
 
urlpatterns = [
    path("", views.blog_list, name="blog_list"),
    path("<int:pk>/", views.blog_detail, name="blog_detail"),
    path("create/<str:title>/", views.blog_create, name="blog_create"),
    path("delete/<int:pk>/", views.blog_delete, name="blog_delete"),
]
# blog > urls.py
from django.urls import path
from . import views
 
urlpatterns = [
    path("", views.blog_list, name="blog_list"),
    path("<int:pk>/", views.blog_detail, name="blog_detail"),
    path("create/<str:title>/", views.blog_create, name="blog_create"),
    path("delete/<int:pk>/", views.blog_delete, name="blog_delete"),
]

이 코드에서 새로 추가된 URL 패턴은 다음과 같습니다. 실제 서비스가 동작하는 것처럼 구현하는 것이 아니라, 여러분의 이해를 돕고자 간단하게 구성한 예제입니다.

  1. path("create/<str:title>/", views.blog_create, name="blog_create") 이 패턴은 게시물 생성을 위한 URL로, <str:title> 부분은 게시물의 제목을 URL에 직접 입력하여 전달받는 방식입니다. 예를 들어 /blog/create/안녕하세요/라고 입력하면 '안녕하세요'라는 제목의 게시물이 생성됩니다.

  2. path("delete/<int:pk>/", views.blog_delete, name="blog_delete") 이 패턴은 게시물 삭제를 하는 URL입니다. 위의 생성 URL과는 조금 다르게, <int:pk> 부분에 삭제할 게시물의 번호를 URL에 직접 입력하여 전달받는 방식입니다. 만약 /blog/delete/3/이라고 입력하면 3번 게시물이 삭제되는 것입니다.

1.2 blog 앱의 views.py 수정

views.py 파일에 URL에 접속하면 실행될 view 함수들을 추가합니다.

# blog > views.py
from django.shortcuts import render, redirect
from .models import Post
 
def blog_list(request):
    blogs = Post.objects.all()
    context = {"object": blogs}
    return render(request, "blog/blog_list.html", context)
 
def blog_detail(request, pk):
    blog = Post.objects.get(pk=pk)
    context = {"object": blog}
    return render(request, "blog/blog_detail.html", context)
 
def blog_create(request, title):
    contents = f"hello world {title}"
    Post.objects.create(title=title, contents=contents)
    return redirect("blog_list")
 
def blog_delete(request, pk):
    q = Post.objects.get(pk=pk)
    q.delete()
    return redirect("blog_list")
# blog > views.py
from django.shortcuts import render, redirect
from .models import Post
 
def blog_list(request):
    blogs = Post.objects.all()
    context = {"object": blogs}
    return render(request, "blog/blog_list.html", context)
 
def blog_detail(request, pk):
    blog = Post.objects.get(pk=pk)
    context = {"object": blog}
    return render(request, "blog/blog_detail.html", context)
 
def blog_create(request, title):
    contents = f"hello world {title}"
    Post.objects.create(title=title, contents=contents)
    return redirect("blog_list")
 
def blog_delete(request, pk):
    q = Post.objects.get(pk=pk)
    q.delete()
    return redirect("blog_list")

각 함수를 간단히 살펴봅시다.

  • blog_create
def blog_create(request, title):
    contents = f"hello world {title}"
    Post.objects.create(title=title, contents=contents)
    return redirect("blog_list")
def blog_create(request, title):
    contents = f"hello world {title}"
    Post.objects.create(title=title, contents=contents)
    return redirect("blog_list")

blog_create(request, title) 함수는 '/blog/create/제목/' 형식의 URL에 접속하면 실행됩니다. 이 함수는 URL에서 받은 제목을 사용하여 새 게시물을 만듭니다. Django ORM의 Post.objects.create() 메서드로 새로운 Post 객체를 생성하고 저장합니다. 별도의 변수를 만들어 q.save()를 통해 저장할 필요는 없습니다. create()은 생성과 동시에 저장합니다. 작업이 완료되면 redirect("blog_list")를 통해 사용자를 게시물 목록 페이지로 이동됩니다.

  • blog_delete
def blog_delete(request, pk):
    q = Post.objects.get(pk=pk)
    q.delete()
    return redirect("blog_list")
def blog_delete(request, pk):
    q = Post.objects.get(pk=pk)
    q.delete()
    return redirect("blog_list")

blog_delete(request, pk) 함수는 '/blog/delete/숫자/' 형식의 URL에 접속하면 실행됩니다. 이 함수는 URL에서 받은 숫자(ID)를 사용하여 해당 게시물을 찾아 삭제합니다. Django ORM의 Post.objects.get(pk=pk)로 지정된 ID의 게시물을 데이터베이스에서 찾고, q.delete()로 그 게시물을 삭제합니다. 삭제 작업이 완료되면, 게시물 생성 때와 마찬가지로 redirect("blog_list")를 통해 사용자를 게시물 목록 페이지로 이동합니다.

1.3 서버 실행 및 작동 테스트

이제 서버를 실행하고 새로 추가한 기능들을 테스트해봅시다.

python manage.py runserver
python manage.py runserver
  1. 게시물 생성 다음과 같은 URL을 브라우저에 입력해보세요.

    http://127.0.0.1:8000/blog/create/orm/
    http://127.0.0.1:8000/blog/create/jeju/
    http://127.0.0.1:8000/blog/create/11/
    http://127.0.0.1:8000/blog/create/orm/
    http://127.0.0.1:8000/blog/create/jeju/
    http://127.0.0.1:8000/blog/create/11/

    각 URL에 접속하면, 해당 제목을 가진 새로운 게시물이 생성되고 게시물 목록 페이지로 이동합니다.

  2. 게시물 삭제 생성된 게시물의 ID를 확인한 후, 다음과 같은 URL을 입력해보세요.

    http://127.0.0.1:8000/blog/delete/1/
    http://127.0.0.1:8000/blog/delete/2/
    http://127.0.0.1:8000/blog/delete/1/
    http://127.0.0.1:8000/blog/delete/2/

    각 URL에 접속하면, 해당 ID를 가진 게시물이 삭제되고 게시물 목록 페이지로 이동합니다.

2. 이미지 업로드 실습

이번에는 이미지 업로드 기능을 추가하여 데이터가 어디에 저장되고 어떻게 읽어오는지 확인해보겠습니다.

pip install pillow
pip install pillow

이전 장에서 개발 환경을 설정할 때 Pillow를 이미 설치했습니다. 혹시 설치하지 않았거나 확실하지 않다면, 위 명령어를 실행하여 Pillow를 설치해주시면 됩니다.

2.1 models.py 수정

먼저, 우리의 블로그 게시물에 이미지를 추가할 수 있게 만들어 봅시다. models.py 파일을 수정합니다.

# blog > models.py
from django.db import models
 
class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    main_image = models.ImageField(upload_to='blog/', blank=True, null=True) 
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
 
    def __str__(self):
        return self.title
# blog > models.py
from django.db import models
 
class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    main_image = models.ImageField(upload_to='blog/', blank=True, null=True) 
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
 
    def __str__(self):
        return self.title

여기서 main_imgage라는 이름의 새로운 이미지 필드를 추가했습니다. 이 필드가 바로 이미지를 저장할 수 있게 해주는 필드입니다. upload_to='blog/'는 업로드된 이미지를 'blog/' 폴더에 저장하라고 Django에게 알려주는 것이고, blank=True는 필드가 필수가 아님을, null=True는 이 필드 이전에 채워지지 않았다고 한다면 null을 허용하여 빈 공간으로 남겨두라는 의미입니다.

모델을 변경했으므로 데이터베이스에 이 변경사항을 적용해야 합니다. 아래 명령어를 입력해 변경사항을 적용합니다.

python manage.py makemigrations
python manage.py migrate
python manage.py makemigrations
python manage.py migrate
2.2 settings.py 수정

이제 Django가 이미지 파일을 어디에 저장하고, 어떻게 불러올지 지정해줘야합니다. settings.py 파일에 미디어 파일 설정을 추가합니다.

#settings.py
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
 
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
#settings.py
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
 
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
2.3 media 폴더 생성

프로젝트의 가장 상위 폴더에 static, media 폴더를 만들어주세요.

📁03_1_model/
┣━ 📁blog/
┣━ 📁config/
┣━ 📁templates/
┣━ 📁main/
┣━ 📁media/
┣━ 📁static/
┗━ 📁venv/
📁03_1_model/
┣━ 📁blog/
┣━ 📁config/
┣━ 📁templates/
┣━ 📁main/
┣━ 📁media/
┣━ 📁static/
┗━ 📁venv/

media 폴더에는 웹사이트에서 업로드한 이미지가 저장됩니다.

2.4 서버 실행 및 작동 테스트

서버를 실행해주세요.

python manage.py runserver
python manage.py runserver

웹 브라우저에서 다음 URL로 접속하여 관리자 페이지에 접속합니다.

http://127.0.0.1:8000/admin
http://127.0.0.1:8000/admin

관리자 페이지에서 이미지를 포함한 게시물을 3개 만들어보세요. 이미지 업로드 기능이 제대로 작동하는지 확인해봅시다. 또한 같은 이미지를 업로드하여 이미지가 어떻게 업로드 되는지 확인해보세요.

2.5 url.py 수정

이제 웹에서 업로드한 이미지를 볼 수 있게 해봅시다. 현재 이미지는 URL이 없는 상태입니다. 따라서 웹 페이지에서 이 이미지를 불러오지 못합니다. 이를 위해 정적 파일을 서빙하기 위한 static 모듈을 사용합니다. static 모듈은 해당하는 URL을 지정된 폴더로 연결해주는 역할을 합니다.

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
 
urlpatterns = [
    path("admin/", admin.site.urls),
    path("blog/", include("blog.urls")),
]
 
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
 
urlpatterns = [
    path("admin/", admin.site.urls),
    path("blog/", include("blog.urls")),
]
 
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

추가한 마지막 줄인 urlpatterns += static(URL, 폴더명) 코드는 미디어 파일을 서빙하기 위한 URL 패턴을 추가합니다. 이 URL로 웹사이트에서 업로드된 파일에 접근할 수 있습니다. settings.MEDIA_URL은 웹 브라우저에서 미디어 파일에 접근할 때 사용되는 URL 경로입니다. document_root는 실제 파일이 저장된 폴더의 경로를 지정합니다. 예를 들어, http://mysite.com/media/image.jpg와 같은 주소로 접근했을 때 중간에 /media/가 있기 때문에 media 폴더로 들어가 이미지에 접근할 수 있습니다.

2.6 템플릿 수정

관리자 페이지에서 올린 파일을 템플릿상에서 볼 수 있도록 템플릿을 수정합니다. 이미지 파일을 표시할 때는 img 태그를 사용합니다. 이미지 파일이 없을 수도 있으므로, 이미지 파일이 있을 때만 표시되도록 조건문을 사용합니다. 만약 이미지가 없는 상태에서 이미지를 불러오려고 하면 오류가 발생하게 됩니다.

  • blog_detail.html
<!-- templates > blog > blog_detail.html -->
<h1>blog_detail</h1>
<h2>{{ object.title }}</h2>
<p>{{ object.id }}</p>
<p>{{ object.content }}</p>
<p>{{ object.created_at }}</p>
<p>{{ object.updated_at }}</p>
{% if object.main_image %}
    <img src="{{ object.main_image.url }}" alt="">
{% endif %}
<a href="{% url 'blog_list' %}">목록</a>
<!-- templates > blog > blog_detail.html -->
<h1>blog_detail</h1>
<h2>{{ object.title }}</h2>
<p>{{ object.id }}</p>
<p>{{ object.content }}</p>
<p>{{ object.created_at }}</p>
<p>{{ object.updated_at }}</p>
{% if object.main_image %}
    <img src="{{ object.main_image.url }}" alt="">
{% endif %}
<a href="{% url 'blog_list' %}">목록</a>
2.7 서버 실행 및 작동 테스트

이제 서버를 실행해서 작동이 되는지 확인해봅시다.

python manage.py runserver
# 만약 4번 게시물에 이미지를 저장하였다면
# 127.0.0.1:8000/blog/4/ 로 접속
python manage.py runserver
# 만약 4번 게시물에 이미지를 저장하였다면
# 127.0.0.1:8000/blog/4/ 로 접속

생성된 게시물의 상세 페이지에서 이미지가 표시되는 것을 확인할 수 있습니다.

만약 이미지가 보이지 않는다면, 파일이 제대로 저장되었는지 확인해보세요. media/blog/ 폴더에 이미지 파일이 있어야 합니다. 같은 이름의 파일을 여러 번 업로드하면 Django는 자동으로 파일 이름에 난수를 추가하여 저장합니다.

3. 검색 기능 구현

검색 기능을 구현해봄으로 어떻게 사용자 요청이 서버로 전달되고, 서버에서 데이터를 필터링하는지 알아보겠습니다.

3.1 views.py 수정

검색 기능을 구현하기 위해 views.py 파일을 수정해야 합니다. 아래의 코드는 검색 로직을 포함한 blog_list 뷰 함수입니다.

#views.py
from django.shortcuts import render
from .models import Post
 
def blog_list(request):
    if request.GET.get("q"):
        q = request.GET.get("q")
        print(request.GET)
        print(q)
        blogs = Post.objects.filter(title__icontains=q)
    else:
        blogs = Post.objects.all()
    context = {"object_list": blogs}
    return render(request, "blog/blog_list.html", context)
#views.py
from django.shortcuts import render
from .models import Post
 
def blog_list(request):
    if request.GET.get("q"):
        q = request.GET.get("q")
        print(request.GET)
        print(q)
        blogs = Post.objects.filter(title__icontains=q)
    else:
        blogs = Post.objects.all()
    context = {"object_list": blogs}
    return render(request, "blog/blog_list.html", context)

사용자가 전달한 정보는 request에 담겨져 있습니다. GET으로 전송한 정보는 request.GET에, POST로 전송한 정보는 request.POST에 담겨져 있습니다. 위 코드에서는 단지 GET으로 넘어온 정보가 있는지만 확인합니다. 이렇게 코드를 변경하셨다면 서버를 실행해주세요. 이미 실행중이면 다시 실행하지 않아도 됩니다.

python manage.py runserver
# 127.0.0.1:8000/blog/?q=hello
python manage.py runserver
# 127.0.0.1:8000/blog/?q=hello

위 URL로 접속하면, 콘솔에 {'q': 'hello'}hello가 출력됩니다. 이는 사용자가 입력한 검색어가 서버로 전달되었음을 의미합니다. 이제 이 키워드를 가지고 검색 기능을 구현해봅시다.

# views.py
from django.shortcuts import render
from django.db.models import Q
from .models import Post
 
 
def blog_list(request):
    if request.GET.get("q"):
        q = request.GET.get("q")
        blogs = Post.objects.filter(
            Q(title__icontains=q) & Q(content__icontains=q)
        ).distinct()
    else:
        blogs = Post.objects.all()
    context = {"object_list": blogs}
    return render(request, "blog/blog_list.html", context)
# views.py
from django.shortcuts import render
from django.db.models import Q
from .models import Post
 
 
def blog_list(request):
    if request.GET.get("q"):
        q = request.GET.get("q")
        blogs = Post.objects.filter(
            Q(title__icontains=q) & Q(content__icontains=q)
        ).distinct()
    else:
        blogs = Post.objects.all()
    context = {"object_list": blogs}
    return render(request, "blog/blog_list.html", context)

만약 검색어가 있었다면 filter를 하고, 검색어가 없었다면 all을 하는 구조로 짜여져 있습니다. 여기서 특이한 점이 Q 객체를 사용했다는 점입니다. Q객체는 Django에서 제공하는 객체로, 복잡한 조건을 사용할 때 유용합니다. Q 객체를 사용하면 여러 조건을 조합하여 검색할 수 있습니다. 위 코드에서는 제목과 내용에서 검색어가 포함된 게시물을 찾기 위해 Q 객체를 사용했습니다. Q 객체를 통해 여러개의 filter를 or 연산하였고, 따라서 만족하는 결과물에서 distinct()로 중복 결과를 제거했습니다.

3.1.1 Q 객체

Django의 Q 객체는 조건이나 복잡한 조건부 검색을 구현할 때 사용하는 유용한 도구 입니다. 예를 들어서, 우리의 검색 기능에서는 아래 예시와 같이 Q 객체를 사용했습니다.

Q(title__icontains=q) | Q(content__icontains=q)
Q(title__icontains=q) | Q(content__icontains=q)

| 연산자는 OR 조건을 나타냅니다. 이 코드는 제목에 검색어가 포함되어 있거나 아니면(OR) 내용에 검색어가 포함되어 있는 조건을 표현합니다. OR 연산 말고도 AND 연산을 사용할 수도 있습니다. 예를 들어, 아래와 같이 사용할 수 있습니다.

Q(title__icontains=q) & Q(content__icontains=q)
Q(title__icontains=q) & Q(content__icontains=q)

이 코드는 제목에 검색어가 포함되어 있고(AND) 내용에 검색어가 포함되어 있는 조건을 표현합니다. 만약 이렇게 Q 객체를 사용하지 않으면 아래와 같이 코드를 작성해야 합니다.

Post.objects.filter(title__icontains=q, content__icontains=q)
Post.objects.filter(title__icontains=q, content__icontains=q)

Q 객체를 사용한다고 성능을 향상시키진 않습니다. 다만 filter()만으로는 OR 조건 등 다양한 조건을 복합적으로 사용할 수 없기 때문에 Q 객체를 사용합니다. 또한 Q 객체를 사용할 경우 복합 적용된 조건이 하나의 쿼리를 생성하게 됩니다. filter를 여러번 하면 각각의 filter에 대한 쿼리가 생성되어 성능이 떨어질 수 있습니다. 이러한 Q 객체 말고도 아래와 같은 F 객체도 있습니다.

from django.db.models import F

# 모든 제품의 가격을 10% 인상
Product.objects.update(price=F('price') * 1.1)
from django.db.models import F

# 모든 제품의 가격을 10% 인상
Product.objects.update(price=F('price') * 1.1)

F 객체는 주로 모델의 필드값을 나타낼 때 사용합니다. F 객체는 데이터베이스 수준에서 필드 값을 참고하고 연산할 수 있습니다.

3.2 템플릿에 검색 폼 추가

다음으로, 사용자가 검색어를 입력할 수 있는 폼을 템플릿에 추가합니다.

<h1>게시판</h1>
<form action="" method="get">
    <input name="q" type="search">
    <button type="submit">검색</button>
</form>
<ul>
    {% for blog_detail in object_list %}
    <li>
        <a href="{% url 'blog_detail' blog_detail.id %}">{{ blog_detail.title }}</a>
        <p>{{blog_detail.content}}</p>
        {% if blog_detail.main_image %}
            <img src="{{ blog_detail.main_image.url }}" alt="" style="width: 100px;">
        {% endif %}
    </li>
    {% endfor %}
</ul>
<h1>게시판</h1>
<form action="" method="get">
    <input name="q" type="search">
    <button type="submit">검색</button>
</form>
<ul>
    {% for blog_detail in object_list %}
    <li>
        <a href="{% url 'blog_detail' blog_detail.id %}">{{ blog_detail.title }}</a>
        <p>{{blog_detail.content}}</p>
        {% if blog_detail.main_image %}
            <img src="{{ blog_detail.main_image.url }}" alt="" style="width: 100px;">
        {% endif %}
    </li>
    {% endfor %}
</ul>

위 코드를 적용하면, 우리의 웹 페이지에 검색 기능이 추가됩니다. form 태그로 인해 페이지 상단에 검색창과 '검색' 버튼이 생성됩니다. 사용자가 검색창에 키워드를 입력하고 '검색' 버튼을 클릭하거나 Enter 키를 누르면 검색이 실행됩니다. 이 때, 입력된 검색어는 URL의 쿼리 매개변수로 서버에 전송됩니다. url에 ?q=검색어가 추가된 것을 볼 수 있습니다.

results = BlogPost.objects.filter(Q(title__icontains=q) | Q(content__icontains=q))
results = BlogPost.objects.filter(Q(title__icontains=q) | Q(content__icontains=q))

서버에서는 전송받은 검색어를 이용해 데이터베이스를 검색합니다. Q 객체를 사용한 우리의 검색 로직은 게시물의 제목과 내용 중 검색어가 포함된 게시물만을 필터링합니다. 검색 결과에 해당하는 게시물만이 페이지에 표시됩니다. 만약 검색어에 해당하는 게시물이 없다면, 아무 게시물도 표시되지 않습니다.

4. 참고 사항

Q객체에 대해서 더 궁금한 사항이 있다면, 아래의 장고 공식 문서를 참고하세요.

Django 공식 문서- Q 객체
3.2 Django ORM3.4 DB 모델 관계1