[장고] choices 옵션과 동적 필터 활용

Saemi An·2025년 5월 2일
post-thumbnail

🥝 choices 옵션으로 area 필드 만들기

장고 모델을 정의할 때 필드에 choices 옵션을 주면 해당 필드에 저장될 값을 제한할 수 있다.

예를 들어 area라는 필드에 서울시 행정구역 25개의 값만 저장 되어야할 경우
행정구역 선택값들을 모아둔 서브클래스 AREA를 만들어 아래와 같이 정의할 수 있다.

class RESTAURANT(models.Model):
    class AREA(models.IntegerChoices):
        Gangnam = 0, '강남구'
        Gangdong = 1, '강동구'
        Gangbuk = 2, '강북구'
        # (생략)
        
    area = models.PositiveBigIntegerField(choices=AREA.choices)

서브클래스 AREA는 열거형(Enumerated Type) 클래스이며 구성 요소는 다음과 같다:

  • Gangnam: 코딩시 AREA.Gangnam처럼 사용할 수 있는 식별자 이름
  • 0: DB에 실제 저장되는 값(IntegerField로 정의된 area 필드와 타입을 맞춰줘야함)
  • '강남구': 사용자에게 보여줄 라벨(표시용 문자열)

실제 예시는 다음과 같다:

장고 쉘 - 정수 출력장고 쉘 스크린샷
장고 어드민 - 라벨 표시장고 어드민 스크린샷

🫒 Enum 이란?

열거형(Enumerated Type)은 사용자 언어의 상수 역할을 하는 식별자이다.
대표적인 예로 1과 0을 True와 False로 표현하는 예시가 있다.

위 예시에서 area 필드를 PositiveBigIntegerField()로 정의 했다.
즉, 0, 1, 2, .. 등의 값을 데이터베이스에 저장하는 언어로 사용하기로 했다.

하지만 실제 사용자 페이지에서도 숫자를 보여주면 곤란하다. 이 값을 꺼내 보여줄 때에는 '강남구', '강동구', '강북구' .. 등의 문자열로 보여줘야 한다.
그리고 이를 위해 열거형 타입 서브클래스 AREA를 정의해 줬다.

🫒 Enum 서브클래스 접근 방식

class RESTAURANT(models.Model):
    class AREA(models.IntegerChoices):
        Gangnam = 0, '강남구'
        Gangdong = 1, '강동구'
        Gangbuk = 2, '강북구'

위 예시에서 Enum 타입의 AREA는 다음과 같은 구조로 변환된다 📌 :

AREA = {
    'Gangnam': <AREA.Gangnam: 0>  # name: Gangnam, value: 0, label: '강남구'
    'Gangdong': <AREA.Gangdong: 1>  # name: Gangdong, value: 1, label: '강동구'
    'Gangbuk': <AREA.Gangbuk: 2>  # name: Gangbuk, value: 2, label: '강북구'
}

⚠️ 유의할 점은 AREA가 파이썬의 dict와 유사하게 생겼지만 둘은 다른 개념이라는 것이다.
딕셔너리는 "키 - 값" 구성이지만
AREA는 "name - (value: label)"로 구성되어 있다.

Enum과 Dict 차이점
enum은 내부적으로 __getitem__이나 __members__와 같은 딕셔너리의 유사 인터페이스를 구현하고 있지만 다음과 같은 차이점이 있다(고 한다. 사실 이해 못했다..)

따라서 다음과 같은 방식으로 실제 인스턴스에 접근할 수 있다:
1. Enum 멤버의 식별자를 통한 접근
2. Enum 멤버의 식별자를 dict처럼 사용한 접근
3. Enum 멤버의 value를 통한 접근


🥝 활용

예를들어 서울 동네별로 식당을 추천해주는 서비스가 있다고 하자.

🫒 동적 filter

사용자가 처음으로 페이지 진입했을 시에는 전체 추천 식당 목록을 보여주고
특정 지역을 클릭 했을 때에는 해당 지역의 추천 식당 목록을 보여주려 한다.
이때 쿼리셋은 다음과 같다:

restaurants = RESTAURANT.objects.all()   # 디폴트 페이지
restaurants = RESTAURANT.objects.filter(area=0)   # 강남구 필터 적용

두 가지 경우를 하나의 코드에 담으면 다음과 같다:

def generate_restaurant_list(str_area=None):

    area_mapping = {
        "Gangnam": 0,
        "Gangdong": 1,
        "Gangbuk": 2,
    }

    filters = {}

    if str_area:
        filters = {"area": area_mapping[str_area]}

    restaurants = RESTAURANT.objects.filter(**filters)
    
    return restaurants

문자열 'Gangnam'을 값으로 갖는 str_area 변수를 위 함수에 넘기면
area_mapping 딕셔너리에서 'Gangnam' 해당하는 정수값과 감께
미리 초기화 해둔 filters 딕셔너리에 추가된다.

filters를 언패킹해서 사용하도록 **filters 형태로 쿼리셋에 넣어주면 다음과 같은 쿼리셋이 만들어 진다:
restaurants = RESTAURANT.objects.filter({'area': RESTAURANT.AREA.Gangnam})

🫒 Enum 멤버 접근방식

하지만 서울에는 동네가 25개나 있다.
이를 mapping해주는 딕셔너리를 모두 적기 귀찮다.

RESTAURANT 모델의 서브클래스 AREA에 이미 해당 정보가 있으니 그걸 가져다 쓰자.

def generate_restaurant_list(str_area=None):

    filters = {"area": RESTAURANT.AREA[f"{str_area}"]} if str_area is not None else {}

    restaurants = RESTAURANT.objects.filter(**filters)
    
    return restaurants

위에서 설명한 Enum 서브클래스 접근 방식 세번째를 활용(RESTAURANT.AREA[f"{str_area}"])하여 동적 필터를 만들어 줬다.

리팩토링 하는 김에 코드도 짧게 줄여줬다.

⚠️ 오류
RESTAURANT.AREA.str_area와 같은 방식으로 Enum 서브클래스의 멤버에 접근하면 오류가 난다.

RESTAURANT.AREA.str_area <-- 이건 변수값을 키로 사용하는 딕셔너리의 접근 방식이다.

하지만 AREA는 Enum타입이고, 장고는 문자열 변수 str_area를 문자 그대로 받아들여 'str_area'라는 식별자를 찾으려 하고 오류가 발생한다.

🫒 결과

디폴트
# html
<img src="{% static 'FOOD_HUNTING/images/seoul_map.png' %}" alt="서울 지도" usemap="#seoul_map" class="seoul_img">
  <map name="seoul_map">
    <area target="_self" alt="서대문구" title="서대문구 검색" href="{% url 'index_with_area' 'Seodaemun' %}" coords="(생략)" shape="poly">
    <area target="_self" alt="종로구" title="종로구 검색" href="{% url 'index_with_area' 'Jongno' %}" coords="(생략)" shape="poly">
  </map>
</map>

지도에서 검색하고싶은 구를 선택하게되면
Enum 타입의 서브클래스 AREA에서 적절한 식별자를 찾아
동적 필터를 통해 원하는 식당이 검색된다.


도커로 CI/CD 환경 구축하는걸 연습하기 위해
더미 장고 프로젝트를 만들다가 여기까지 흘러와 버렸다

근데 더 흘러갈 것 같다..
5월 내로 도커를 사용하기는 할까? ㅎㅎ..

끝!

profile
하나씩 차근차근 천천히

0개의 댓글