(Django) URL 설계 팁

duo2208·2021년 10월 5일
0

Django

목록 보기
7/23
post-thumbnail
장고로 오는 요청들은 흔히 urls.py라는 모듈 내에서 URLConf를 통해 뷰로 라우팅 된다. 장고의 URL 디자인 철학에 따르면 뷰와 URL의 결합은 최대한의 유연성을 제공하기 위해 느슨하게 구성되어야 하며 이는 많은 경험이 요구되는 일이다.

URL Reverse : 느슨한 결합


장고는 urls.py 변경만으로 '각 뷰에 대한 URL' 이 변경되는 유연한 URL 시스템을 지니고 있다.

처리 함수나 처리용 스크립트 파일 이름이 들어간 하드 코딩된 URL은 사이트의 URL 패턴이 변경되면 사이트 전반에 걸친 모든 URL을 변경시켜줘야하는 번거로움이 생긴다.

그러나 Reverse 를 이용하면 URL이 변경되어도 변경된 URL을 알아서 추적하기 때문에, viws.py 로직과 분리된다. 즉, 모든 프로젝트 소스 코드를 검토하지 않아도 되는 것이다.

# urls.py
from news import views

path('archive', views.archive, name='news-archive')
# views.py
from django.urls import reverse

def myview(request):
    return HttpResponseRedirect(reverse('news:news-archive'))

+ Django에서 제공하는 URL Reverse 도구

  • 템플릿에서 url 템플릿 태그 사용하기
  • python 코드에서 reverse() 함수 사용하기
  • 모델 인스턴스의 URL 처리와 관련된 상위 수준 코드에서 ⚡ get_absolute_url() 메서드 사용하기.
🚀 (Django) Reverse resolution of URLs

URL Reverse를 수행하는 4가지 함수

1. reverse()
문자열 return
→ 매칭 URL이 없으면 NoReverseMatch 예외 발생

from django.core.urlresolvers import reverse

reverse('blog:post_detail', args=[100]) # '/blog/100
reverse('blog:post_detail', kwargs={'pk':100}) # 'blog/100'
reverse('nonono') # NoReverseMatch

+ reverse() VS reverse_lazy()

  • reverse()가 동작하기 위해서는 프로젝트의 urlconf가 모두 로드되어야한다. 주로 FBV에서 사용.
  • reverse_lazy()reverselazily evaluated version 이므로 프로젝트의 urlconf가 로드되기 전에 URL reverse을 사용해야 할 때 유용하다. 주로 CBV에서 success_url 을 사용할 경우 사용.

2. resolve_url()
문자열 return
→ 내부적으로 reverse 함수를 사용
→ 매칭 URL이 없으면 인자문자열을 그대로 return

from django.shortcuts import resolve_url

resolve_url('blog:post_detail', 100) # 'blog/100'
resolve_url('blog:post_detail', pk=`100) # 'blog/100'
resolve_url('/blog/100/') # 'blog/100'
resolve_url('/nonono/') # '/nonono/'

3. redirect()
HttpResponseRedirect return
→ 내부적으로 resolve_url 함수를 사용
→ 매칭 URL이 없으면 인자문자열을 그대로 return

redirect('blog:post_detail', 100) # 'blog/100'
redirect('blog:post_detail', pk=`100) # 'blog/100'
redirect('/nonono/') # '/nonono/'

4. url template tag
문자열 return
→ 내부적으로 reverse 함수를 사용

{% url "blog:post_detail" 100 %}
{% url "blog:post_detail" pk=100 %}

🚀 (Django) django.urls utility functions

get_absolute_url 사용하기


모델을 구현했다면 get_absoltue_url() 함수부터 구현하는 것이 좋다.

1. resolve_url / redirect 를 위한 모델 클래스 추가 구현

모델 클래스에 get_absolute_url() 함수를 구현해두면 resolve_url 과 redirect 에서 get_absoulte_url() 사용할 준비가 되어 있기 때문에 바로 리턴값으로 사용이 가능하다.

# models.py
from django.urls import reverse

class Post(models.Model):
	#...중략
	def get_absoulte_url(self):
    	return reverse('blog:post_detail', args=[self.pk])

2. get_absolute_url 활용

resolve_url 함수는 가장 먼저 get_absolute_url() 함수의 존재 여부를 체크하고, 존재할 경우 reverse를 수행하지 않고 그 리턴값을 즉시 리턴한다.

# django/shortcuts.py
def resolve_url(to, *args, **kwargs):
	if hasattr(to, 'get_absoulte_url'):
    		return to.get_absoulte_url()
    # 중략
    try:
    	return(reverse(to, args=args, kwargs=kwargs)
    except NoReverseMatch:

+ 그 외 활용

(1) CreateView / UpdateView
success_url을 제공하지 않을 경우, 해당 model instance 의 get_absolute_url 주소로 이동이 가능한지 체크하고, 이동이 가능할 경우 이동. Create / Update 하고나서 Detail화면으로 이동하는 것은 자연스러운 시나리오.


(2) 특정 모델에 대한 Detail뷰를 작성할 경우
Detail 뷰에 대한 URLConf설정을 하자마자, 필히 get_absolute_url 설정하는 것은 좋은 방법.

🚀 (Django) get_absoulte_url()



URL namespace를 이용하자


URL namespace앱 레벨 또는 인스턴스 레벨에서의 구분자를 제공한다. 쉽게 말해 각각의 앱이 관리하는 독립된 이름 공간이다.

겉으론 그다지 필요 없는 것처럼 보이지만 일단 이용하기 시작하면 매우 유용하다.

# icecreamapp/urls.py
from django.urls import path

app_name ='icecream'	# app 수준의 URL namespace

urlpatterns += [
	path('', include(icecream.urls, namespace='incecream'),
]
# snackapp/urls.py
from django.urls import path

app_name ='snack'	# app 수준의 URL namespace

urlpatterns += [
	path('', include(snack.urls, namespace='snack'),
]  
# base.html
...
# <a href="{% url "icecream_detail" %} detail </a>
<a href="{% url "icecream:detail" %} detail </a>
<a href="{% url "snack:detail" %} detail </a>
  • 템플릿에서 icecream_detail 보다 icecream:detail 처럼 namespace를 이용하는 쪽이 직관적이며 매핑을 더 명확하게 해준다.
  • 새로운 서드 파티 라이브러리와의 상호 운영성을 높여준다.
    서로 다른 앱에서 같은 URL 별칭을 사용하면 중복 문제가 생기는데, namespace 를 이용하면 이러한 문제를 피할 수 있다.

🚀 (Django) URL namespace



URLConf 로부터 뷰 로직을 분리하자


➔ Avoid code : 종속적

  1. 뷰와 url, 모델 사이에 느슨한 결합대신 단단한 종속적인 결합으로 이루어져 있다. 이는 뷰의 재사용을 어렵게 만든다.
  2. 클래스 기반 뷰들 사이에서 같거나 비슷한 인자들이 계속 이용된다.
  3. URL들의 무한한 확장성이 파괴되었다. 클래스 기반뷰의 최대 장점인 클래스 상속이 안티 패턴을 이용함으로써 불가능해졌다.
  4. 다른 이슈도 많다. 인증 절차를 추가해야 한다면 어떻게 될까? 권한 처리 문제는 각 URLConf 뷰를 두 개 또는 그 이상의 데코레이터로 감싸야하나?
from django.urls import path
from django.views.generic import DetailView
from tastings.models import Tasting
  
urlpatterns = [
    path('<int:pk>',
        DetailView.as_view(
            model=Tasting,
            template_name='tastings/detail.html'),
        name='detail'),
    path('<int:pk>/results/',
        DetailView.as_view(
            model=Tasting,
            template_name='tastings/results.html'),
        name='results'),
]

➔ Good code : 유연함

앞 서 이야기된 문제를 피하기 위해 views, url 별도의 파일로 구성한다.
두 개의 파일로 나뉘었고 코드는 늘어났지만 더 유연한 방법이다.

  1. 뷰들 사이에서 인자나 속성이 중복되지않아 반복 작업을 하지 않는다.
  2. 한 번에 한 가지씩의 업무를 명확하고 매끄럽게 처리한다. 뷰의 로직을 찾기 위해 뷰나 URLConf를 둘 다 뒤지지 않아도 된다. 로직은 뷰안에 다 존재하니까 말이다.
  3. 클래스 기반의 장점을 살린다. 뷰 모듈에서 표준화된 정의를 가지게 됨으로써 다른 클래스에서 우리의 뷰를 얼마든지 상속해서 쓸 수 있다. 이 말은 인증 절차, 권한 설정 추가 등의 처리가 훨씬 수월해짐을 의미한다.
  4. 뷰 모델에서 표준화된 정으를 구현함에 따라 우리의 뷰는 어떤 커스텀 로직이라도 구현할 수 있다.
# testings/views.py
from django.urls import reverse
from django.views.generic import ListView, DetailView, UpdateView
from .models import Tasting 

class TasteListView(ListView):
    model = Tasting

class TasteDetailView(DetailView):
    model = Tasting

class TasteResultsView(TasteDetailView):
    template_name = 'tastings/results.html'

class TasteUpdateView(UpdateView): 
    model = Tasting

    def get_success_url(self):
        return reverse('tastings:detail', kwargs={'pk': self.object.pk})
# testings/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path(
        route='',
        view=views.TasteListView.as_view(),
        name='list'
    ), 
    path(
        route='<int:pk>/',
        view=views.TasteDetailView.as_view(),
        name='detail'
    ), 
    path(
        route='<int:pk>/results/',
        view=views.TasteResultsView.as_view(),
        name='results'
    ), 
    path(
        route='<int:pk>/update/',
        view=views.TasteUpdateView.as_view(),
        name='update'
    ) 
]

📌 참고 출처

0개의 댓글