Django에서 요즘 즐겁게 쓰고 있는 것들 몇 가지 더
안녕하세요! 요즘 좀 재미있는 여정을 하고 있어요. 2010년 스타일로 웹사이트를 만드는 법을 배우려는 여정이거든요. SQL 데이터베이스를 두고 백엔드에서 HTML을 렌더링하는 그런 방식이에요.
솔직히 이 방식으로 웹사이트를 만드는 게 꼭 ‘쉽다’고 느껴지진 않아서 더 흥미롭기도 해요. 2000년대나 2010년대에 이런 방식을 배운 적이 없거든요. 그래서 배워야 할 게 정말 많더라고요.
그래서 오늘은 이런 사이트를 만드는 걸 Go 표준 라이브러리나 Flask로 시도하다가 실패했을 때보다 훨씬 더 해볼 만하게 만들어준 Django 기능 몇 가지를 소개해보려고 해요. 그리고 Django를 쓰면서 겪은 문제점 몇 가지도 얘기해볼게요.
왜 굳이 2010년처럼 웹사이트를 만들려고 할까요?
원래 제가 자신 있게 쓸 수 있다고 느꼈던 툴킷은 이런 것들이었어요.
- 정적 사이트 생성기(이 블로그처럼)
- 자바스크립트로 재밌는 걸 좀 하는 정적 사이트(이 sql playground 같은)
- 백엔드로 Lambda나 Go를 쓰는 간단한 Vue.js 싱글 페이지 앱(mess with dns 같은)
이런 프론트엔드 중심 접근 방식은 정말 단순한 애플리케이션에는 아주 마음에 들었어요. 그런데 페이지가 정말 많은 무언가를 만들어야겠다고 생각하니, 프론트엔드 코드가 많이 들어가는 선택지들은 별로 끌리지 않더라고요. 그래서 백엔드를 한번 해보기로 했죠.
가능한 한 JS를 적게 쓰는 백엔드 중심 사이트를 만드는 건, 백엔드는 최대한 적게 쓰면서 JS로 싱글 페이지 웹사이트를 만드는 것과 제게는 똑같이 느껴져요. 서로 정반대처럼 보일 수도 있지만요. 어느 쪽이든 로직을 최대한 한곳에 모아두려는 거니까요.
이제 Django에 대한 생각을 좀 풀어볼게요!
쿼리 빌더가 마음에 들어요
Django에서는 다양한 WHERE 조건을 메서드로 만들어 둔 ‘쿼리셋’ 클래스를 정의할 수 있다는 걸 알게 됐어요.
각 메서드가 뭘 하는지 정의해두면 뷰 코드에서는 이렇게 쓸 수 있어요.
Events.objects.approved()
.for_tab(tab)
.with_festivals(tab_params.festival_slugs)
.is_free(tab_params.free)
.is_outdoors(tab_params.outdoors)그리고 메서드는 이렇게 정의하고요.
class EventQuerySet(SearchableQuerySetMixin, models.QuerySet):
def approved(self):
return self.filter(approved_at__isnull=False)
def future(self):
today = timezone.localdate()
return self.filter(end__gt=self._midnight(today))
def with_tags(self, tags):
if tags:
return self.filter(tags__name__in=tags).distinct()
return self필터를 정의하는 문법이 제일 마음에 드는 건 아니지만, 대부분은 그냥 만들어 둔 메서드를 쓰는 데 시간을 쓰게 되고, 그건 정말 읽기 쉽고 쓰기 편하더라고요. 앞으로 다른 쿼리 빌더 라이브러리도 찾아보고 싶어졌어요. 예전에는 ‘SQL을 아는데 쿼리 빌더가 뭐 필요해?’라고 생각했었는데, 이렇게 구조를 잡아두니까 가독성이 정말 좋아지더라고요.
파이썬으로 직접 작은 쿼리 빌더를 만든 사례를 하나 찾았는데, 이런 더 최소한의 버전을 쓰면 어떨지 고민해보려고 나중에 읽어보려 해요.
템플릿 필터가 정말 좋아요
Django 템플릿에서 쓸 수 있는 자잘하지만 삶의 질을 높여주는 필터들이 정말 많아요. 지금까지 제가 써본 것들은 이렇고요.
- 일반 텍스트 URL을 링크로 바꾸거나 줄바꿈을
<br>로 바꿔주는 것({{ event.description|urlize|linebreaksbr }}) - 날짜 포맷팅(
{{ row.date|date:"M j" }}) json_script— 파이썬 딕셔너리를 받아 JSON으로 변환한 뒤 안전하게 HTML 안에<script>태그로 넣어주는 필터예요
하나하나 보면 사소한 것들이지만, 그냥 바로 쓸 수 있다는 게 은근히 큰 차이가 되더라고요.
querystring이 멋져요
제가 제일 좋아하는 템플릿 필터는 querystring인 것 같아요. 이 사이트에서는 ?date=2026-06-01 같은 필터를 써서 뭘 보여줄지 정하곤 하는데, querystring을 쓰면 같은 쿼리 스트링에서 하나만 바꿔서 링크를 만들 수 있어요. 예를 들어 이전 날짜로 가는 링크는 이렇게 만들고요.
<a href="{% querystring date=nav.prev_date%}">또 outdoors 파라미터를 없애는 링크는 이렇게 만들 수 있어요.
<a href="{% querystring outdoors=None %}">자동 데이터베이스 마이그레이션은 여전히 훌륭해요
Django의 자동 데이터베이스 마이그레이션 시스템이 여전히 정말 마음에 들어요. 모델을 수정해서 필드를 추가하거나 뭘 바꾸면 Django가 알아서 마이그레이션을 만들어주니까요.
지금까지 마이그레이션을 19번 했는데, 앞으로도 더 하게 될 것 같아요. 문제에 대한 이해가 바뀌면서 데이터베이스를 이렇게 쉽게 바꿀 수 있다는 게 제게는 정말 큰 도움이 돼요.
상속으로 코드를 정리하고 싶지는 않아요
Django 문서에서는 뷰 코드를 정리하는 방법으로 클래스 기반 뷰와 상속을 쓰라고 제안할 때가 있어요. 예를 들어 코드가 많이 겹치는 뷰가 네 개 있는데, 부모 클래스를 하나 정의하고 나머지 뷰들이 그걸 상속받게 해서 관리할 수도 있죠.
한번 써봤는데, 뷰 간에 코드를 공유하려고 상속을 쓰는 경험이 별로 즐겁지 않더라고요. 그래서 이 글에서 주장하는 것처럼 함수로 바꾸니 훨씬 더 명료해졌어요. 파이썬에서 상속을 써서 좋은 경험을 해본 적이 한 번도 없고, 앞으로도 다시 써볼 생각은 없어요.
하지만 Django 자체가 제공하는 인터페이스를 쓰기 위해 상속을 쓰는 건 괜찮아요. 예를 들어 쿼리셋을 정의하려면 class EventQuerySet(SearchableQuerySetMixin, models.QuerySet) 같은 코드를 써야 하죠. 크게 고민하지 않고 쓰면 그냥 잘 동작하더라고요.
(메타 코멘트: 요즘은 프로그래밍에 대한 의견을 말할 때 그냥 ‘이건 제게 별로 맞지 않고, 대신 저걸 선호해요’ 식으로 말하려고 노력하고 있어요. 링크한 글에서는 함수 기반 뷰가 ‘올바른 방법’이라고 말하더라고요. 그게 ‘정답’인지에 대해서는 별로 집착하지 않지만, 다른 사람들도 저랑 비슷하게 느낀다는 걸 확인하게 되어서 뭔가 위로가 됐어요)
Django 성능을 어떻게 생각해야 할지 잘 모르겠어요
어느 순간 LLM 스크래퍼들이 저희 사이트를 발견해서 초당 10개 정도 요청을 보내기 시작했어요. 일단 차단은 해둬서 지금은 괜찮지만, 사이트가 어느 정도까지 버틸 수 있는지 생각해보게 되더라고요. 저는 원래 Go 백엔드를 주로 써왔는데, 거기서는 성능이 꽤 단순하거든요(보통은 그냥 충분히 빠르니까요). Django 사이트는 정말 다르더라고요.
가볍게 부하 테스트(ab -n 1000 -c 1)를 해보니, 지금은 한 달에 10달러 정도 하는 VM에서 초당 2~3개 정도의 요청을 처리할 수 있더라고요.
프로파일링을 잔뜩 해서 뭐가 느린지 찾아내고 더 빠르게 만들어보려는 토끼굴에 빠지고 싶은 유혹이 들어요(py-spy가 그 용도로 정말 좋고 쓰기도 엄청 쉽고, 프로파일링 자체도 재밌거든요!). 하지만 Django 사이트에서 성능이 어느 정도 나와야 하는지, 더 큰 그림에서는 어떻게 생각해야 하는지를 아직 잘 모르겠어요.
아직 답을 못 찾은 것들은 이런 것들이에요.
- 가끔씩 트래픽이 몰리는 사이트라면, 스케일 업할 수 있게 해야 할까요?
- 더 많은 걸 캐시할 수 있게 사이트를 설계해야 할까요? (정말 그래야 할까요? 캐시는 제대로 만들기가 너무 귀찮은데요!)
- Django 성능 문서에서는 Jinja가 템플릿 렌더링이 더 빠르다고 하는데, 템플릿 시스템을 바꿀 걸 고민해야 할까요?
- 그 문서에는 또 “{% block %}이 {% include %}를 쓰는 것보다 빠르다”고 나와 있는데, 얼마나 큰 차이인지, 그렇다면 왜 그런지 궁금하더라고요
템플릿 캐싱이 중요할 수도 있어요
Django에 대해 배우고 있는 것 중 하나는, Django가 프레임워크(Framework™)이다 보니 실수로 설정을 잘못하기 쉽다는 거예요. 예를 들어, 아까 사이트가 왜 느린지 고민하면서 Django 성능 문서를 읽다가 이런 문장을 봤어요.
캐시된 템플릿 로더를 활성화하면 각 템플릿을 렌더링할 때마다 컴파일하지 않아도 되므로 성능이 대폭 향상되는 경우가 많습니다.
CPU 프로파일링을 했을 때 템플릿 렌더링에 시간이 많이 쓰이고 있다는 걸 봤었거든요! 이게 도움이 될지도 모르겠더라고요.
링크를 따라가 보니 캐시된 템플릿 로더는 기본적으로 켜져 있어야 하는데, 제가 다른 걸 하려다가 실수로 꺼버렸더라고요. ‘캐시된 템플릿 로더를 기본값에서 꺼버렸다’는 게 제가 아직 Django 설정 파일이 꽤 헷갈리고 어렵다고 느끼는 대표적인 예시인 것 같아요. 앞으로는 설정 파일을 건드릴 때 좀 더 조심해야겠어요.
템플릿 캐싱을 다시 켜고 나니, 이제는 CPU를 다 쓰지 않고도 초당 12개 정도의 요청을 꽤 쉽게 처리할 수 있게 된 것 같아요. 전후를 꼼꼼하게 벤치마크한 건 아니지만, 꽤 큰 차이가 난 것 같아요.
Django 성능에 대해 놀라웠던 점 중 하나는, 원래 ‘성능 문제가 있으면 데이터베이스 쿼리를 확인해 봐! 인덱스를 추가해 봐!’라는 조언을 많이 들었는데, 제가 겪은 여러 성능 문제는(이번 템플릿 캐싱 같은) 느린 쿼리 때문이 아니었다는 거예요. 그래서 지금까지는 CPU 프로파일을 먼저 돌려보는 게 더 도움이 됐어요. 그리고 SQLite를 쓰고 있으니, 느린 데이터베이스 쿼리 문제가 있더라도 어차피 CPU 프로파일에 나타나니까요.
어쨌든 사이트 성능에 너무 깊게 들어가고 싶지는 않아요. 말했듯이 프로파일링에 빠지기 쉽지만, 사실 저는 프로파일링에 대해 이미 많이 알고 있어서 지금 제게 가장 중요한 배움은 아니거든요.
오늘은 여기까지예요!
Django에서 즐거웠던 점(혹은 어려웠던 점!)에 대해서는 나중에 더 얘기해볼지도 몰라요. 요즘 짧은 글을 좀 써보려고 하고 있거든요.
글을 무작위로 읽기