2026년 Django Admin 커스터마이징 완벽 가이드: 액션, 필터, 면접 질문

Django 6 Admin 인터페이스 커스터마이징 방법을 알아봅니다. 커스텀 액션, 리스트 필터, 필드셋, 인라인 설정과 기술 면접 대비 질문을 다룹니다.

2026년 Django Admin 커스터마이징 완벽 가이드: 액션, 필터, 면접 질문

Django Admin 커스터마이징은 기본 제공되는 관리 인터페이스를 프로덕션 애플리케이션을 위한 맞춤형 백오피스 도구로 변환합니다. Django 6에서는 필터의 패싯 카운트, 커스터마이징 가능한 비밀번호 변경 폼, Font Awesome 아이콘 등 향상된 관리 기능이 포함되어 있어 잘 구성된 Admin은 실제로 사용하기 편리합니다.

핵심 요약

ModelAdmin은 모든 Admin 커스터마이징의 핵심 클래스입니다. list_display, list_filter, search_fields, fieldsets, 커스텀 액션을 마스터하면 Admin 구성 요구사항의 90%를 처리할 수 있습니다.

ModelAdmin 기초: list_display와 search_fields

list_display는 변경 목록 페이지에 표시되는 컬럼을 제어합니다. 단순한 필드 이름 외에도 계산된 값을 위해 @admin.display() 데코레이터로 장식된 호출 가능 객체를 받습니다. search_fields는 기본적으로 icontains를 사용하여 레코드를 필터링하는 검색 상자를 활성화합니다.

python
# admin.py
from django.contrib import admin
from django.utils.html import format_html
from .models import Article


class ArticleAdmin(admin.ModelAdmin):
    list_display = ["title", "author_name", "status", "publish_date", "is_recent"]
    search_fields = ["title", "author__email", "content"]
    list_per_page = 50
    list_select_related = ["author"]  # N+1 쿼리 방지

    @admin.display(description="작성자", ordering="author__last_name")
    def author_name(self, obj):
        return f"{obj.author.first_name} {obj.author.last_name}"

    @admin.display(description="최근?", boolean=True)
    def is_recent(self, obj):
        from django.utils import timezone
        return obj.publish_date >= timezone.now() - timezone.timedelta(days=7)


admin.site.register(Article, ArticleAdmin)

@admin.display() 데코레이터는 함수 속성으로 short_description을 설정하던 이전 패턴을 대체합니다. boolean=True 매개변수는 True/False 텍스트 대신 체크마크나 X 아이콘을 렌더링합니다. list_select_related를 사용하면 list_display에서 외래 키 필드에 접근할 때 N+1 쿼리를 방지할 수 있습니다.

SimpleListFilter를 사용한 커스텀 list_filter

list_filter는 변경 목록 페이지의 사이드바 필터를 활성화합니다. 기본 필터링에는 필드 이름이 작동하지만, 여러 필드에 걸친 복잡한 필터링 로직이나 커스텀 쿼리가 필요한 경우 SimpleListFilter를 사용합니다.

python
# admin.py
from datetime import date, timedelta
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from .models import Article


class PublishDateFilter(admin.SimpleListFilter):
    title = _("발행 기간")
    parameter_name = "published"  # URL 매개변수: ?published=this_week

    def lookups(self, request, model_admin):
        # 필터 사이드바용 (값, 레이블) 튜플 반환
        return [
            ("this_week", _("이번 주")),
            ("this_month", _("이번 달")),
            ("this_year", _("올해")),
            ("unpublished", _("미발행")),
        ]

    def queryset(self, request, queryset):
        # self.value()를 기반으로 쿼리셋 필터링
        today = date.today()
        if self.value() == "this_week":
            return queryset.filter(publish_date__gte=today - timedelta(days=7))
        if self.value() == "this_month":
            return queryset.filter(
                publish_date__year=today.year, publish_date__month=today.month
            )
        if self.value() == "this_year":
            return queryset.filter(publish_date__year=today.year)
        if self.value() == "unpublished":
            return queryset.filter(publish_date__isnull=True)
        return queryset


class ArticleAdmin(admin.ModelAdmin):
    list_filter = [
        "status",
        PublishDateFilter,
        ("author", admin.RelatedOnlyFieldListFilter),  # 기사가 있는 작성자만 표시
    ]

RelatedOnlyFieldListFilter는 데이터베이스의 모든 사용자를 나열하는 대신 실제로 기사가 있는 작성자만 작성자 드롭다운에 표시합니다. 이는 관련 모델에 많은 레코드가 있을 때 사용성을 향상시킵니다.

Django 6 패싯: 사이드바의 필터 카운트

Django 6에서는 각 필터 옵션 옆에 일치하는 객체 수를 표시하는 패싯 카운트가 도입되었습니다. show_facets 속성으로 이 동작을 제어합니다.

python
# admin.py
from django.contrib import admin
from django.contrib.admin import ShowFacets
from .models import Article


class ArticleAdmin(admin.ModelAdmin):
    list_display = ["title", "status", "category"]
    list_filter = ["status", "category", "author"]
    show_facets = ShowFacets.ALWAYS  # 항상 카운트 표시

    # 대체 값:
    # ShowFacets.ALLOW - URL에 ?_facets=1이 있을 때 카운트 표시
    # ShowFacets.NEVER - 카운트를 표시하지 않음 (대규모 데이터셋용)

패싯 카운트는 각 필터 옵션에 대해 추가 COUNT 쿼리를 실행합니다. 수백만 행이 있는 테이블에서는 성능 저하를 피하기 위해 show_facets = ShowFacets.NEVER를 설정합니다. 카운트는 필터가 적용됨에 따라 동적으로 업데이트되어 현재 필터 조합과 일치하는 레코드 수를 표시합니다.

일괄 작업을 위한 커스텀 Admin 액션

Admin 액션은 선택된 객체에 대해 일괄 작업을 수행합니다. @admin.action 데코레이터는 권한과 드롭다운에 표시되는 설명을 설정합니다.

python
# admin.py
from django.contrib import admin
from django.contrib import messages
from django.utils.translation import ngettext
from .models import Article


@admin.action(description="선택한 기사 발행", permissions=["change"])
def publish_articles(modeladmin, request, queryset):
    # 이미 발행된 기사 제외
    unpublished = queryset.exclude(status="published")
    updated = unpublished.update(status="published")

    modeladmin.message_user(
        request,
        ngettext(
            "%d개의 기사가 발행되었습니다.",
            "%d개의 기사가 발행되었습니다.",
            updated,
        )
        % updated,
        messages.SUCCESS,
    )


@admin.action(description="선택한 항목을 JSON으로 내보내기")
def export_as_json(modeladmin, request, queryset):
    from django.http import HttpResponse
    from django.core import serializers

    response = HttpResponse(content_type="application/json")
    response["Content-Disposition"] = 'attachment; filename="articles.json"'
    serializers.serialize("json", queryset, stream=response)
    return response  # HttpResponse 반환 시 파일 다운로드 트리거


class ArticleAdmin(admin.ModelAdmin):
    actions = [publish_articles, export_as_json]

    def get_actions(self, request):
        # 사용자 권한에 따라 조건부로 액션 제거
        actions = super().get_actions(request)
        if not request.user.has_perm("articles.delete_article"):
            del actions["delete_selected"]
        return actions

액션에서 HttpResponse를 반환하면 파일 다운로드가 트리거되거나 확인 페이지로 리다이렉트됩니다. permissions 매개변수는 특정 모델 권한을 가진 사용자로 액션을 제한합니다. get_actions()를 오버라이드하여 현재 요청에 따라 동적으로 액션을 표시하거나 숨길 수 있습니다.

Django 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

필드셋과 인라인 모델을 통한 폼 구성

fieldsets는 추가 및 변경 폼을 접을 수 있는 섹션으로 구성합니다. TabularInlineStackedInline은 관련 모델을 부모 폼에 직접 임베드하여 페이지 간 탐색을 제거합니다.

python
# admin.py
from django.contrib import admin
from .models import Article, ArticleImage, ArticleComment


class ArticleImageInline(admin.TabularInline):
    model = ArticleImage
    extra = 1  # 표시할 빈 폼 수
    max_num = 5  # 기사당 최대 이미지 수
    fields = ["image", "caption", "order"]


class ArticleCommentInline(admin.StackedInline):
    model = ArticleComment
    extra = 0
    readonly_fields = ["created_at", "author"]
    can_delete = True


class ArticleAdmin(admin.ModelAdmin):
    fieldsets = [
        (
            None,
            {
                "fields": ["title", "slug", "author"],
            },
        ),
        (
            "콘텐츠",
            {
                "fields": ["content", "excerpt"],
                "classes": ["wide"],  # 전체 너비 필드
            },
        ),
        (
            "발행 설정",
            {
                "fields": ["status", "publish_date", "category", "tags"],
                "classes": ["collapse"],  # 기본적으로 접힘
            },
        ),
        (
            "SEO",
            {
                "fields": ["meta_title", "meta_description"],
                "classes": ["collapse"],
                "description": "검색 엔진 최적화 필드",
            },
        ),
    ]
    inlines = [ArticleImageInline, ArticleCommentInline]
    prepopulated_fields = {"slug": ["title"]}  # 제목에서 슬러그 자동 생성
    autocomplete_fields = ["author", "category"]  # AJAX 기반 선택
    filter_horizontal = ["tags"]  # M2M 선택용 위젯


admin.site.register(Article, ArticleAdmin)

TabularInline은 관련 객체를 컴팩트한 테이블 형식으로 표시하며 필드가 적은 단순한 모델에 적합합니다. StackedInline은 각 관련 객체를 별도의 폼 블록으로 표시하며 필드가 많은 모델에 더 적합합니다. autocomplete_fields 옵션은 기본 드롭다운을 AJAX 검색 위젯으로 교체하며, 관련 모델에 수천 개의 레코드가 있을 때 필수적입니다.

Admin 사이트 커스터마이징

AdminSite를 서브클래싱하면 다른 브랜딩, 권한 또는 모델 등록을 가진 여러 관리 인터페이스를 만들 수 있습니다. Django 6에서는 더 엄격한 비밀번호 정책을 위한 password_change_form 커스터마이징이 추가되었습니다.

python
# admin.py
from django.contrib import admin
from django.contrib.admin import AdminSite
from .models import Article, Category


class ContentAdminSite(AdminSite):
    site_header = "콘텐츠 관리"
    site_title = "CMS"
    index_title = "대시보드"

    def has_permission(self, request):
        # 'editors' 그룹의 스태프로 제한
        return (
            request.user.is_active
            and request.user.is_staff
            and request.user.groups.filter(name="editors").exists()
        )


content_admin = ContentAdminSite(name="content_admin")
content_admin.register(Article, ArticleAdmin)
content_admin.register(Category)

# urls.py에서:
# path('content-admin/', content_admin.urls),

AdminSite 인스턴스는 자체 URL 네임스페이스, 로그인 페이지, 등록된 모델 세트를 가집니다. 이 패턴은 편집자가 사용자 관리나 시스템 설정을 볼 필요가 없는 명확한 사용자 역할이 있는 애플리케이션에 적합합니다.

면접 질문: Django Admin 커스터마이징

다음 질문들은 Django 기술 면접에서 자주 등장합니다. 구문뿐만 아니라 기본 메커니즘을 이해하는 것이 중급 엔지니어와 시니어 엔지니어를 구분하는 요소입니다.

Q: 정렬할 수 없는 계산 컬럼을 list_display에 추가하려면 어떻게 해야 합니까?

@admin.display() 데코레이터는 정렬에 사용할 데이터베이스 필드를 지정하는 ordering 매개변수를 받습니다. 이것이 없으면 컬럼 헤더를 클릭할 수 없습니다. 여러 레코드를 집계하거나 외부 서비스를 호출하는 계산 값은 기반이 되는 데이터베이스 컬럼이 없기 때문에 정렬할 수 없습니다.

Q: 패싯 카운트가 성능에 미치는 영향은 무엇이며, 언제 비활성화해야 합니까?

패싯은 각 필터 옵션당 하나의 COUNT 쿼리를 실행하며, 이는 활성 필터 수에 곱해집니다. 1,000만 행의 테이블에서 3개 필드에 각각 5개의 필터 옵션이 있다면 페이지 로드당 15개의 추가 COUNT 쿼리가 발생합니다. 적절한 인덱스 없이 10만 행을 초과하는 테이블을 필터링할 때는 show_facets = ShowFacets.NEVER를 설정합니다.

Q: Admin 액션과 커스텀 Admin 뷰는 어떻게 다릅니까?

액션은 선택된 객체의 쿼리셋에서 작동하며 변경 목록의 선택 체크박스와 통합됩니다. 커스텀 Admin 뷰는 커스텀 URL을 통해 액세스되는 독립 페이지로, 선택된 레코드에서 작동하지 않는 보고서, 가져오기 또는 대시보드에 적합합니다. 액션은 요청을 차단하므로 장시간 실행되는 작업을 수행해서는 안 됩니다.

Q: 기본 필터 대신 RelatedOnlyFieldListFilter를 사용하는 이유는 무엇입니까?

기본 RelatedFieldListFilter는 관련 테이블의 모든 객체를 쿼리합니다. ForeignKey가 10만 레코드가 있는 User 모델을 가리키면 필터 드롭다운이 10만 개의 모든 옵션을 로드하여 브라우저가 멈춥니다. RelatedOnlyFieldListFilter는 현재 모델의 레코드에 존재하는 값만 쿼리하므로 일반적으로 훨씬 작은 세트가 됩니다.

프로덕션에서 피해야 할 일반적인 실수

개발 환경에서 작동하는 여러 패턴이 대규모에서 문제를 일으킵니다.

python
# admin.py
from django.contrib import admin
from .models import Order


class OrderAdmin(admin.ModelAdmin):
    # 실수: list_select_related 없이 관련 필드에 접근
    # 이는 N+1 쿼리를 유발하며 목록 뷰의 각 행마다 하나씩 발생
    list_display = ["id", "customer_email", "total"]

    # 수정: 관련 객체 프리페치
    list_select_related = ["customer"]

    def customer_email(self, obj):
        return obj.customer.email

    # 실수: 인덱스되지 않은 대용량 텍스트 컬럼에서 search_fields
    # search_fields = ["notes"]  # 대용량 테이블에서 느림

    # 수정: 인덱스된 필드로 검색 제한
    search_fields = ["id", "customer__email"]

    # 실수: list_display에서 비용이 많이 드는 계산
    # def order_total(self, obj):
    #     return sum(item.price * item.qty for item in obj.items.all())  # N+1

    # 수정: get_queryset에서 어노테이션 사용
    def get_queryset(self, request):
        from django.db.models import Sum, F

        qs = super().get_queryset(request)
        return qs.annotate(computed_total=Sum(F("items__price") * F("items__qty")))

    @admin.display(description="합계", ordering="computed_total")
    def total(self, obj):
        return obj.computed_total

get_queryset()을 오버라이드하여 어노테이션을 추가하면 행마다 데이터베이스에 접근하는 대신 단일 쿼리에서 계산된 값을 유지합니다. Django Debug Toolbar로 Admin을 프로파일링하여 프로덕션에 도달하기 전에 N+1 쿼리를 잡습니다.

서드파티 Admin 테마: Django Unfold

Django Unfold는 Tailwind CSS로 구축된 Django 6용으로 가장 활발하게 유지되는 Admin 테마입니다. 표준 Admin을 대체하는 것이 아니라 확장하므로 기존 ModelAdmin 구성이 계속 작동합니다.

python
# settings.py
INSTALLED_APPS = [
    "unfold",  # django.contrib.admin 전에 위치해야 함
    "unfold.contrib.filters",  # 고급 필터 위젯
    "unfold.contrib.forms",  # 향상된 폼 위젯
    "django.contrib.admin",
    # ...
]

Unfold는 다크 모드, 반응형 레이아웃, 사이드바 탐색, RangeNumericFilterRangeDateFilter 같은 고급 필터 유형을 추가합니다. HTMX와 Alpine.js 통합은 전체 SPA 프레임워크의 오버헤드 없이 인터랙티비티를 제공합니다. 커스텀 대시보드를 구축하지 않고 세련된 Admin이 필요한 팀에게 Unfold는 수 주간의 프론트엔드 작업을 절약합니다.

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

Django Admin 커스터마이징에서 기억해야 할 사항

  • @admin.display()와 함께 list_display로 컬럼 제어. 정렬 가능한 계산 값에는 ordering 추가
  • SimpleListFilter는 여러 필드에 걸친 커스텀 필터 로직이나 날짜 범위 처리
  • Django 6 패싯은 필터 카운트 표시. 대용량 테이블에서 COUNT 쿼리 오버헤드를 피하려면 ShowFacets.NEVER로 비활성화
  • @admin.action(permissions=[...])을 사용한 Admin 액션으로 일괄 작업 처리. 파일 내보내기에는 HttpResponse 반환
  • list_select_relatedget_queryset()의 어노테이션된 쿼리셋을 사용하여 목록 뷰에서 N+1 쿼리 방지
  • RelatedOnlyFieldListFilter는 대용량 테이블의 ForeignKey로 필터링할 때 브라우저 멈춤 방지
  • Admin 구성을 대상으로 하는 Django 면접 질문으로 이러한 패턴 연습
오늘의 챌린지

Django 코드의 버그를 찾을 수 있나요

실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

Anthony Fillion-Maillet

작성자

Anthony Fillion-Maillet

SharpSkill 창업자

10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.

2026년 9월 13일 업데이트

공유

관련 기사