캐시 무효화/버스팅 적용

Ihwan Shin·2024년 12월 22일

CI/CD & devOps

목록 보기
4/10

웹 서비스에서 스태틱 파일(이미지, CSS, JavaScript 등)을 브라우저가 캐시하여 사용하는 것은 성능을 최적화하는 중요한 기법입니다. 하지만 파일이 업데이트되었을 때, 기존에 캐시된 버전이 계속 사용되면 사용자는 최신 버전을 볼 수 없는 문제가 발생합니다. 이 문제를 해결하기 위해 캐시 버스터(Caching Busting)을 사용합니다.

기존 문제점

브라우저는 성능 최적화를 위해 이미 로드된 스태틱 파일을 캐시합니다. 이 과정에서 업데이트 되어야 하는데에도 불구하고 업데이트가 되지않는 문제가 발생합니다.

기존에는 이 문제를 해결하기 위해 참조하는 곳에서 쿼리스트링 추가해서 호출하는 방식을 사용했습니다. 배포하기 전에 js및 css파일에서 static파일의 경로 뒤에 배포할 때의 timestamp를 일괄적으로 붙였습니다.

이로 인해 발생하는 문제는 다음과 같습니다:

  • 업데이트되지 않은 파일도 새로 불러옴: 배포마다 js, css 파일에 타임스탬프를 일괄적으로 붙였기 때문에, 업데이트되지 않은 파일도 매번 새로운 파일처럼 요청하게 되었습니다. 이로 인해 불필요하게 서버에 요청이 늘어나고, 캐시를 우회하는 방식이 자주 사용되면서 성능이 저하될 수 있었습니다.

  • 캐시 클리어의 비효율성: 사용자가 캐시를 클리어하고 새로운 파일을 다운로드하는 것이 아니라, 타임스탬프를 통해 우회하는 방식이었기 때문에 캐시의 정확한 관리가 어려웠습니다. 특히, 불필요한 파일이 캐시된 상태로 남을 수 있어 결국 사용자 경험에 악영향을 미쳤습니다.

두가지 방식

버전 번호 추가 (Query String 방식)

예: style.css?v=1.0, app.js?v=2.0
파일이 변경될 때마다 버전 번호를 바꿔서 새로운 버전으로 요청을 유도합니다.

장점:

  • 간단한 구현: 쿼리 문자열에 버전 번호를 추가하는 방식은 구현이 간단하고 직관적입니다.
  • 서버와 클라이언트에서 쉽게 관리 가능: 서버나 클라이언트 코드에서 버전 번호만 변경하면 되므로 파일 자체에 대한 변경 없이 관리가 가능합니다.
  • 유연성: 자주 업데이트가 필요 없는 파일에 대해서 버전 관리가 덜 복잡하게 처리됩니다.

단점:

  • CDN 캐싱 문제: 일부 CDN(컨텐츠 배급 네트워크)이나 프록시 서버가 쿼리 문자열을 무시하고, 파일 이름만 보고 캐시할 수 있습니다. 이 경우 style.css?v=1.0이 기존 캐시된 style.css와 같은 파일로 처리될 수 있어 캐시 버스터 효과가 떨어질 수 있습니다.
  • 버전 관리 어려움: 여러 파일에 대해 버전 번호를 일일이 관리해야 하며, 잘못된 버전 번호가 캐시를 강제로 갱신하지 않게 할 수 있습니다.

파일 해시값 추가 (파일명에 해시 포함)

예: style.abc123.css, app.456def.js
빌드 과정에서 파일 내용의 해시값을 계산하여, 파일 이름에 포함시킴으로써 내용이 바뀔 때마다 새로운 이름으로 요청을 유도합니다.

장점:

  • 강력한 캐시 갱신: 파일의 내용이 변경되면 해시값이 자동으로 달라지므로, 파일 이름 자체가 변경됩니다. 이 방법은 브라우저가 항상 최신 파일을 요청하게 만듭니다.
    예: style.abc123.css → 내용 변경 후 style.xyz456.css
  • CDN에서 잘 동작: CDN과 브라우저가 파일 이름을 기준으로 캐시를 관리하므로, 쿼리 문자열에 의한 문제를 피할 수 있습니다. 해시가 포함된 파일 이름은 변경될 때마다 캐시가 갱신됩니다.
  • 자동화된 빌드 시스템에 적합: 빌드 도구(예: Webpack, Gulp 등)를 사용하면, 파일의 내용이 변경될 때마다 자동으로 해시값을 생성하고 이를 파일 이름에 포함시킬 수 있습니다.

단점:

  • 구현 복잡도: 파일 이름에 해시값을 포함시키는 것은 빌드 시스템을 구성하거나 자동화하는 추가 작업이 필요합니다. 이를 위해서는 빌드 도구의 설정이나 스크립트를 설정해야 합니다.
  • 파일 이름 변경 관리 필요: 파일 이름 자체가 변경되기 때문에, HTML, CSS, JavaScript 파일 내에서 해당 파일을 참조하는 경로를 모두 업데이트해야 합니다. 자동화된 빌드 시스템을 사용하지 않으면 이 작업이 수동으로 이루어질 수 있습니다.

캐시버스터 적용

django-staticfiles-finder 이용

기존 서비스에서는 서버비용 절감 문제로 해당 이슈가 가시화되었고,
기존 프로젝트는 django를 이용하여 프론트서버가 구성되어있었기에
cdn과 연동할 수 있으며, 기존 django와 호환하기 좋은 django플로그인을
최대한 활용하고자 하였다

추가적으로 기존 로직이 html파일에서만 static파일을 요청하지 않고
js,css에서도 요청하는 곳이 있어서 이 것을 처리할 때 유용하게 쓸 수 있는 메타데이터(JSON) 파일을 같이 생성해주는 플러그인을 선택했다

플러그인 설치

pip install django-staticfiles-finder

플러그인 적용

# settings.py

INSTALLED_APPS = [
    ...
    'staticfiles_finder',  # 추가
    ...
]

STATICFILES_FINDERS = [
    'staticfiles_finder.finders.HashingFileSystemFinder',  # 파일 경로 해시값 생성
    ...
]

# collectstatic 명령어 실행 시, 파일을 해시값을 포함시켜 저장하도록 설정
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
]

추가로 제공된 metadata

{
  "img/logo.png": "img/logo.a3b2c4d.png",
  "css/style.css": "css/style.a3b2c4d.css",
  "js/app.js": "js/app.5d6f8e9.js"
}

추가적인 static 처리

위 방법만으로는 django 템플릿이 적용되는 html이외의 js,css에서의 static에 대한 경로처리는 제대로 되지 않는다
그래서 이에 대한 처리를 위한 스크립트를 작성해서 배포하기 전에 동작하게 해두었다.

import os
import json

# manifest.json 파일 경로
MANIFEST_PATH = 'static/manifest.json'


def load_manifest(manifest_path):
    """Load manifest.json file and return its content."""
    with open(manifest_path, 'r') as file:
        return json.load(file)


def get_files_in_static_directory(directory):
    """Get all .js and .css file paths in the static/ directory."""
    files_to_process = []
    for root, _, files in os.walk(directory):
        for file in files:
            if file.endswith(('.js', '.css')):
                files_to_process.append(os.path.join(root, file))
    return files_to_process


def update_file_with_manifest(file_path, manifest):
    """Update file content by replacing old paths with new ones from the manifest."""
    with open(file_path, 'r') as file:
        file_content = file.read()

    for old_path, new_path in manifest.items():
        file_content = file_content.replace(old_path, new_path)

    with open(file_path, 'w') as file:
        file.write(file_content)


def update_static_paths():
    """Main logic to update static paths in all .js and .css files."""
    # Load the manifest file
    manifest = load_manifest(MANIFEST_PATH)

    # Get all .js and .css files in the static/ directory
    files_to_process = get_files_in_static_directory('static')

    if not files_to_process:
        print("No files to process.")
    else:
        for file_path in files_to_process:
            update_file_with_manifest(file_path, manifest)
        print("File updates completed!")


if __name__ == "__main__":
    update_static_paths()

결론

위의 캐시 버스터 적용 방법을 통해 얻은 주요 이점은 다음과 같습니다:

  • 불필요한 서버 요청 감소: 파일 업데이트가 이루어졌을 때, 캐시 버스터를 통해 파일 경로를 자동으로 갱신함으로써 불필요한 서버 요청을 줄일 수 있었습니다. 이전에는 파일 이름에 타임스탬프를 매번 추가하여, 업데이트되지 않은 파일도 서버에서 새로 요청하게 되는 문제를 겪었습니다. 이제 해시값을 파일명에 포함시켜 파일 내용 변경 시에만 새로운 파일로 요청되도록 했습니다.

  • 성능 최적화: CDN을 통해 배포된 스태틱 파일들이 정확히 캐시되고 갱신되므로, 클라이언트는 항상 최신 버전의 파일을 사용하게 됩니다. 이로 인해 캐시와 CDN 관리가 원활해져 성능이 향상되었습니다.

  • 자동화된 빌드 시스템 활용: django-staticfiles-finder 플러그인과 manifest.json을 사용하여, 빌드 시 자동으로 해시값을 포함한 파일명을 생성하고, 이를 HTML, JS, CSS 파일 내에서 참조하도록 했습니다. 이로 인해 수동으로 파일을 갱신하고 관리하는 작업에서 벗어나, 자동화된 방식으로 일관된 캐시 관리를 할 수 있게 되었습니다.

  • 확장성 있는 캐시 관리: 기존에는 타임스탬프 방식이였기에 여러 파일에 대해 버전 번호를 관리하는 데 어려움이 있었고, 캐시를 명확히 제어하는 데 한계가 있었습니다. 해시값을 사용함으로써 각각의 파일에 대해 독립적인 캐시 제어가 가능해졌습니다.

결과적으로, 캐시 버스터를 통해 캐시 관리가 보다 명확하고 효율적이 되었으며, 서버의 과도한 요청을 방지하고, 최신 파일을 항상 제공할 수 있는 환경을 구축할 수 있었습니다.

profile
Backend Engineer 💻 (since. 21/07/01)

0개의 댓글