💡 AI 인사이트

🤖 AI가 여기에 결과를 출력합니다...

댓글 커뮤니티

쿠팡이벤트

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

검색

    로딩 중이에요... 🐣

    [코담] 웹개발·실전 프로젝트·AI까지, 파이썬·장고의 모든것을 담아낸 강의와 개발 노트

    25 검색기능구현(Query String 기반 검색) | ✅ 편저: 코담 운영자

    25강 - 검색기능구현(Query String 기반 검색)

    기능 - query string


    ✨ 이번 강의 목표

    • Query String을 활용한 검색 처리 방식 이해
    • 검색어(q 파라미터)를 URL에 포함하여 서버에 전달
    • Django ORM을 활용한 필터링(queryset filtering) 구현
    • DRF + 무한 스크롤 기반 검색 처리 연동까지 포함

    📦 django_instagram/posts/api/urls.py

    from django.urls import path
    from .views import PostAllListView, PostListView, PostLikeView
    from . import views
    
    app_name = "posts_api"
    
    urlpatterns = [
        # 전체 게시글 (현재 사용자 + 전체 최신글)
        path("all/posts/", PostAllListView.as_view(), name="api_posts"),
    
        # 현재 사용자 + 팔로잉 게시글
        path("posts/", PostListView.as_view(), name="api_posts"),
    
        # posts 함수형 리스트 뷰
        path("posts/list", views.posts_list_view, name="api_posts_list"),
    
        # 검색 기능 (GET 요청)
        path("posts/searchList/", views.posts_search_list, name="api_posts_searchList"),
    
        # 좋아요 추가/취소 (POST 요청)
        path("posts/<int:post_id>/post_like/", PostLikeView.as_view(), name="post_like"),
    ]
    
    • urlpatterns는 각 URL 요청에 어떤 뷰(View)가 응답할지를 정의하는 목록입니다.

    • path()는 첫 번째 인자에 URL 경로, 두 번째 인자에 뷰 함수 또는 클래스형 뷰, 세 번째 인자에 URL 별칭(name)을 지정합니다.

    • 이 설정을 통해 프론트엔드에서 /api/posts/ 같은 경로로 데이터 요청이 가능합니다.


    🧩 django_instagram/posts/api/views.py

    ✅ 전체 게시글 리스트 뷰 (PostAllListView)

    class PostAllListView(generics.ListAPIView):
        serializer_class = PostSerializer  # 어떤 serializer로 데이터를 가공할지 지정
        permission_classes = [permissions.IsAuthenticated]  # 인증된 사용자만 접근 가능
    
        def get_queryset(self):
            user = get_object_or_404(user_model, pk=self.request.user.id)  # 현재 로그인한 유저
            search_keyword = self.request.GET.get('q', "")  # 검색어 추출
    
            # 현재 사용자 본인의 게시글과 다른 사람의 게시글 분리
            user_posts = Post.objects.filter(author=user, caption__icontains=search_keyword)
            other_posts = Post.objects.filter(caption__icontains=search_keyword).exclude(author=user)
    
            # 다른 사람의 게시글만 최신순으로 정렬하여 반환
            queryset = other_posts.order_by('-author_id', '-created_at')
            return queryset
    
    • ListAPIView는 리스트 데이터를 보여주는 Django REST Framework의 클래스입니다.

    • get_queryset()은 어떤 데이터를 보여줄지 결정합니다.

    • caption__icontains는 대소문자 구분 없이 검색어가 포함된 게시글을 필터링합니다.


        def list(self, request, *args, **kwargs):
            queryset = self.get_queryset()  # 전체 게시글 목록 가져오기
    
            # 페이지 처리 (예: 1페이지당 5개씩)
            page = self.request.GET.get('page', 1)
            page_size = self.request.GET.get('pageSize', 5)
            paginator = Paginator(queryset, page_size)
            page_obj = paginator.get_page(page)
    
            # 직렬화 처리
            serializer = self.get_serializer(page_obj, many=True, context={'request': request})
            login_user_serializer = UserSerializer(request.user, context={'request': request})
    
            return Response({
                "posts": serializer.data,
                "loginUser": login_user_serializer.data,
                "has_next": page_obj.has_next(),
                "total_pages": paginator.num_pages
            }, status=status.HTTP_200_OK)
    
    • 페이지네이션은 게시글이 너무 많을 때 화면에 한 번에 다 보여주지 않고, 일정 수만큼씩 나눠서 보여주는 방식입니다.

    • paginator.get_page(page)는 지정된 페이지 번호에 해당하는 데이터를 가져옵니다.

    • has_next는 다음 페이지가 존재하는지를 알려줍니다.


    ✅ 사용자 + 팔로잉 게시글 리스트 뷰 (PostListView)

    class PostListView(generics.ListAPIView):
        serializer_class = PostSerializer
        permission_classes = [permissions.IsAuthenticated]
    
        def get_queryset(self):
            user = get_object_or_404(user_model, pk=self.request.user.id)
            search_keyword = self.request.GET.get('q', "")
            following = user.following.all()  # 내가 팔로우하는 유저 목록
    
            queryset = Post.objects.filter(
                Q(author=user) | Q(author__in=following),
                caption__icontains=search_keyword
            ).annotate(like_count=Count('image_likes'))  # 좋아요 수 카운트 추가
    
            return queryset.order_by('-author', '-like_count', '-created_at')
    
    • 이 뷰는 내가 쓴 게시글 + 내가 팔로우한 유저들의 게시글을 보여줍니다.

    • 게시글 정렬 기준은 작성자 우선 → 좋아요 많은 순 → 최신순 입니다.

    • .annotate(like_count=...)를 통해 좋아요 수를 미리 계산해두는 이유는 정렬 시 사용하기 위해서입니다.

        def list(self, request, *args, **kwargs):
            queryset = self.get_queryset()
    
            page = self.request.GET.get('page', 1)
            page_size = self.request.GET.get('pageSize', 5)
            paginator = Paginator(queryset, page_size)
            page_obj = paginator.get_page(page)
    
            serializer = self.get_serializer(page_obj, many=True, context={'request': request})
            login_user_serializer = UserSerializer(request.user, context={'request': request})
    
            return Response({
                "posts": serializer.data,
                "loginUser": login_user_serializer.data,
                "has_next": page_obj.has_next(),
                "total_pages": paginator.num_pages
            }, status=status.HTTP_200_OK)
    
    • get_serializer()는 queryset을 JSON 형태로 바꿔주는 직렬화 과정입니다.

    • 클라이언트에 posts, loginUser, has_next, total_pages라는 구조로 응답을 줍니다.

    • 이 구조를 그대로 자바스크립트에서 받아 무한스크롤 처리에 활용합니다.


    🧠 무한 스크롤 및 검색 기능 자바스크립트 (loadMorePosts.js)

    ✅ lodash 스크립트 로드

    <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
    

    🔰 설명:

    • lodash는 자바스크립트 유틸리티 라이브러리입니다.

    • 여기서는 _.throttle() 함수를 사용해 스크롤 이벤트 과도 호출을 방지합니다.


    ✅ 게시글 비동기 로딩 함수 (loadMorePosts)

    let page = 1;  // 현재 페이지 번호
    let loading = false;  // 중복 요청 방지를 위한 로딩 플래그
    const container = document.querySelector('#postList');  // 게시글이 표시될 영역
    let postUrl = "/api/posts/";  // API 요청 URL
    
    const loadMorePosts = async () => {
      if (loading) return;  // 이미 로딩 중이면 요청 중단
      loading = true;
    
      try {
        const q = document.querySelector('#q').value.trim();  // 검색어 값 가져오기
        const response = await fetch(`${postUrl}?format=json&page=${page}&q=${q}&pageSize=3`);  // 게시글 요청
        const data = await response.json();  // JSON 파싱
    
        data.posts.forEach(post => {
          const postElement = postsHtmlTemplate(post, data.loginUser);  // 템플릿을 통해 HTML 생성
          container.insertAdjacentHTML('beforeend', postElement);  // DOM에 추가
        });
    
        if (!data.has_next) {
          // 더 이상 페이지가 없으면 스크롤 이벤트 제거
          window.removeEventListener('scroll', handleScroll);
        }
    
        page += 1;  // 다음 페이지로 이동
      } catch (error) {
        console.error("Error loading posts:", error);  // 오류 출력
      } finally {
        loading = false;  // 로딩 상태 해제
      }
    };
    

    🔰 설명:

    • 이 함수는 서버에서 게시글 데이터를 비동기로 받아오고, 페이지에 렌더링합니다.

    • has_next 값이 false이면 무한스크롤을 종료합니다.

    • postsHtmlTemplate() 함수는 서버에서 받은 데이터를 HTML로 변환하는 함수입니다.


    ✅ 검색 기능 처리 함수

    function searchOnEnter(event) {
      if (event.key === "Enter") {
        event.preventDefault();  // 기본 제출 방지
        searchInstagram();  // 검색 실행
      }
    }
    
    function searchInstagram() {
      document.querySelector("#postList").innerHTML = "";  // 기존 게시글 초기화
      page = 1;  // 첫 페이지부터 시작
      window.addEventListener('scroll', handleScroll);  // 스크롤 이벤트 등록
      loadMorePosts();  // 게시글 로드
    }
    

    🔰 설명:

    • 사용자가 검색창에서 Enter를 누르면 기존 목록을 비우고 새로운 검색 결과를 불러옵니다.

    • 페이지 번호도 다시 1로 초기화해야 검색 결과가 처음부터 올바르게 로드됩니다.


    ✅ 스크롤 이벤트 처리 (handleScroll)

    const handleScroll = _.throttle(() => {
      const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
      if (scrollTop + clientHeight >= scrollHeight - 10) {
        loadMorePosts();
      }
    }, 200);
    

    🔰 설명:

    • scrollTop + clientHeight는 현재 사용자의 화면 하단 위치를 의미합니다.

    • scrollHeight - 10은 전체 문서에서 10px 남았을 때를 뜻합니다.

    • 즉, 거의 맨 아래 도달하면 추가 게시글을 불러오도록 합니다.

    • _.throttle(..., 200)은 이 이벤트가 0.2초에 한 번만 실행되도록 제한합니다.


    ✅ 초기 실행 코드

    window.addEventListener('scroll', handleScroll);  // 스크롤 이벤트 연결
    loadMorePosts();  // 첫 게시글 로드
    

    🔰 설명:

    • 페이지 진입 시 최초 게시글을 불러오고, 이후 스크롤을 통해 추가 게시글을 자동으로 불러오게 됩니다.

    ✅ 작동 방식 요약

    1. 페이지 진입 시 loadMorePosts()로 첫 게시글 로드

    2. 사용자가 스크롤을 아래로 내리면 handleScroll()이 실행되고, 조건을 만족하면 추가 게시글 요청

    3. searchOnEnter()searchInstagram() 함수로 키워드 검색 시 기존 게시글 초기화 + 새로운 검색 결과 로드

    4. has_next가 false일 경우 더 이상 로딩하지 않도록 스크롤 이벤트를 제거

    5. _.throttle()로 이벤트 과도 실행을 방지하여 성능을 높임

    📌 이 구조를 통해 검색 기능과 무한 스크롤을 동시에 구현하며, 사용자 경험을 개선할 수 있습니다.

    ✅ 정리

    • 검색은 query string 방식으로 이루어짐 (?q=검색어)
    • Django DRF 뷰 내부에서 caption__icontains를 통해 검색 처리
    • JS에서는 검색어를 input으로 받아 URL에 포함해 fetch 요청 전송
    • 무한 스크롤 방식과 검색 기능을 결합하여 UX 향상

    👉 다음 강의는 배포 관련 내용을 다룹니다.

    TOP
    preload preload