UI5

ETag vs Cache-Control — UI5 캐시 전략 핵심 #shorts #SAP #UI5

▶ YouTube에서 보기

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.htmlsap-ui-core.jsmanifest.jsonComponent-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.jsonresponseHeaders 옵션으로 헤더를 주입할 수 있어요.

{
  "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)을 앞단에 둘 경우 private vs public 구분에 특히 주의 — 인증된 사용자별 응답에 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.htmlmanifest.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

아직 댓글이 없습니다.