2026/02/20 Blog - 6

김기훈·2026년 2월 20일

TIL

목록 보기
146/194
post-thumbnail

코드 분석

user = cast(User, request.user)

  • "실행 시점(Runtime)에는 아무 일도 하지 않지만, 개발 시점(Static Analysis)에는 매우 중요한 역할"
    • 타입 힌트 제공 (Type Hinting)

      • Python의 typing.cast 함수는 정적 타입 검사기(Mypy, Pyright 등)와
      • IDE(PyCharm / VSCode)에게 "이 변수(request.user)는 지금부터 무조건 User 클래스의
        • 인스턴스로 취급해줘"라고 강제로 지정하는 것
    • 자동 완성 활성화

      • 일반적으로 프레임워크에서 request.user는 로그인하지 않은 경우
        • AnonymousUser일 수도 있기 때문에
        • IDE가 User 모델에 정의한 필드(예: user.nickname)를 자동으로 추천해주지 못할 때가 많음
        • cast를 쓰면 이때부터 자동 완성이 완벽하게 동작함
  • 주의 ⚠️

    • 타입 안정성을 확보하는 데는 아주 유용하지만, 남용하면 위험

      • cast는 강제로 타입을 규정하는 것이라
        • 실제로 request.user가 익명 사용자(AnonymousUser)인 상태에서
          • 이 코드가 실행되어도 오류를 뱉지 않음
        • 나중에 user.nickname 같은 속성에 접근할 때 비로소 AttributeError가 발생

페이지네이션

core/paginations.py

  • page_size = 10
    • 별도의 파라미터가 없으면 한 페이지에 게시글 10개를 보여줍니다.
  • page_size_query_param = "size"
    • 사용자가 URL에 ?size=20과 같이 입력하여 한 페이지에 보일 개수를 조절할 수 있게 합니다.
  • max_page_size = 50
    • 서버 과부하를 막기 위해 사용자가 요청할 수 있는 최대 개수를 50개로 제한합니다.
class PostPageNumberPagination(PageNumberPagination):
    """
    프로젝트 전역에서 사용할 커스텀 페이지네이션 클래스입니다.
    페이지 범위를 벗어난 요청 시 에러 대신 빈 리스트를 반환하도록 설계되었습니다.
    """

    page_size = 10  # 기본 페이지 당 데이터 개수
    page_size_query_param = "size"  # 클라이언트가 size 파라미터로 개수 조절 가능
    max_page_size = 50  # 최대 페이지 당 데이터 개수 제한
    
    def paginate_queryset(...):
    		...
    def get_paginated_response(...):
        return Response({...})

def paginate_queryset(
    self,
    queryset: QuerySet[Any],
    request: Request,
    view: Optional[APIView] = None,
) -> list[Any] | None:
    try:
        # DRF의 부모 클래스가 제공하는 기본 페이지네이션 로직을 먼저 수행
        # 정상적인 페이지 요청(예: 100개 데이터 중 1페이지 요청)이라면 여기서 결과가 반환됨
        return super().paginate_queryset(queryset, request, view)

    # 만약 요청한 페이지에 데이터가 없을 때(NotFound 예외 발생 시) 아래 로직을 수행
    # ex. 전체 2페이지까지 있는데 사용자가 5페이지를 요청한 경우
    except NotFound:
        # URL 파라미터에서 'page' 값을 가져오고, 없으면 기본값 '1'을 사용
        page_param = request.query_params.get(self.page_query_param, "1")

        try:
	        # 가져온 page 값이 숫자인지(예: ?page=abc) 검사
            page_number = int(page_param) 
        except (TypeError, ValueError):
            raise ValidationError("page는 정수여야 합니다.")
            
		# page 번호가 0 이하(예: ?page=-1)인지 검사
        if page_number <= 0:
            raise ValidationError("page는 1 이상이어야 합니다.")

        # 현재 설정된 페이지 크기(size)를 가져옴
        page_size = self.get_page_size(request)
        if page_size is None:
            return list(queryset)

        paginator = self.django_paginator_class(queryset, page_size)
        """
        메타데이터(전체 개수 등)를 유지하기 위한 처리
        장고의 기본 Paginator를 생성하여 전체 페이지 정보 등을 계산
        """
        
        self.page = paginator.page(paginator.num_pages)
        """
        마지막 페이지 정보를 self.page에 강제로 설정
        이렇게 하면 응답 JSON에 'count'(총 개수) 등은 올바르게 포함
        """
        
        self.page.object_list = []
        """
        실제 결과물(object_list)은 빈 리스트로 비워서 반환
        결과적으로 프론트엔드는 404 에러 대신 '결과가 없는 빈 배열'을 받게 됨
        """

        return []

    # DRF가 최종적으로 JSON 응답을 만들 때 호출하는 메서드입니다.
    def get_paginated_response(self, data):
        return Response(
            {
                # 전체 데이터의 개수를 보냅니다.
                "count": self.page.paginator.count,
                # 장고의 paginator가 이미 계산해둔 '총 페이지 수'를 프론트엔드로 보냅니다.
                "total_pages": self.page.paginator.num_pages,
                # 다음 페이지로 가는 API URL을 보냅니다.
                "next": self.get_next_link(),
                # 이전 페이지로 가는 API URL을 보냅니다.
                "previous": self.get_previous_link(),
                # 실제 게시글 데이터(리스트)를 보냅니다.
                "results": data,
            }
        )

사용방법

  • paginator.get_paginated_response(serializer.data)
    • get_paginated_response 메서드가 호출될 때
    • 부모 클래스인 PageNumberPagination 내부에 정의된 로직에 따라
      • 데이터를 다음과 같이 조립하여 반환
    • count : 전체 데이터의 개수
    • next : 다음 페이지로 이동할 수 있는 API URL
    • previous : 이전 페이지로 이동할 수 있는 API URL
    • results : 현재 페이지에 해당하는 실제 데이터 목록 (Serializer를 통해 변환된 데이터)
class PostAPIView(APIView):
    pagination_class = PostPageNumberPagination 

    @extend_schema(tags=["포스트"], summary="전체 포스트 피드 조회")
    def get(self, request: Request):
        # 1. 서비스 레이어에서 전체 포스트 목록(QuerySet)을 가져옴
        posts = get_global_posts() 

        # 2. 설정된 페이지네이션 객체를 생성
        paginator = self.pagination_class() 
        
        # 3. 쿼리셋에 페이지네이션을 적용('범위 초과 시 빈 리스트 반환' 도 내부에서 처리)
        page = paginator.paginate_queryset(posts, request, view=self) 

        # 4. 페이지 결과가 있다면(성공 혹은 커스텀 처리된 빈 리스트) 결과를 반환
        if page is not None:
            serializer = PostListSerializer(page, many=True)
            # 5. 표준화된 페이지네이션 응답 형식(count, next 등 포함)으로 반환
            return paginator.get_paginated_response(serializer.data) 

        return Response(PostListSerializer(posts, many=True).data)

적용 결과

  • 200

  {
      "count": 100,
      "next": "http://.../?page=2",
      "previous": null,
      "results": [ { "id": 1, "title": "첫 글" }, ... ]
  }

  • 데이터 범위를 벗어난 페이지 요청 시 (200 OK - 커스텀 결과)

    • 총 10페이지까지 있는데 사용자가 ?page=99를 요청한 상황
      • DRF식으로는 404반환이지만 200을 유지하며 results만 빈 리스트로 전달
{
    "count": 100, 
    "next": null,
    "previous": "http://.../?page=10",
    "results": [] 
}

count / next / previous / results

  • DRF의 PageNumberPagination 클래스가 기본적으로 제공하는 응답 규격

    • class PostPageNumberPagination(PageNumberPagination):
      • 커스텀 페이지네이션은 PageNumberPagination을 상속받고 있음
  • PageNumberPagination 은 페이지네이션된 결과를 반환할 때

    • 표준화된 데이터 구조를 사용하도록 설계
  • 커스텀 방법

    • 키 이름을 바꾸거나 구조를 변경하기 위해서는
      • PostPageNumberPagination 클래스에서
      • get_paginated_response 메서드를 오버라이드(재정의)
from rest_framework.response import Response

class PostPageNumberPagination(PageNumberPagination):
	
    ...
    
    def get_paginated_response(self, data):
        return Response({
            'total_count': self.page.paginator.count,
            'next_link': self.get_next_link(),
            'prev_link': self.get_previous_link(),
            'post_list': data  
        })

백엔드 보완

게시글 하나를 가져오는 기능을 추가

def get_post_detail(post_id: int) -> Post:
    """
    특정 ID의 게시글을 상세 조회합니다. (삭제되지 않은 글만)
    """
    # 1. Post 객체를 가져올 때 작성자(user) 정보를 한 번에 가져오도록(select_related) 최적화합니다.
    # 2. deleted_at이 None인(삭제되지 않은) 데이터만 필터링합니다.
    return Post.objects.select_related("user").filter(id=post_id, deleted_at__isnull=True).first()

희망 기능

등급제

  • 고민중

profile
안녕하세요.

0개의 댓글