본문으로 건너뛰기
Advertisement

Apache 정적 파일 서빙과 캐시 제어

Apache에서 정적 파일을 효율적으로 서빙하고 브라우저 캐시를 올바르게 제어하는 방법을 다룹니다. mod_expires, mod_deflate, ETag 설정을 통해 웹 성능을 크게 향상시킬 수 있습니다.


Apache의 정적 파일 서빙 기본

Apache는 요청된 URL을 DocumentRoot와 결합하여 파일 시스템에서 파일을 찾아 반환합니다.

<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/html

<Directory /var/www/html>
Options -Indexes +FollowSymLinks # 디렉터리 목록 비활성화, 심볼릭 링크 허용
AllowOverride None
Require all granted
</Directory>
</VirtualHost>
요청: GET /images/logo.png
→ 실제 파일: /var/www/html/images/logo.png

mod_expires — 캐시 만료 헤더

mod_expiresExpiresCache-Control: max-age 헤더를 자동으로 설정합니다.

# 모듈 활성화 (Ubuntu)
sudo a2enmod expires
sudo systemctl reload apache2
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/html

<IfModule mod_expires.c>
ExpiresActive On

# 기본값 — 모든 파일에 1주일 캐시
ExpiresDefault "access plus 1 week"

# HTML — 캐시 없음 (항상 최신 확인)
ExpiresByType text/html "access plus 0 seconds"

# CSS / JavaScript — 1년 (빌드 해시 포함 파일)
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType text/javascript "access plus 1 year"

# 이미지 — 1개월
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/webp "access plus 1 month"
ExpiresByType image/svg+xml "access plus 1 month"
ExpiresByType image/x-icon "access plus 1 year"

# 폰트 — 1년 (변경 거의 없음)
ExpiresByType font/woff "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType application/font-woff "access plus 1 year"
ExpiresByType application/font-woff2 "access plus 1 year"

# 기타
ExpiresByType application/pdf "access plus 1 month"
ExpiresByType application/json "access plus 0 seconds"
</IfModule>
</VirtualHost>

mod_headers — Cache-Control 정밀 제어

mod_expiresExpires 헤더를 설정하지만, Cache-Control을 더 세밀하게 제어하려면 mod_headers를 함께 사용합니다.

sudo a2enmod headers
sudo systemctl reload apache2
<IfModule mod_headers.c>
# 빌드 해시가 붙은 JS/CSS — 1년 강력 캐시 (immutable)
<FilesMatch "\.[0-9a-f]{8,}\.(js|css)$">
Header set Cache-Control "max-age=31536000, public, immutable"
</FilesMatch>

# 일반 JS/CSS
<FilesMatch "\.(js|css)$">
Header set Cache-Control "max-age=31536000, public"
</FilesMatch>

# 이미지·폰트
<FilesMatch "\.(png|jpg|jpeg|gif|webp|ico|woff|woff2|ttf|svg)$">
Header set Cache-Control "max-age=2592000, public"
</FilesMatch>

# HTML — no-cache
<FilesMatch "\.html$">
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "0"
</FilesMatch>

# API 응답 — 캐시 없음
<LocationMatch "^/api/">
Header set Cache-Control "no-cache, no-store, must-revalidate"
</LocationMatch>
</IfModule>

ETag 설정

ETag는 파일의 고유 식별자로, 브라우저가 캐시 만료 후 서버에 재검증 요청을 보낼 때 사용합니다.

# ETag 활성화 (기본값)
FileETag MTime Size

# 분산 서버 환경에서 ETag 비활성화 권장
# (서버마다 inode가 달라 ETag 값이 다를 수 있음)
FileETag None

# 또는 inode 제외
FileETag MTime Size

분산 환경 주의: 여러 서버를 로드밸런싱하는 경우 기본 ETag에 inode 번호가 포함되어 서버마다 다른 ETag를 반환할 수 있습니다. FileETag MTime Size로 설정하거나 FileETag None으로 ETag를 비활성화하세요.


mod_deflate — Gzip 압축

sudo a2enmod deflate
sudo systemctl reload apache2
<IfModule mod_deflate.c>
# 압축할 MIME 타입
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE text/javascript
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/json
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE image/svg+xml
AddOutputFilterByType DEFLATE font/woff font/woff2

# 이미 압축된 파일 형식은 제외
SetEnvIfNoCase Request_URI \
\.(?:gif|jpe?g|png|webp|zip|gz|bz2|rar|7z|mp4|mp3|ogg)$ \
no-gzip dont-vary

# Vary 헤더 추가 (프록시 캐시가 인코딩별로 캐시하도록)
Header append Vary Accept-Encoding

# 압축 레벨 (1~9)
DeflateCompressionLevel 6
</IfModule>

Gzip 효과 확인

curl -H "Accept-Encoding: gzip" -I http://example.com/app.js

# 응답 헤더 확인:
# Content-Encoding: gzip
# Vary: Accept-Encoding

정적 파일 MIME 타입 설정

Apache가 파일 확장자에 올바른 Content-Type을 반환하도록 MIME 타입을 설정합니다.

# /etc/apache2/mime.types에 없는 타입 추가
AddType application/wasm .wasm
AddType font/woff2 .woff2
AddType image/webp .webp
AddType video/mp4 .mp4
AddType application/manifest+json .webmanifest

종합 설정 예시

실무 운영 환경에서 사용하는 정적 파일 서빙 최적화 전체 설정:

<VirtualHost *:443>
ServerName static.example.com
DocumentRoot /var/www/static

SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/static.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/static.example.com/privkey.pem

<Directory /var/www/static>
Options -Indexes -MultiViews +FollowSymLinks
AllowOverride None
Require all granted
</Directory>

# Gzip 압축
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json image/svg+xml font/woff font/woff2
DeflateCompressionLevel 6
Header append Vary Accept-Encoding
</IfModule>

# 캐시 만료 설정
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/html "access plus 0 seconds"
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/webp "access plus 1 month"
ExpiresByType font/woff2 "access plus 1 year"
</IfModule>

# Cache-Control 헤더
<IfModule mod_headers.c>
<FilesMatch "\.(js|css)$">
Header set Cache-Control "max-age=31536000, public, immutable"
</FilesMatch>
<FilesMatch "\.(png|jpg|jpeg|gif|webp|ico|woff|woff2)$">
Header set Cache-Control "max-age=2592000, public"
</FilesMatch>
<FilesMatch "\.html$">
Header set Cache-Control "no-cache"
</FilesMatch>
</IfModule>

ErrorLog /var/log/apache2/static.error.log
CustomLog /var/log/apache2/static.access.log combined
</VirtualHost>

정리

최적화 항목모듈효과
캐시 만료 헤더mod_expires재방문 시 서버 요청 감소
Cache-Control 정밀 제어mod_headersimmutable 등 세밀한 캐시 정책
Gzip 압축mod_deflate텍스트 파일 50~80% 전송량 감소
ETag 조정FileETag분산 환경에서 불필요한 재검증 방지
디렉터리 목록 비활성화Options -Indexes보안 강화
Advertisement