🧩

019 태그 검색

 
이번에는 태그와 검색 기능을 구현해보겠습니다.
 
post_list.html에 아직 포스트의 content를 노출시킨 적이 없습니다. 포스트의 content를 노출하는 부분을 추가해줍니다. div.likes.m_text 태그 아래에 추가합니다.
 
파일명 : post/templates/post/post_list.html
<div class="likes m_text"> <span id="like-count-{{ post.id }}">좋아요{{ post.like_count}}개 </span> <span id="bookmark-count-{{ post.id }}"> 북마크{{ post.bookmark_count}} 개</span> </div> <div class="content">{{ post.content }}</div>
 
이제 컨텐츠가 출력되는 것을 볼 수 있습니다.
notion imagenotion image
 
인스타그램을 보시면 글 내용 중 해시태그가 있는 부분은 자동으로 링크로 표시가 되고, 클릭하면 해당 해시태그와 관련된 게시글이 검색되는 시스템입니다. 지금은 검색창과 태그 기능이 구현 되어있지 않습니다. 글의 content에 있는 내용 중에 정규표현식을 이용해 해시태그를 분리하도록 하겠습니다. 링크를 주고 태그 모델에다가 저장하는 것까지 해보도록 하겠습니다.
notion imagenotion image
 

1. Tag 모델

post/models.pyTag 모델을 추가합니다.
 
파일명 : post/models.py
class Tag(models.Model): name = models.CharField(max_length=140, unique=True) def __str__(self): return self.name
 
tag_set 필드를 Post 모델에 추가합니다. 그리고 포스트의 content 중에 태그가 있다면 태그를 저장해주는 함수 tag_save를 추가합니다. re.findall는 정규표현식을 이용한 문자열 처리 함수 중 하나입니다. 주어진 정규표현식에 매칭되는 부분 문자열 리스트를 return 합니다.
 
파일명 : post/models.py
import re ... class Post(models.Model): ... tag_set = models.ManyToManyField('Tag', blank=True) ... def tag_save(self): tags = re.findall(r'#(\w+)\b', self.content) if not tags: return for t in tags: tag, tag_created = Tag.objects.get_or_create(name=t) self.tag_set.add(tag) # NOTE: ManyToManyField 에 인스턴스 추가
 
이후 데이터베이스에 적용 합니다.
(venv)root@goorm:/workspace/instaclone/instaclone# python manage.py makemigrations
 
notion imagenotion image
 
(venv)root@goorm:/workspace/instaclone/instaclone# python manage.py migrate
 
notion imagenotion image
 
그리고 admin.pyTag를 추가하도록 하겠습니다.
 
파일명 : post/admin.py
from .models import Post, Like, Bookmark, Comment, Tag ... @admin.register(Tag) class TagAdmin(admin.ModelAdmin): list_display = ['name']
 

2. 커스텀 필터

여기서 부터 중요한 부분입니다. 포스트의 content에서 태그들을 해당 태그에 해당하는 포스트들로 이동하는 링크로 바꾸어주는 커스텀 필터를 만들 것입니다.
post 앱에 templatetags 폴더를 하나 추가하도록 하겠습니다. 그리고 안에 __init__ .py, post_extras.py 파일을 만들도록 하겠습니다.
notion imagenotion image
post_extras.py 파일에 add_link 함수를 다음과 같이 작성합니다. 템플릿에 사용될 필터를 정의하는 것 입니다.
 
파일명 : post/templatetags/post_extras.py
from django import template import re # register는 유효한 tag library를 만들기 위한 모듈 레벨의 인스턴스 객체이다. register = template.Library() @register.filter def add_link(value): content = value.content # 전달된 value 객체의 content 멤버변수를 가져온다. tags = value.tag_set.all() # model.py tag_set안에 전달된 value 객체의 tag_set 전체를 가져오는 queryset을 리턴한다. for tag in tags: # tags의 각각의 인스턴를(tag)를 순회하며, content 내에서 해당 문자열을 => 링크를 포함한 문자열로 replace 한다. content = re.sub(r'\#'+tag.name+r'\b', '<a href="/post/explore/tags/'+tag.name+'">#'+tag.name+'</a>', content) # re.sub(pattern, repl, string) string에서 pattern과 매치하는 텍스트를 repl로 치환한다 return content # 원하는 문자열로 치환이 완료된 content를 리턴한다.
 
이제 템플릿에서 add_link 필터를 사용할 수 있게 되었습니다. "explore/tags/" URL은 아직 만들지 않았습니다. urls.py로 가셔서 url을 하나 생성하도록 하겠습니다. 해당 <tag>로 검색하는 요청 URL 입니다.
 
파일명 : post/urls.py
urlpatterns = [ ... path('explore/tags/<tag>/', post_list, name='post_search'), ]
 
post_list.html에서 content를 표시하는 부분에 필터를 적용시킵니다. add_link는 우리가 방금 만든 필터이고, safelinebreaksbr는 장고 내장 필터입니다.
 
파일명 : post/templates/post/post_list.html
<div class="content">{{ post.content|add_link|safe|linebreaksbr }}</div>
 
add_link 태그를 사용하기 위해서는 맨 위에 내용을 추가해야 합니다. post_extras.py 파일을 로드하는 것입니다.
 
파일명 : post/templates/post/post_list.html
{% extends "post/layout.html" %} {% load static %} {% load post_extras %} ...
 

3. post_list 함수 수정

이번에는 views.py 위에서 부터 수정하도록 하겠습니다. 필요한 Tag 모델과 django.db.models.Count를 import 합니다.
 
파일명 : post/views.py
from django.db.models import Count ... from .models import Post, Like, Comment, Tag
 
우리가 포스트를 검색할 때 태그를 통해 검색 할 수 있습니다. 태그를 처음에는 없는 것으로 해야 Post 들이 문제 없이 나타납니다. 태그가 들어왔을때는 태그를 이용해 내용을 필터링 할 수 있습니다. post_list 함수에 태그에 대한 내용을 추가하도록 하겠습니다.
 
"post/explore/tags/<tag>/" URL로 요청이 들어올 때 tag 인자를 받을 수 있도록 post_list 함수에 tag 매개변수를 추가합니다.
 
파일명 : post/views.py
def post_list(request, tag=None): ...
 
이전에는 post_list 변수에 post의 내용을 전부 다 가져왔었습니다.
 
파일명 : post/views.py
post_list = Post.objects.all()
 
이제는 태그로 검색되었을 때는 그 태그에 해당하는 포스트만 필요하기 때문에 위의 코드를 다음과 같이 수정합니다.
 
파일명 : post/views.py
if tag: post_list = Post.objects.filter(tag_set__name__iexact=tag) else: post_list = Post.objects.all()
 
[심화]
지금까지 만든 모델들 간의 1:N 또는 N:M 관계가 많습니다. 이런 경우에 조인(join)연산이 빈번하게 일어납니다. 조인 연산은 비용이 많이 들기 때문에 필요할 때마다 조인하는 것은 비효율적이므로 미리 조인을 해놓고 사용하면 좋습니다. prefetch_related 또는 selected_related 메서드를 사용할 수 있습니다.
 
post_list를 다음과 같이 작성할 수 있습니다.
 
파일명 : post/views.py
if tag: post_list = Post.objects.filter(tag_set__name__iexact=tag) \ # 위에서 받은 태그를 대소문자 구분없이 tag_set_name로 검색한다. .prefetch_related('tag_set', 'like_user_set__profile', 'comment_set__author__profile', # 1:1, 1:N, M:N 가능 'author__profile__follower_user', 'author__profile__follower_user__from_user') \ .select_related('author__profile') # 1:1의 관계에서만 사용 else: post_list = Post.objects.all() \ .prefetch_related('tag_set', 'like_user_set__profile', 'comment_set__author__profile', 'author__profile__follower_user', 'author__profile__follower_user__from_user') \ .select_related('author__profile')
 
하지만 최적화는 프로젝트의 개선을 위한 단계이지 완성을 위한 단계는 아니므로 우선순위가 낮습니다. 어려운 내용이니 추후에 쿼리에 익숙해진 후에 공부하는 것을 추천드립니다.
 
아래쪽에 post_new 함수 부분에서 주석처리해 놓은 부분이 있었습니다. post도 저장하는데 post의 태그도 저장하는 부분의 주석을 해제하겠습니다.
 
파일명 : post/views.py
@login_required def post_new(request): if request.method == 'POST': form = PostForm(request.POST, request.FILES) if form.is_valid(): ... post.save() post.tag_save() ...
 
이제 해시태그가 포함된 글을 작성하면, 해시태그에 링크가 잡히는 것을 확인하실 수 있습니다.
notion imagenotion image
 
이 링크는 templatetags/post_extras.pyadd_link 필터에서 정의를 해둔 내용입니다.
 
파일명 : post/templatetags/post_extras.py
content = re.sub(r'\#'+tag.name+r'\b', '<a href="/post/explore/tags/'+tag.name+'" style="color: #00376B;">#'+tag.name+'</a>'
 
이 부분을 다시 보시면 #으로 시작하는 태그를 찾게 되면 a 태그를 입혀지도록 하였습니다. 링크를 누르면 주소창에 *.goorm.io/post/explore/tags/해당태그명 으로 이동하고, 해당 태그의 게시글의 리스트가 나오게 됩니다.
 

4. 검색 창

이제 검색창에서도 태그가 검색되도록 검색창을 수정하겠습니다.
 
config/templates/layout.htmldiv.search_field 태그 안의 내용을 다음과 같이 수정합니다. 검색어를 담아서 post_list URL로 POST 방식으로 요청을 보냅니다.
 
파일명 : config/templates/layout.html
<div class="search_field"> <form class="search-form" action="{% url 'post:post_list' %}" method="post"> {% csrf_token %} <input class="tag-search" type="text" name="tag" placeholder="태그검색" pattern="#?[\wㄱ-ㅎ|ㅏ-ㅣ|가-힣]+" title="특수문자, 공백 입력불가" required > <div class="fake_field"> <span class="sprite_small_search_icon"></span> <span>태그검색</span> </div> </form> </div>
 
post_list로 POST 요청이 오면, 검색어 tag를 필요없는 문자들을 제거한 유효한 검색어 tag_clean으로 바꾸고, 이를 추가한 post_search URL로 리다이렉트 합니다.
 
파일명 : post/views.py
def post_list(request, tag=None): ... if request.is_ajax(): ... if request.method == 'POST': tag = request.POST.get('tag') tag_clean = ''.join(e for e in tag if e.isalnum()) return redirect('post:post_search', tag_clean) ...
 
이제 검색 창으로도 해당 태그의 포스트 목록을 검색할 수 있습니다.