Django Improvements - Part 3: Frontend Optimizations(장고 성능 향상 - Part 3: 프론트엔드 최적화)
Django 애플리케이션 성능 향상에 관한 이전 시리즈에서는 데이터베이스와 코드 최적화에 중점을 두었습니다. 3부에선 프론트엔드 성능 향상 방법에 중점을 두도록 하겠습니다.
- Minification(축소)
- Put Javascript Files at the bottom of the Page(페이지 하단에 자바스크립트 파일 배치)
- Http performance(HTTP 성능)
- Caching(캐시)
- CDN systems
성능 속도 측정
모든 사이트 최적화의 첫번째 단계는 속도를 측정해 벤치마크를 얻는것 입니다. Google Page Insights는 웹 사이트가 모바일, 데스크톱 기기에서 어떻게 동작하는지에 대한 인사이트를 제공하여 웹사이트의 상태 및 아래의 측정항목을 기반으로 성능 점수를 제공하므로 웹 사이트 성능을 측정하는데 적합한 도구입니다.
- First Contentful Paint(CFP)
- 페이지가 로드되기 시작한 시점부터 페이지 콘텐츠 일부가 화면의 랜더링 될 때까지의 시간
- Time to Interactive(TTI)
- Interactive(상호작용)이 가능하게 되는데 걸리는 시간
- Speed Index(SI)
- 뷰포트내의 콘텐츠가 눈에 띄게 채워지는 속도
- Total Blocking Time(TBT)
- 페이지가 클릭, 키보드 입력 같은 사용자와 상호작용하지 못했던 시간의 총 합
- Largest Contentful Paint(LCP)
- 뷰포트의 컨텐츠 중 가장 큰 영역을 차지하는 이미지나 텍스트 요소가 처음 로딩되는 시점
- Cumulative Layout Shift(CLS)
- 사용자가 예상하지 못한 레아아웃을 경험하는 빈도
Google Page Insights는 웹사이트를 더 빠르게 만들기 위한 제안도 제공합니다. 예를 들어 아래 이미지는 Django에서 실행되는 사이트 및 개선 방법을 보여줍니다.
이 데이터가 있으면 아래 설명된 방법으로 속도를 향상시키기 위해 최적화가 가능합니다.
Minification
Django의 정적 파일에는 CSS, JS파일, Image등이 포함됩니다. Django의 static 폴더에 저장이 됩니다. Django 애플리케이션은 아래 코드와 같이 settings.py에서 다음과 같이 명시해두고 있습니다.
STATIC_URL = 'static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "templates/static"),
]
만약 정적 파일이 너무 크면, 애플리케이션을 로드하는데는 훨씬 더 많으 시간을 줍니다. 그것은 사용자에게 불쾌한 경험을 제공하게 될 수 있습니다.
따라서, 불필요한 공간, 주석 및 긴 변수 이름을 제거해 정적 파일을 압축을 해서 빨리 로드를 하고 메모리 공간을 덜 차지하도록 합니다.
어떻게 Minification을 하는가?
아래와 같이 mystyle.css
이 있습니다.
body {
background-color: lightblue;
}
h1 {
color: navy;
margin-left: 20px;
}
위 코드를 ninification을 한다면 아래와 같이 됩니다.
body{background-color:#add8e6}h1{color:navy;margin-left:20px}
원본 파일은 7줄에 추가 공백을 포함하지만, 축소된 버전에선 2줄만 사용합니다.
JavaScript파일 Minification도 위와 같이 작동합니다. 예를들어 AJAX POST 요청에서 토큰을 얻는 코드가 있다고 해봅시다.
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const csrftoken = getCookie('csrftoken');
이걸 Minification을 한다면 아래와 같이 됩니다.
function getCookie(a){let c=null;if(document.cookie&&""!==document.cookie){let d=document.cookie.split(";");for(let b=0;b<d.length;b++){let e=d[b].trim();if(e.substring(0,a.length+1)===a+"="){c=decodeURIComponent(e.substring(a.length+1));break}}}return c}const csrftoken=getCookie("csrftoken")
Django Compressor
Django Compressor는 Djangoe 템플릿에서 CSS, JS파일을 Minification을 하는데 사용됩니다. 파일을 단일 캐시 파일로 결합하고 축소하여 작동합니다.
Django Compressor를 사용하기 위해 pip로 라이브러리를 설치하세요.
pip install django_compressor
설치가 완료되면, settings.py
에 아래와 같이 앱을 추가하세요.
INSTALLED_APPS = [
compressor,
]
Django compressor dependencies
settings.py
에 구성을 추가하면 됩니다.
STATICFILES_FINDERS를 추가하면 됩니다.
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'pipeline.finders.PipelineFinder',
)
COMPRESS_ENABLED = True
위와 같이 True로 사용여부를 설정합니다.
마지막으로 압축이 필요한 템플릿 파일에 load compress
태그를 추가합니다. 예를들어 아래와 같이 말이죠.
{% load compress %}
{% compress css %}
<link rel="stylesheet" href="/static/css/main.css" type="text/css" charset="utf-8">
{% endcompress %}
{% load compress %}
{% compress js %}
<script src="/static/js/main.js" type="text/javascript" charset="utf-8"></script>
{% endcompress %}
Django-compressors는 python manage.py compress
명령을 실행한 후 모든 CSS, JS파일을 압축하고 캐시된 파일로 저장합니다. 캐시된 파일을 아래와 같이 참조하도록 템플릿 파일을 변경합니다.
<script src="/static/CACHE/js/main.js" type="text/javascript" charset="utf-8"></script>
JS파일을 page 아래에 둬라.
페이지 성능을 높이하는 또 다른 방법은 JS파일을 아래로 배치하는 것입니다. 콘텐츠가 먼저 로드되어 사용자는 기다릴 필요가 없습니다.
이미지 최적화
사이트의 큰 이미지는 많은 대욕폭으로 소비하고 로드 시간을 증가시킵니다. 이미지 최적화는 이미지 품질에 영향을 미치지 않으면서 이미지를 최적의 크기와 사이즈로 만들어 더 빠르게 로드하게 만드는 방법입니다.
때때로 이미지 업로드를 지원하는 경우 사용자가 업로드 한 이미지의 크기와 사이즈를 제어 할수 없는 경우가 있습니다. 이러한 이미지는 최적화를 해야 합니다. 이미지를 압축하기 위해 가장 좋은 방법은 Pillow입니다. Pillow는 이미지를 동일한 형식으로 변환하고 응용 프로그램에 저장하기 전에 적절하게 압축 할 수 있습니다.
from django.db import models
from PIL import Image
from io import BytesIO
from django.core.files import File
# Create your models here.
def compress_image(image):
img = Image.open(image)
if img.mode != "RGB":
img = img.convert("RGB")
img_output = BytesIO()
img.save(img_output, 'JPEG', quality=80)
compressed_image = File(img_output, name=image.name)
return compressed_image
class Person(models.Model):
email = models.EmailField()
name = models.CharField(max_length= 100, blank=False, null=False)
image = models.ImageField(upload_to= 'files/',null=True)
def save(self, *args, **kwargs):
self.image = compress_image(self.image)
super().save(*args, **kwargs)
여러 페이지를 리다이렉션 하지 않도록 해라.
페이지 리다이렉션은 요청된 페이지가 로드를 위해 다른 리로스로 리다이렉션이 되는 플로우로, 단일 페이지에 대한 요청을 수행하는 페이지가 아닌 여러 요청을 수행하게 되니다. 페이지 리다이렉션으로 인해 페이지를 로드하는데 추가 시간이 많이 발생함으로 사이트가 느려집니다.
Compress content
Django는 콘텐츠를 압축하고 더 작게 만드는데 사용할 수 있는 Gzip 미들웨어를 제공합니다. Django 애플리케이션에 이 미들웨어를 활성화 하기 위해 settings.py
미들웨어 목록에 추가하면 됩니다.
MIDDLEWARE = [
“django.middleware.gzip.GZipMiddleware “
]
Gizp 미들웨어는 공격에 취학함으로 주의해야 사용해야 합니다.
Caching
캐시된 파일은 캐시에서 제공됩니다. 사용자가 요청을 제출하면 먼저 캐시에서 확인하고 페이지가 캐시에 있으면 케시에서 제공됩니다. 요청된 페이지가 캐시에 없으면 먼저 가져와서 나중에 사용할 수 있도록 캐시한 다음에 페이지를 제공하게 됩니다.
Django의 기본 캐시는 LocMemCache이며, settings.py에 추가하면 됩니다.
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'unique-snowflake',
}
}
그러나 LocMemCache는 메모리 효율성이 좋지 않기 때문에 프로덕션엔 적합하지 않습니다.
Views Caching
Django는 View Chacing을 지원합니다. 자주 변경되지 않는 URL을 제공하는 View가 있다고 해봅시다. 페이지를 요청하는 모든 사용자에 대해 페이지에 대한 view를 검색하지 않게 하기 위해 캐시하는 것이 좋스니다. Django View의 캐시는 아래와 같이 @cache_page
데코레이터로 쉽게 적용이 가능합니다.
from django.views.decorators.cache import cache_page
@cache_page(60*60*24)
def index_view(request):
...
60x60x24 means the view will be cached for 24 hours.60x60x24
는 24시간
동안 캐시됨을 의미합니다.
위의 index_view는 아래와 같습니다.
urlpatterns = [
path('app/index', index_view),
]
URL app/index/
가 요청되면 캐시되고 후속 요청을 캐시에 있는 내용을 사용합니다.
Template caching
템플릿 캐싱은 페이지 속도를 향상시키는 또 다른 방법입니다.
자주 변경되는 콘텐츠에 대해 템플릿 캐싱이 가능합니다. Django는 fragment 캐싱을 제공해 템플릿의 특정 부분만 캐시되고 나머지 동적 콘텐츠는 캐시 되지 않은 상태로 유지시킬수 있습니다. Django 프로젝트에 이 설정을 추가하려면 템플릿 상단에 load cache
를 넣고 콘텐츠 블록을 지정해야 합니다.
{% load cache %}
{% cache header %}
<title class = "title">{{object.title}}</title>
{% end cache %}
위의 설정은 header라는 캐시 조각을 만들어 영원히 캐시합니다. 특정 기간 동안 템플릿을 캐시해야 할 경우 아래와 같이 초 단위로 시간을 제한해두면 됩니다.
{% load cache %}
{% cache 500 header %}
<!-- …….
Header content here -->
{% end cache%}
CDN(Content Delivery Network) 서비스를 사용해 정적 파일 제공
CDN 서비스는 애플리케이션의 페이지 로드 속도를 높이는 휼륭한 방법입니다. CDN(콘텐츠 전달 네트워크)은 콘텐츠를 제공하고 빠른 전달을 보장하기 위해 여러 위치에 분산된 서버 집합입니다. 예를 들어 애플리케이션이 미국 서버에서 호스팅되고 사용자가 전 세계에 있다고 하면, CDN을 사용한다면 중국의 클라이언트가 지리적 위치에 가장 가깝고 대기 시간이 가장 짧은 CDN 서버에 연결됩니다. CDN은 로드밸러스를 사용해 잘 동작해 클라이언트 요청이 가장 가까운 CDN서버로 전달해 최대 효율성을 제공합니다. CDN은 정적 파일 복사본을 다른 CDN 위치에 캐시해 작동하므로 사용자가 콘텐츠에 빠르게 액세스 할 수 있습니다.
위에 Minification 색션에서 볼 수 있듯이, Django는 STATIC_ROOT 설정으로 하나의 폴더에 정적 파일을 저장합니다.
애플리케이션을 배포할때 애플리케이션과 동일한 서버에 정적 파일을 두는 것은 성능적으로 최적이 아닙니다. CDN을 사용하면 멀리서 가져올 필요가 없고 가까운 위치에서 가져오기 떄문에 더 빠릅니다. 인기 있는 CDN 서비스는 Akamai, Amazon CloudFront, Cloudflare, Fastly, Google Cloud CDN등이 있습니다.
Django에는 CDN 서버의 정적 파일을 저장하고 서버에 저장할 수 있는 django-storages 패키지가 있습니다. 해당 라이브러리는 Amazon S3, Google Cloud, Digital Ocean 등을 지원합니다.
첫번째로 django-storages를 pip를 이용해서 설치하세요.
pip install django-storages
다음으론 settings.py
의 설치된 앱 목록에 storages를 넣어둡니다.
INSTALLED_APPS = [
...
'storages',
...
]
If you decide to use Amazon S3, you will need to create an account. Amazon S3 offers file storage, and data is stored in buckets.
만약 Amazon S3를 사용한다면 계정을 생성해야 하며, Amazon S3는 파일 스토리지를 제공해 버킷에 데이터가 저장됩니다.
정적 파일을 S3 버킷에 업로드하기 위해 아래와 같이 설정해주세요.
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3StaticStorage'
Amazon S3 및 해당 CloudFront 서비스를 사용하기 위해 버킷을 생성하고 AWS 자격 증명을 설정합니다.
AWS_ACCESS_KEY_ID = 'your-access-key'
AWS_SECRET_ACCESS_KEY = 'your-secret-key'
AWS_S3_CUSTOM_DOMAIN = 'cdn.mydomain.com'
AWS_S3_SECURE_URLS = True
AWS_STORAGE_BUCKET_NAME = 'bucket_name'
AWS_IS_GZIPPED = True
마지막으로 STATIC_URL에 CDN 도메인을 넣습니다.
STATIC_URL = 'https://cdn.mydomain.com'
Django 애플리케이션의 프론트엔드 모니터링
데이터베이스, 미들웨어, 뷰와 같은 많은 요소들이 애플리케이션의 전반적인 성능에 관련이 있습니다. 센트리는 코드 수준 애플리케이션 성능 모니터링을 통해 느리게 수행되는 HTTP 작업과 모든 페이지 트랜잭션에 걸리는 시간을 파악할 수 있습니다.
아래와 같이 센트리 계정을 설정하고 프로젝트를 만드세요.
pip를 이용해 센트리 SDK를 설치하세요.
pip install --upgrade sentry-sdk
다음으로 settings.py
에 센트리 SDK 설정을 하세요.
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
sentry_sdk.init(
dsn="https://examplePublicKey@o0.ingest.sentry.io/0",
integrations=[DjangoIntegration()],
# Set traces_sample_rate to 1.0 to capture 100%
# of transactions for performance monitoring.
# We recommend adjusting this value in production.
traces_sample_rate=1.0,
# If you wish to associate users to errors (assuming you are using
# django.contrib.auth) you may enable sending PII data.
send_default_pii=True
)
Setting the traces_sample rate
Performance > Forntend 대시보드로 이동하세요.
metrics를 보려면 맨 아래로 이동하세요. 아래와 같이 표시됩니다.
위 데이터에서 TPM은 분당 평균 트랜잭션 수를 보여줍니다. 기타 측정항목은 다음과 같습니다.
- p50: 트랜잭션의 50%가 p50 임계값에 설정된 시간 이상이 됩니다. 예를들어 P50 임계값이 10ms로 설정된 경우 트랜잭션 50%가 해당 임계값을 초과해 10ms보다 오래 걸립니다.
- p75: 트랜잭션의 25%가 p75 임계값에 설정된 시간.
- p95: 트랜잭션 5%가 p65 임계값에 설정된 시간.
각 트랜잭션을 확장하면 요청의 각 부분이 전체 응답 시간에 어떻게 영향을 미치는지 아래와 같이 자세히 알 수 있습니다.
위 다이어그램은 각 요청이 수행되는 방식과 요청이 느려지는 원인에 대해 대시보드를 제공해 신속하게 수정가능합니다. 설정에는 모든 트랜잭션을 Sentry로 보내는 traces_sample_rate를 1.0으로 설정했습니다. 이를 통해 애플리케이션의 어떤 부분이 성능에 부정적인 영향을 미치는지 한번 확인해보세요.