안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

회사 프로젝트 작업 일지

[오픈갤러리] Django 서버 정리 작업 ⑧ - 템플릿 블록 정리 (feat. django-compressor)

피그브라더 2021. 6. 11. 10:11

목차
⦁ 내용이 비어 있는 블록 삭제
⦁ 블록들의 개행 간격 통일 (+ 블록 내 인덴트 수정)
⦁ <script> 태그의 type 어트리뷰트 삭제
 Context 변수와 템플릿 태그 표현식을 JavaScript 전역 변수로 표현
 alert('{{ context_value|escapejs }}')

 

오픈갤러리 프로젝트는 완전한 Django 풀 스택 기반이기 때문에 Django의 템플릿 엔진도 적극 활용하고 있다. 기본적으로 각 페이지는 베이스 템플릿에 해당하는 HTML 파일을 상속함으로써 구현되는데, 베이스 템플릿에는 CSS 코드와 JavaScript 코드를 작성하기 위한 블록이 각각 별도로 정의되어 있다. 그리고 CSS 코드를 위한 블록과 JS 코드를 위한 블록도 각각 두 종류씩 존재하는데, 하나는 django-compressor에 의해 코드가 압축되는 압축 블록이고 다른 하나는 그렇지 않은 일반 블록이다.

 

여기서 말하는 블록이란, {% block %}의 형태로 정의되는 블록을 말한다. 베이스 템플릿에서 이러한 형태로 블록이 정의되면 이 템플릿을 상속하는 하위 템플릿에서는 그 블록도 상속받아 원하는 코드로 그 블록의 내용을 채울 수 있다.

 

이렇게 블록의 종류를 나눈 것은 압축 블록에 매 요청(Request)마다 값이 달라질 수 있는 템플릿 변수를 사용하여 코드를 작성하면 성능이 상당히 저하될 수 있기 때문이다(그 이유는 아래에서 설명). 그런데 예상외로 이런 식으로 작성된 코드가 여전히 많은 것으로 파악되어, 이번 기회에 이런 부분들을 모두 찾아서 정리하기로 하였다. 이번 포스팅은 이 주제와 관련하여 진행했던 여러 정리 작업에 대해 다룬다.

 

참고로, 이번 작업을 통해 개인적으로 가장 크게 얻은 것을 뽑자면 바로 정규식(RegEx)의 활용 능력인 것 같다. IDE를 이용하여 정리가 필요한 부분을 찾기 위해 정규식을 적극 활용하였기 때문이다. 긍정형/부정형 전방/후방 탐색을 비롯한 정규식의 대부분의 기능을 활용하면서 정규식을 활용하는 것에 훨씬 더 숙련된 느낌이었다. 이것이 목적은 아니었지만 일거양득인 듯하여 매우 보람찼다.

 


▎내용이 비어 있는 블록 삭제

큰 작업을 정리하기에 앞서, 눈에 거슬리는 사소한 부분들을 먼저 정리하기로 하였다. 대표적인 작업이 바로 이것이다. 이는 말 그대로 내용이 비어 있는 의미 없는 블록들을 찾아 지우는 것이다. 즉, 다음과 같은 블록들을 말한다.

{% block javascript_general %}{% endblock %}

{% block javascript_general %}
    <script></script>
{% endblock %}

{% block javascript_compress %}{% endblock %}

{% block javascript_compress %}
    <script></script>
{% endblock %}

{% block css_general %}{% endblock %}

{% block css_general %}
    <style></style>
{% endblock %}

{% block css_compress %}{% endblock %}

{% block css_compress %}
    <style></style>
{% endblock %}

이와 같은 형태의 블록들을 탐색할 때는 정규식을 적극 활용하였다.

 


▎블록들의 개행 간격 통일 (+ 블록 내 인덴트 수정)

다음으로, 베이스 템플릿을 상속하는 여러 하위 템플릿에서 블록 간 간격에 일관성이 없음을 발견하여 이를 통일하였다. 간단히 요약을 하자면 기본적으로 모든 블록들은 두 번의 개행으로 구분을 하되 두 종류의 CSS 블록은 서로 붙이고 두 종류의 JavaScript 블록도 서로 붙이도록 통일하였다. 즉, 대략 다음과 같은 형태로 말이다.

{% block javascript_general %}
    ...
{% endblock %}
{% block javascript_compress %}
    ...
{% endblock %}

{% block some_block %}
    ...
{% endblock %}

{% block css_general %}
    ...
{% endblock %}
{% block css_compress %}
    ...
{% endblock %}

이러한 규칙이 지켜지지 않은 하위 템플릿을 탐색할 때도 마찬가지로 정규식을 적극 활용하였다.

 

추가로, 이러한 정리 작업 과정 도중 블록 내 코드의 인덴트가 엉망으로 되어 있는 경우도 있음을 종종 발견하여 이것도 함께 정리하였다. 즉, 블록 내에서 코드를 작성할 때는 한 번의 인덴트로 시작해야 하는데 인덴트 없이 바로 시작하거나 두 번 이상의 인덴트로 시작하는 경우가 있었던 것이다. 이러한 부분들도 마찬가지로 정규식을 적극 활용하여 탐색을 해서 적절히 인덴트를 맞춰주었다.


<script> 태그의 type 어트리뷰트 삭제

과거에는 웹 브라우저에서 여러 종류의 스크립트 언어를 사용했기에 <script> 태그로 JavaScript 코드를 사용하고 싶다면 type="text/javascript"를 반드시 달아줘야 했다. 그리고 여전히 이것이 표준인 것도 맞다. 그러나 오늘날 HTML5에서는 <script> 태그의 type 어트리뷰트 값이 기본적으로 "text/javascript"이기 때문에 더는 이것을 명시적으로 작성해줄 필요가 없어졌다.

 

그런데 오픈갤러리 프로젝트의 경우 어떤 <script> 태그에는 type 어트리뷰트가 달려 있고 어떤 <script> 태그에는 달려 있지 않았기 때문에, 이번 기회에 type 어트리뷰트가 명시적으로 작성된 <script> 태그를 전부 찾아 type 어트리뷰트를 지워주는 작업을 진행하였다. 이것도 결국은 통일성의 확보 측면에서 진행한 정리 작업이었다.

 


▎Context 변수와 템플릿 태그 표현식을 JavaScript 전역 변수로 표현

Django에서는 뷰에서 템플릿으로 넘겨주는 데이터를 Context라고 한다. Context 변수를 템플릿에서 참조하려면 {{ context_value }} 형식을 사용하면 되며, 그 부분은 Django의 템플릿 엔진에 의해 적절한 문자열로 치환이 된다. 그리고 템플릿에서는 {% some_tag %} 형식의 템플릿 태그 표현식도 사용이 가능한데, 이것 또한 Django의 템플릿 엔진에 의해 적절한 문자열로 치환이 된다. 대표적인 예시로는 정적 파일의 경로를 계산해주는 {% static '. . .' %} 태그나 특정 뷰의 URL을 계산해주는 {% url '. . .' %} 태그가 있다.

 

그런데 앞서 말했듯, 오픈갤러리 프로젝트에 속하는 대부분의 페이지에서는 그러한 Context 변수와 템플릿 태그 표현식 등을 작성할 수 있는 블록을 제한하고 있다. 즉, django-compressor에 의해 코드가 압축되는 압축 블록에서는 그러한 코드를 작성하는 것을 피하고, 대신 일반 블록에 그러한 Context 변수 혹은 템플릿 태그 표현식 등을 JavaScript 전역 변수로 정의해둔 뒤 압축 블록에서 그 전역 변수를 참조하는 방식을 채택하고 있다.

 

그러나 예상외로 이 원칙이 잘 지켜지지 않는 페이지가 많다는 것을 알게 되면서, 이번 정리 작업을 통해 전부 정리하기로 하였다. 즉, 압축 블록에 Context 변수나 템플릿 태그 표현식이 존재하는 템플릿들을 전부 찾고, 그 부분들을 일반 블록 내의 JavaScript 전역 변수로 옮기는 작업을 진행한 것이다. 이를 통해 통일성을 확보하는 것은 물론, django-compressor의 동작 상 성능도 개선할 수 있었다. (참고로, django-compressor의 동작 상 성능이 향상되는 이유는 아래에서 설명한다.)

 

이 작업도 마찬가지로 정규식을 적극 활용하였는데, 대략 다음과 같은 정규식을 활용하였다.

regex1 = '{\% block javascript_compress \%}[\s\S]+?{{'
regex2 = '{\% block javascript_compress \%}[\s\S]+?{%'

※ 물론 이것 외에도 다양한 정규식을 활용하여 탐색을 했지만, 종류가 너무 다양하므로 여기선 설명을 생략한다.

 

👉 django-compressor 동작 원리

여기서 잠깐, django-compressor가 코드를 압축하는 원리가 무엇이길래 압축 블록에 Context 변수나 템플릿 태그 표현식이 들어가면 안 된다는 것인지 알아보자. 단, 여기서는 django-compressor를 사용하는 방법은 알고 있다는 가정 하에 설명을 진행한다.

 

다만 동작 원리에 대한 django-compressor 공식 문서의 설명이 다소 빈약해서, 여기서 설명하는 내용의 대부분은 직접 실험을 통해 최대한 논리적이고 합리적으로 추측한 사실에 해당한다. 즉, 실제 구현부를 뜯어본 것이 아니고 공식 문서에 나온 설명도 아니기에 약간의 오류는 있을 수 있음을 미리 밝힌다. 만약 잘못된 내용이 있다면 댓글로 지적 바란다. 

 

우선, django-compressor의 기본적인 사용 형태는 다음과 같다.

<!--
    {% compress css %} 블록 내부에는
    내부 CSS 파일을 가리키는 <link> 태그, 혹은
    자체 CSS 코드를 감싸는 <style> 태그가 존재해야 한다.
-->
{% compress css %}
    <link rel="stylesheet" href=". . ." />
    <style>
        . . .
    </style>
{% endcompress %}

<!--
    {% compress js %} 블록 내부에는
    내부 JavaScript 파일을 가리키는 <script> 태그, 혹은
    자체 JavaScript 코드를 감싸는 <script> 태그가 존재해야 한다.
-->
{% compress js %}
    <script src=". . ." />
    <script>
        . . .
    </script>
{% endcompress %}

 

이제 위와 같은 템플릿의 페이지에 요청을 보내는 상황을 예로 django-compressor의 동작 원리를 살펴보자.

1) 템플릿 문법의 해석 (파싱)
브라우저가 서버에게 특정 페이지에 대해 최초로 요청을 보내고 나면, 서버는 요청받은 페이지에 해당하는 템플릿을 찾고 Django의 템플릿 엔진은 그 안의 템플릿 문법들을 전부 해석하여 순수한 HTML 코드만을 남긴다. 그런데 그 템플릿에 위 예시와 같은 {% compress js %} 블록이 존재했다고 가정해보자. 해당 블록 내부도 마찬가지로 템플릿 문법들을 전부 해석하고 나면 순수한 HTML 코드만 남을 것이다. 정확히는, 외부 JavaScript 파일을 가리키는 <script> 태그 혹은 자체 JavaScript 코드를 감싸는 <script> 태그만 남을 것이다.


2) 해싱 (해시 값 계산)
만약 외부 JavaScript 파일을 가리키는 <script> 태그가 없다면, 자체 JavaScript 코드를 감싸는 <script> 태그 안의 내용을 가지고 해싱을 하여 해시 값을 얻어낼 것이다. 그런데 만약 외부 JavaScript 파일을 가리키는 <script> 태그가 있다면, 그것이 가리키는 파일의 경로에 따라 두 가지로 나뉜다. 먼저, STATIC_URL로 시작하는 경로라면 Django가 프로젝트 내부에서 해당 정적 파일을 찾을 수 있기 때문에 그 파일을 찾아서 그 내용을 이곳에 가져온다. 그러고 나면 이제 아까와 마찬가지로 자체 JavaScript 코드만이 남기 때문에 해싱을 하여 해시 값을 얻어낼 수 있다. 그런데 STATIC_URL로 시작하지 않는, 즉 정말 외부의 경로라면 Django가 프로젝트 내부에서 해당 정적 파일을 찾을 수 없기 때문에 에러가 발생한다. (압축을 할 때마다 외부 경로에 요청을 보내는 것은 부담이 되기 때문이 아닐까 감히 추측해본다.) 따라서 {% compress %} 블록 안에는 프로젝트의 관리 영역에서 벗어난 외부 파일을 링크하면 안 된다.

3) 캐시 검색 및 파일 압축
이제 이전 단계에서 얻어낸 해시 값을 캐시 키로 활용하여 Django의 캐시에서 검색을 한다. 해싱의 특성상 해싱되는 내용이 조금이라도 달라지면 해시 값은 달라지고, 달라지지 않았다면 해시 값은 같다. 만약 캐시에서 검색을 실패했다면, 압축 결과 생성된 JavaScript 파일을 COMPRESS_URL에 해당하는 경로에 저장시키고 Django의 캐시에 해당 파일의 경로 정보를 저장한다. 이후 템플릿의 {% compress js %} 블록 부분은 <script src="압축된 JavaScript 파일의 경로" /> 태그로 대체된 뒤 브라우저에게 해당 템플릿이 전달된다. 그런데 만약 캐시에서 검색을 성공했다면, 이미 압축된 JavaScript 파일이 COMPRESS_URL에 해당하는 경로에 저장되어 있다는 의미이므로 캐시에 저장되어 있는 해당 파일의 경로를 이용하여 아까와 같은 방식으로 브라우저에게 응답한다.

 

위 설명에서 빨간색으로 표시된 문장이 핵심이다. 매 요청마다 코드의 내용을 통해 얻어낸 해시 값을 가지고 캐시에서 검색을 하기 때문에, 매 요청마다 코드의 내용이 바뀌는 것은 굉장한 성능 저하를 일으킨다. 따라서 최대한 매 요청마다 코드는 같도록 관리해주는 것이 성능적으로 이득인 것이다. 이번 정리 작업의 가장 핵심은 바로 이것이었다.

 


▎alert('{{ context_value|escapejs }}')

사소한 작업이지만, 이번 정리 작업을 하면서 escapejs 필터와 관련하여 잘못 작성되어 있던 몇몇 부분을 발견하여 함께 정리하기로 하였다. 뷰에서 템플릿으로 넘겨준 Context 변수의 값을 템플릿 단에서 JavaScript 문자열로 변환하고자 할 때, 해당 Context 변수의 값이 문자열이라면 escapejs 필터를 써주는 것이 안전하다. 만약 해당 Context 변수가 작은따옴표가 포함된 "it's me"와 같은 문자열이었다면, '{{ context_value }}'는 'it's me'로 변환되면서 JavaScript 문법 에러를 발생시킬 것이기 때문이다.

 

그런데 이렇게 작은따옴표와 같은 특수 문자들이 들어가는 문자열을 템플릿에서 가장 많이 사용하는 곳이 바로 alert() 함수였다. 경고 메시지는 정말 다양한 문자들로 이뤄질 수 있기 때문이다. 그래서 alret() 함수를 호출할 때는 해당되는 Context 변수의 값에 항상 escapejs 필터를 사용해주도록 통일하였다.