본문 바로가기
기술정리/Django

Django slug 필드를 활용하여 검색하기

by bingual 2024. 4. 6.
반응형

URL Pattern에 Slug를 포함하는 이유

URL에 Slug를 포함하게 되면 검색엔진최적화(SEO)를 개선하는 것에 도움이 되고 좀 더 직관적이게 사용자가 어느 페이지를 방문하고 있는지를 알 수 있어 사용자 친화적일 수 있다. 

pk와 slug을 동시에 사용하는 방법으로 로직을 작성하는것을 전제로 하고 있다.
Django ORM을 사용하기 때문에 pk를 노출하는건 보안상 문제가 되지 않지만 불편하다면 해당 값을 해시 데이터로 암호화하여 노출시키거나 리버스 프록시 설정을 하여 URL을 숨기는 방법이 있다.

 

 

Post Model에 Slug를 적용하기

 

models.py

class Post(models.Model):
    slug = models.SlugField(
        max_length=120,
        allow_unicode=True,
        help_text="title 필드로부터 자동 생성합니다.",
    )
    
    def slugify(self, force=False):
        if force or not self.slug:
            self.slug = slugify(self.title, allow_unicode=True)
            self.slug = self.slug[:120]

    def save(self, *args, **kwargs):
        self.slugify()
        super().save(*args, **kwargs)

 

  • pk를 그대로 활용하면서 slug를 사용하기 때문에 unique 옵션은 적용하지 않는다.
  • 레코드를 생성할 때 slugify를 호출하여 title 필드를 slug로 변환해준다.
  • 마이그레이션 파일을 생성할 때 slug의 기본 값은 빈 문자열로 할당한다.

 

 

0004_post_slug.py

# Generated by Django 4.2.7 on 2024-04-06 19:15

from django.db import migrations, models
from django.utils.text import slugify


def update_slug_if_empty(apps, schema_editor):
    Post = apps.get_model("blog", "Post")
    post_qs = Post.objects.filter(slug="")
    for post in post_qs:
        post.slug = slugify(post.title, allow_unicode=True)
    Post.objects.bulk_update(post_qs, ["slug"])


class Migration(migrations.Migration):

    dependencies = [
        ("blog", "0003_category_post_category"),
    ]

    operations = [
        migrations.AddField(
            model_name="post",
            name="slug",
            field=models.SlugField(
                allow_unicode=True,
                default="",
                help_text="title 필드로부터 자동 생성합니다.",
                max_length=120,
            ),
            preserve_default=False,
        ),
        migrations.RunPython(update_slug_if_empty, migrations.RunPython.noop),
    ]

 

마이그레이션시 이미 생성된 게시글에 slug를 추가하는 작업 또한 추가하도록 한다.

 

 

 

urls.py

from django.urls import path

from blog import views

app_name = "blog"

urlpatterns = [
    path("<int:pk>/", views.post_detail, name="post_detail"),
    path("<int:pk>/<str:slug>/", views.post_detail, name="post_detail"),
]

 

테스트를 위한 패턴 설정을 해주도록 한다.

 

 

 

views.py

from django.http import HttpResponse
from django.shortcuts import render, get_object_or_404, redirect
from django.views.generic import DetailView

from blog.models import Post


# FBV
def post_detail(request, pk, slug=None):
    post = get_object_or_404(Post, pk=pk)
    if post.slug and (slug is None or post.slug != slug):
        return redirect("blog:post_detail", pk=pk, slug=post.slug, permanent=True)
    return HttpResponse(f"{post.pk}번 글의 {post.slug}")


# CBV
class PostDetailView(DetailView):
    model = Post
    context_object_name = "post"
    slug_url_kwarg = "slug"
    pk_url_kwarg = "pk"

    def get(self, request, *args, **kwargs):
        self.object = self.get_object()
        if self.object.slug and (
            self.kwargs.get(self.slug_url_kwarg) is None
            or self.object.slug != self.kwargs.get(self.slug_url_kwarg)
        ):
            return redirect(
                "blog:post_detail",
                pk=self.object.pk,
                slug=self.object.slug,
                permanent=True,
            )
        return HttpResponse(f"{self.object.pk}번 글의 {self.object.slug}")


post_detail = PostDetailView.as_view()

 

테스트를 위하여 뷰 설정도 해주도록 한다.

 

 

결과

 

blog/pk/ 주소로 접속 시 Post 모델에 slug 필드가 존재한다면 자동으로 리다이렉트가 되어 이동하는 것을 확인할 수 있다.