1. 개요 및 핵심 포인트
UI5 애플리케이션을 BTP에 배포하면 사용자가 페이지를 열 때마다 sap-ui-core.js, Component-preload.js, 컨트롤 라이브러리 등 수백 KB의 정적 리소스가 네트워크로 흘러갑니다. 이 트래픽을 줄이고 초기 로딩을 1초 이하로 끌어내리는 핵심 도구가 바로 HTTP 캐시 헤더(Cache-Control, ETag)와 UI5 Cache Buster 입니다. 잘못 설정하면 사용자에게 이전 버전이 영원히 박제되거나, 반대로 캐시 효과가 전혀 나지 않아 서버 비용이 폭증합니다.
이 글이 답하는 질문은 다음과 같아요.
- Approuter(
xs-app.json)에Cache-Control헤더 주입 방법 이해 - ETag/If-None-Match 흐름으로 304 응답 받아내기
- UI5 Cache Buster 토큰(
~타임스탬프~) 동작 원리 파악 - OData v2/v4 응답에 대한 캐시 전략 선택 기준 정립
- 정적/동적 리소스에 대해 서로 다른 헤더를 적용하는 실전 패턴 습득
2. 사전에 알아두면 좋은 배경
본격 진입 전에 알고 있으면 흐름이 한결 잘 보이는 항목을 정리해요.
- HTTP 캐시 모델: 강한 캐시(strong cache,
max-age)와 조건부 검증 캐시(conditional,ETag/Last-Modified)의 차이 - UI5 부트스트랩 흐름:
index.html→sap-ui-core.js→manifest.json→Component-preload.js순서 - SAP BTP Cloud Foundry의 Approuter(@sap/approuter) 라우팅 구조
- OData 서비스가 응답 헤더에 ETag를 넣는 방식(엔티티의
sap:etag또는@Core.OptimisticConcurrency)
3. 실습 환경 및 버전
이 글은 다음 환경을 기준으로 작성됐어요. 다른 버전에서도 큰 차이는 없지만 헤더 키 이름이나 옵션 명칭이 다를 수 있으니 공식 릴리스 노트를 함께 확인하길 권장해요.
- SAP BTP Cloud Foundry runtime, 리전 EU10/US10
- @sap/approuter 14.x (npm 패키지)
- UI5 1.120 LTS (또는 1.108 LTS)
- @sap/ui5-builder, @ui5/cli 3.x
- HTML5 Application Repository service (standard plan)
- Node.js 18 LTS, BTP CAP 8.x (OData v4 백엔드용)
로컬에서 헤더를 직접 관찰하려면 Chrome DevTools Network 탭의 Disable cache 체크를 풀고, Response Headers 영역에서 cache-control, etag, x-cache 값을 확인해요.
4. 핵심 개념과 동작 원리
4.1 Cache-Control max-age
Cache-Control: public, max-age=31536000, immutable 같은 헤더를 받은 브라우저는 해당 리소스를 1년간 네트워크 호출 없이 디스크 캐시에서 가져와요. 도서관에서 같은 책을 빌릴 때마다 사서한테 물어보지 않고 내 책상 위 책을 그대로 보는 것과 비슷해요. 단, 이 헤더는 절대로 변하지 않는 파일에만 안전해요. 변경 가능성이 있다면 파일 이름 자체를 바꾸는 cache busting 전략과 짝지어야 해요.
4.2 ETag와 304 Not Modified
ETag는 리소스의 지문(fingerprint)이에요. 서버는 응답에 ETag: "v3-abc123"을 붙이고, 브라우저는 다음 요청에 If-None-Match: "v3-abc123"을 동봉해요. 서버 측 지문이 같으면 본문 없이 304 Not Modified만 돌려줘요. 네트워크 왕복은 발생하지만 본문 전송이 사라져서 OData JSON 같이 크기는 크지만 자주 안 바뀌는 응답에 잘 어울려요.
강한 캐시는 "물어보지도 마", ETag는 "물어는 보되 같으면 본문은 안 받을게" 라고 기억하면 헷갈리지 않아요.
4.3 UI5 Cache Buster
UI5는 부트스트랩 시 data-sap-ui-appCacheBuster 속성을 통해 앱 루트 경로에 ~타임스탬프~ 토큰을 자동 삽입해요. 예: /myapp/~20260609123045~/Component-preload.js. 서버는 토큰을 무시하고 동일 파일을 응답하지만, 새 배포 시점에 토큰이 바뀌므로 브라우저 캐시 키가 강제로 갱신돼요. 덕분에 정적 리소스에 max-age=31536000을 안전하게 걸 수 있어요.
4.4 OData 응답 캐시
OData 컬렉션 응답에 강한 캐시를 거는 건 일반적으로 위험해요. 트랜잭션 데이터가 옛 값으로 고정되니까요. 대신 ETag 기반 조건부 캐시 또는 Cache-Control: private, max-age=60 처럼 짧은 TTL을 사용해요. CAP v4는 @Core.OptimisticConcurrency 어노테이션이 붙은 엔티티에 자동으로 ETag를 생성해 줘요.
5. 실전 코드 (3단계)
5.1 1단계: 기본 — Approuter에서 정적 리소스 캐시 켜기
HTML5 앱 모듈을 Approuter로 서빙할 때 xs-app.json의 responseHeaders 옵션으로 헤더를 주입할 수 있어요.
{
"welcomeFile": "/index.html",
"authenticationMethod": "route",
"routes": [
{
"source": "^/resources/(.*)$",
"target": "/resources/$1",
"localDir": "webapp",
"cacheControl": "public, max-age=31536000, immutable",
"authenticationType": "none"
},
{
"source": "^/(.*\\.(?:js|css|woff2|png|svg))$",
"localDir": "webapp",
"cacheControl": "public, max-age=2592000",
"authenticationType": "xsuaa"
},
{
"source": "^/(.*)$",
"localDir": "webapp",
"cacheControl": "no-cache",
"authenticationType": "xsuaa"
}
]
}
핵심은 라우트 우선순위에요. /resources/*(UI5 코어)는 거의 안 변하니 1년 캐시, 앱 정적 리소스는 30일, HTML/manifest는 no-cache로 항상 재검증하게 분리했어요. no-cache는 "캐시는 저장하되 매 요청마다 서버에 유효성 검사" 의미이고, no-store는 아예 저장 금지라는 점이 달라요.
5.2 2단계: 실무 — UI5 Cache Buster + ETag 활성화
index.html 부트스트랩에서 앱 캐시 버스터를 켜요.
<script
id="sap-ui-bootstrap"
src="resources/sap-ui-core.js"
data-sap-ui-theme="sap_horizon"
data-sap-ui-resourceroots='{"my.app": "./"}'
data-sap-ui-appCacheBuster='["./"]'
data-sap-ui-async="true"
data-sap-ui-compatVersion="edge">
</script>
서버 측에서는 /sap/bc/ui5_ui5/sap/<app>/~timestamp~/ 형식 토큰을 받아들이도록 라우팅을 추가해요. SAP Build Work Zone 또는 HTML5 App Repository를 쓰면 토큰 처리는 인프라가 자동으로 해 줘요. 자체 Approuter일 경우 다음과 같이 처리 미들웨어를 추가할 수 있어요.
// approuter custom middleware (server.js)
const approuter = require('@sap/approuter');
const ar = approuter();
ar.beforeRequestHandler.use((req, res, next) => {
// ~timestamp~ 토큰 제거 후 원본 경로로 재작성
req.url = req.url.replace(/\/~[\d]+~/, '');
next();
});
ar.start();
OData 백엔드(CAP)에서는 엔티티에 ETag 어노테이션을 달아요.
// db/schema.cds
entity Orders : managed {
key ID : UUID;
total : Decimal(15, 2);
status : String(20);
}
// srv/orders-service.cds
service OrdersService {
@Core.OptimisticConcurrency: [modifiedAt]
entity Orders as projection on db.Orders;
}
이 설정이 적용되면 OData 응답에 ETag: W/"2026-06-09T12:30:45Z"가 자동으로 추가되고, 클라이언트가 보낸 If-None-Match가 일치하면 CAP 런타임이 304를 돌려줘요.
5.3 3단계: 프로덕션 — 차등 캐시 정책 + 모니터링
실제 운영에서는 환경별 캐시 차등이 필요해요. 개발 환경은 즉시 반영, 프로덕션은 장기 캐시 패턴이에요. xs-app.json 대신 커스텀 Approuter 미들웨어로 더 세밀하게 제어해요.
// server.js
const approuter = require('@sap/approuter');
const ar = approuter();
const ENV = process.env.CF_INSTANCE_GUID ? 'prod' : 'dev';
const CACHE_RULES = [
{ pattern: /\/resources\//, header: 'public, max-age=31536000, immutable' },
{ pattern: /\.(woff2|png|svg|jpg)$/, header: 'public, max-age=2592000' },
{ pattern: /Component-preload\.js$/, header: 'public, max-age=86400, must-revalidate' },
{ pattern: /\.(html|json)$/, header: 'no-cache' }
];
ar.beforeRequestHandler.use((req, res, next) => {
if (ENV === 'dev') {
res.setHeader('Cache-Control', 'no-store');
return next();
}
const rule = CACHE_RULES.find(r => r.pattern.test(req.url));
if (rule) {
res.setHeader('Cache-Control', rule.header);
res.setHeader('Vary', 'Accept-Encoding');
}
// 모니터링용 커스텀 헤더
res.setHeader('X-Cache-Policy', rule ? rule.header : 'default');
next();
});
ar.start();
운영 단계 체크리스트.
- 배포 직후
curl -I https://<route>/Component-preload.js로 헤더 검증 - SAP Cloud Logging 또는 Dynatrace로
X-Cache-Policy, 304 비율 추적 - CDN(예: CloudFront, BTP Edge)을 앞단에 둘 경우
privatevspublic구분에 특히 주의 — 인증된 사용자별 응답에public이 붙으면 정보 유출 가능 - CSRF 토큰 응답에는 절대 캐시를 걸지 않기
6. 실전 패턴
6.1 정적 리소스 패턴
파일명 자체에 해시(app.7c2f.js)를 넣는 콘텐츠 해싱 + immutable 헤더 조합이 가장 안전해요. UI5 빌드 시 ui5 build --include-task=generateVersionInfo를 활성화하면 sap-ui-version.json이 생성되고 Cache Buster가 이를 참조해요.
6.2 동적 API 패턴
OData 컬렉션은 Cache-Control: private, max-age=30, must-revalidate처럼 짧은 TTL + 사용자별 분리. 단건 조회/수정은 ETag 기반 낙관적 잠금 활용. PATCH 요청 시 If-Match 헤더로 동시 수정 충돌을 412 Precondition Failed로 잡아낼 수 있어요.
6.3 배포 전략
blue-green 배포 시 두 버전이 동시에 떠 있는 동안 캐시 버스터 토큰이 충돌하지 않도록 토큰 생성 알고리즘을 빌드 시점 git commit SHA로 고정하길 권장해요. 타임스탬프 방식은 동일 commit이 두 인스턴스에서 다른 토큰을 받는 문제가 일반적으로 발생해요.
7. 흔한 실수와 트러블슈팅
FAQ 1. 헤더를 분명히 설정했는데 응답에 안 보여요
대부분 라우트 매칭 순서 문제예요. xs-app.json은 위에서부터 첫 매치만 적용해요. 일반화된 ^/(.*)$ 라우트를 맨 위에 두면 그 아래 세부 규칙이 무시돼요. cf logs <app> --recent로 Approuter 로그를 보고 어느 라우트가 매치됐는지 확인하세요.
FAQ 2. no-cache와 no-store, 뭘 써야 하나요
no-cache는 캐시는 저장하되 사용 전 서버 검증을 강제해요(ETag와 궁합 좋음). no-store는 디스크/메모리에 아예 저장하지 말라는 강한 지시예요. 토큰, 결제 정보 등 민감 데이터는 no-store, private 조합, 일반 HTML/manifest는 no-cache가 일반적으로 적합해요. 둘을 혼동해서 정적 리소스에 no-store를 걸면 성능 이득이 0이 돼요.
FAQ 3. ETag가 응답에 안 붙어요
CAP의 경우 엔티티에 @Core.OptimisticConcurrency가 누락됐거나, 엔티티에 managed aspect가 없어서 modifiedAt 필드 자체가 없는 경우가 흔해요. NetWeaver Gateway라면 etag-control 메타데이터를 별도로 설정해야 해요. Approuter나 CDN이 응답 헤더에서 ETag를 스트립하는 사례도 있으니 백엔드 직결 호출로 먼저 확인하세요.
FAQ 4. 배포했는데 사용자에게 옛 버전이 계속 보여요
index.html 자체에 max-age가 걸린 경우가 99%에요. index.html과 manifest.json은 반드시 no-cache로 두고, 나머지 정적 리소스는 Cache Buster 토큰을 통해 자연스럽게 무효화되도록 하세요. 사용자에게 강제 새로고침(Ctrl+F5)을 요청하는 건 근본 해결이 아니에요.
8. 다음으로 확장해볼 주제 + 참고 자료
여기까지 익혔다면 다음 단계로 확장해볼 수 있는 주제예요.
- Service Worker로 오프라인 캐시(Workbox + UI5)
- SAP BTP CDN/Edge 서비스와 Approuter 캐시 헤더 정합성
- HTTP/2 Server Push 및
preload/prefetch힌트 - OData v4 Delta Query로 컬렉션 캐시 신선도 유지
- Performance Budget 설정 + Lighthouse CI 연동
댓글 0
아직 댓글이 없습니다.