web

웹 캐싱 동작 원리 — HTTP 헤더, Service Worker, 캐시 파티셔닝

캐싱의 기본 개념부터 HTTP 캐시 헤더, 캐싱 전략 패턴, Service Worker 캐싱, 브라우저의 캐시 파티셔닝까지 정리합니다.

caching http service-worker performance browser cache-control

들어가며

웹 개발을 하다 보면 “캐시 때문에 안 보여요”, “새로고침하면 되네요” 같은 말을 자주 듣게 된다. 캐싱은 웹 성능에서 중요한 역할을 하지만, 제대로 이해하지 않으면 디버깅이 어려워지기도 한다.

이 글에서는 캐싱의 기본 개념부터 HTTP 캐시 헤더, 캐싱 전략, Service Worker 캐싱, 그리고 브라우저의 캐시 파티셔닝 정책까지 다룬다.

캐싱이란?

캐시의 정의

캐시(Cache) 는 자주 사용하는 데이터를 임시로 저장해두는 공간이다. 원본 데이터에 매번 접근하는 대신, 가까운 곳에 복사본을 두고 빠르게 가져다 쓰는 것이다.

일상생활의 예로 들면 이렇다.

도서관 책 vs 책상 위 책

- 도서관에서 책을 빌려오는 것 → 원본 서버 요청
- 자주 보는 책을 책상에 두는 것 → 캐싱
- 책상에서 바로 집어드는 것 → 캐시 히트

왜 캐싱이 필요한가?

웹에서 캐싱이 중요한 이유는 크게 세 가지다.

1. 성능 향상

서버까지 왕복하는 네트워크 요청은 시간이 걸린다. 한국에서 미국 서버까지 요청이 갔다 오려면 최소 100ms 이상이 필요하다. 캐시된 리소스는 로컬에서 즉시 가져오므로 응답 시간이 수 밀리초로 줄어든다.

2. 비용 절감

서버 요청이 줄어들면 서버 비용과 대역폭 비용이 줄어든다. 특히 CDN을 사용할 때 데이터 전송량에 따라 비용이 책정되므로, 캐싱은 직접적인 비용 절감 효과가 있다.

3. 사용자 경험

페이지 로딩이 빨라지면 사용자 이탈률이 줄어든다. Google의 연구에 따르면 페이지 로드 시간이 1초에서 3초로 늘어나면 이탈 확률이 32% 증가한다.

캐싱 레이어 개요

웹에서 캐싱은 여러 계층에서 일어난다.

[브라우저] → [CDN/프록시] → [오리진 서버]
     ↓              ↓
  브라우저 캐시    공유 캐시
  (Private)      (Shared)

이 글에서는 주로 브라우저 캐시를 중심으로 다루고, CDN 캐싱은 후속 글에서 자세히 다룰 예정이다.

브라우저 캐싱의 동작 원리

HTTP 캐시의 기본 흐름

브라우저가 리소스를 요청할 때 일어나는 과정을 살펴보자.

1. 첫 번째 요청 (캐시 없음)
   브라우저 → 서버: "index.js 주세요"
   서버 → 브라우저: "여기 있어요 + 1시간 동안 저장해두세요"
   브라우저: 파일을 캐시에 저장

2. 두 번째 요청 (캐시 있음, 유효함)
   브라우저: "캐시에 있고, 아직 유효하네"
   → 서버에 요청하지 않고 캐시에서 바로 사용

3. 세 번째 요청 (캐시 있음, 만료됨)
   브라우저 → 서버: "캐시가 만료됐는데, 바뀐 거 있어요?"
   서버 → 브라우저: "안 바뀌었어요 (304 Not Modified)"
   브라우저: 기존 캐시를 계속 사용

캐시 저장 위치: Memory Cache vs Disk Cache

브라우저 개발자 도구의 Network 탭을 보면 (memory cache) 또는 (disk cache)라고 표시되는 것을 볼 수 있다.

Memory Cache

Disk Cache

브라우저가 어디에 저장할지는 리소스의 크기, 사용 빈도, 가용 메모리 등을 고려해 자동으로 결정한다.

캐시 히트와 미스

캐시 히트(Cache Hit): 요청한 리소스가 캐시에 있고 유효해서 바로 사용할 수 있는 경우.

캐시 미스(Cache Miss): 캐시에 없거나, 있더라도 만료되어 서버에 다시 요청해야 하는 경우.

캐시 히트율이 높을수록 성능이 좋아진다. 개발자 도구에서 네트워크 요청의 Size 컬럼을 보면 캐시 상태를 확인할 수 있다.

bundle.js    (disk cache)   → 캐시 히트
api/users    200  45.2 kB   → 캐시 미스 (서버 응답)
logo.png     (memory cache) → 캐시 히트

HTTP 캐시 헤더 상세

HTTP 캐시의 동작은 서버가 보내는 응답 헤더로 제어된다. 이 헤더들을 이해하면 캐싱을 정확하게 제어할 수 있다.

Cache-Control

Cache-Control은 가장 중요한 캐시 헤더다. 다양한 디렉티브(directive)를 조합해서 캐시 동작을 세밀하게 제어할 수 있다.

max-age

캐시의 유효 기간을 초 단위로 지정한다.

Cache-Control: max-age=3600

이 응답은 3600초(1시간) 동안 캐시가 유효하다. 이 시간 동안은 서버에 재요청하지 않고 캐시를 사용한다.

no-cache

이름이 헷갈리지만, 캐시를 하지 말라는 뜻이 아니다. “캐시해도 되지만, 사용하기 전에 반드시 서버에 확인하라”는 뜻이다.

Cache-Control: no-cache

매번 서버에 “이거 아직 유효해요?”라고 물어보고, 서버가 304를 주면 캐시를 사용하고, 200과 함께 새 데이터를 주면 캐시를 갱신한다.

no-store

진짜로 캐시하지 말라는 뜻이다. 민감한 정보(개인정보, 금융 데이터 등)에 사용한다.

Cache-Control: no-store

브라우저는 이 응답을 어디에도 저장하지 않는다. 매번 서버에서 새로 받아온다.

private vs public

private: 브라우저 캐시에만 저장 가능. CDN이나 프록시는 캐시하면 안 됨.

Cache-Control: private, max-age=3600

사용자별로 다른 응답(로그인한 사용자의 프로필 정보 등)에 사용한다.

public: CDN이나 프록시도 캐시 가능.

Cache-Control: public, max-age=86400

모든 사용자에게 동일한 응답(정적 파일 등)에 사용한다.

immutable

리소스가 절대 변경되지 않음을 명시한다.

Cache-Control: public, max-age=31536000, immutable

immutable이 있으면 브라우저는 사용자가 새로고침을 해도 서버에 재검증 요청을 보내지 않는다. 파일명에 해시가 포함된 정적 자산(bundle.a1b2c3.js)에 적합하다.

s-maxage

공유 캐시(CDN, 프록시)에만 적용되는 max-age다.

Cache-Control: public, max-age=60, s-maxage=3600

이 설정은 “브라우저는 1분간 캐시하고, CDN은 1시간 캐시해라”라는 뜻이다. 브라우저에는 자주 갱신하면서 CDN에는 오래 캐시하고 싶을 때 유용하다.

stale-while-revalidate

캐시가 만료된 후에도 지정된 시간 동안은 일단 캐시를 사용하면서 백그라운드에서 재검증한다.

Cache-Control: max-age=60, stale-while-revalidate=3600

1분 후 캐시가 만료되면, 1시간 동안은 먼저 캐시된 응답을 주고 뒤에서 새 데이터를 받아온다. 사용자는 기다림 없이 바로 콘텐츠를 볼 수 있다.

ETag와 조건부 요청

ETag는 리소스의 버전을 나타내는 식별자다. 파일 내용을 기반으로 생성된 해시값이라고 생각하면 된다.

첫 번째 응답

HTTP/1.1 200 OK
ETag: "a1b2c3d4"
Content-Type: application/javascript

(파일 내용)

캐시 만료 후 재요청

GET /bundle.js HTTP/1.1
If-None-Match: "a1b2c3d4"

서버 응답 - 변경 없음

HTTP/1.1 304 Not Modified
ETag: "a1b2c3d4"

파일이 변경되지 않았으면 304와 함께 빈 본문을 보낸다. 브라우저는 기존 캐시를 계속 사용한다. 이렇게 하면 대역폭을 절약할 수 있다.

서버 응답 - 변경됨

HTTP/1.1 200 OK
ETag: "e5f6g7h8"
Content-Type: application/javascript

(새로운 파일 내용)

파일이 변경되었으면 200과 함께 새 파일을 보낸다.

Strong ETag vs Weak ETag

ETag: "a1b2c3"      # Strong ETag - 바이트 단위로 정확히 일치
ETag: W/"a1b2c3"    # Weak ETag - 의미적으로 동등함

Strong ETag는 파일이 바이트 단위로 완전히 같을 때만 일치한다. Weak ETag는 내용이 의미적으로 같으면 일치로 본다(예: 공백만 다른 경우).

Last-Modified와 If-Modified-Since

ETag의 시간 기반 버전이다.

첫 번째 응답

HTTP/1.1 200 OK
Last-Modified: Tue, 08 Apr 2026 10:00:00 GMT

재요청

GET /bundle.js HTTP/1.1
If-Modified-Since: Tue, 08 Apr 2026 10:00:00 GMT

서버는 파일 수정 시간을 비교해서 304 또는 200을 응답한다.

ETagLast-Modified가 둘 다 있으면 ETag가 우선한다. ETag가 더 정확하기 때문이다. 같은 시간에 두 번 수정되면 Last-Modified로는 구분할 수 없다.

Expires (레거시)

HTTP/1.0 시절의 캐시 헤더다.

Expires: Tue, 08 Apr 2026 11:00:00 GMT

절대적인 만료 시간을 지정한다. Cache-Control: max-age가 있으면 무시된다. 하위 호환성을 위해 함께 보내는 경우가 있다.

Cache-Control: max-age=3600
Expires: Tue, 08 Apr 2026 11:00:00 GMT

이 경우 Cache-Control이 우선한다.

캐싱 전략 패턴

상황에 따라 다른 캐싱 전략이 필요하다. 자주 사용되는 패턴들을 살펴보자.

Cache-First (캐시 우선)

캐시에 있으면 캐시를 사용하고, 없을 때만 네트워크 요청을 보낸다.

요청 → 캐시 확인 → 있음 → 캐시 반환
                 → 없음 → 네트워크 요청 → 캐시 저장 → 반환

적합한 경우

주의점

Network-First (네트워크 우선)

항상 네트워크 요청을 먼저 보내고, 실패하면 캐시를 사용한다.

요청 → 네트워크 요청 → 성공 → 캐시 저장 → 반환
                    → 실패 → 캐시 확인 → 캐시 반환

적합한 경우

주의점

Stale-While-Revalidate

캐시된 응답을 즉시 반환하면서, 백그라운드에서 캐시를 갱신한다.

요청 → 캐시 반환 (즉시)
    → 동시에 네트워크 요청 → 캐시 갱신 (다음 요청을 위해)

적합한 경우

주의점

정적 자산 vs API 응답

리소스 유형에 따라 캐싱 전략이 달라진다.

정적 자산 (JS, CSS, 이미지)

Cache-Control: public, max-age=31536000, immutable

HTML

Cache-Control: no-cache

또는

Cache-Control: max-age=0, must-revalidate

API 응답

Cache-Control: private, max-age=60, stale-while-revalidate=300

Service Worker 캐싱

HTTP 캐시는 브라우저가 자동으로 관리한다. 하지만 때로는 더 세밀한 제어가 필요하다. Service Worker를 사용하면 JavaScript로 캐시를 프로그래밍 방식으로 제어할 수 있다.

Service Worker란?

Service Worker는 브라우저와 네트워크 사이에서 동작하는 프록시 스크립트다. 웹 페이지와 별도의 스레드에서 실행되며, 네트워크 요청을 가로채서 처리할 수 있다.

[웹 페이지] ←→ [Service Worker] ←→ [네트워크/캐시]

Cache API 기본 사용법

Service Worker에서는 Cache API를 사용해 리소스를 저장하고 가져온다.

캐시 열기와 저장

// 캐시 저장소 열기
const cache = await caches.open('my-cache-v1');

// 단일 리소스 캐시
await cache.add('/styles.css');

// 여러 리소스 캐시
await cache.addAll(['/', '/styles.css', '/app.js', '/logo.png']);

캐시에서 가져오기

const response = await caches.match('/styles.css');
if (response) {
  // 캐시 히트
  return response;
}

캐시 삭제

// 특정 항목 삭제
await cache.delete('/old-file.js');

// 전체 캐시 삭제
await caches.delete('my-cache-v1');

fetch 이벤트 가로채기

Service Worker의 핵심은 fetch 이벤트를 가로채는 것이다.

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cachedResponse) => {
      // 캐시에 있으면 캐시 반환, 없으면 네트워크 요청
      return cachedResponse || fetch(event.request);
    }),
  );
});

Workbox 캐싱 전략

직접 캐싱 로직을 구현하는 것은 복잡하다. Workbox는 Google이 만든 Service Worker 라이브러리로, 다양한 캐싱 전략을 쉽게 적용할 수 있다.

CacheFirst

import { CacheFirst } from 'workbox-strategies';
import { registerRoute } from 'workbox-routing';

registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 30 * 24 * 60 * 60, // 30일
      }),
    ],
  }),
);

NetworkFirst

import { NetworkFirst } from 'workbox-strategies';

registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({
    cacheName: 'api-cache',
    networkTimeoutSeconds: 3, // 3초 안에 응답 없으면 캐시 사용
  }),
);

StaleWhileRevalidate

import { StaleWhileRevalidate } from 'workbox-strategies';

registerRoute(
  ({ request }) => request.destination === 'style',
  new StaleWhileRevalidate({
    cacheName: 'stylesheets',
  }),
);

HTTP 캐시와 Service Worker 캐시의 관계

두 캐시는 독립적으로 동작하며, Service Worker가 더 높은 우선순위를 가진다.

요청 → Service Worker 캐시 확인
    → 미스 → HTTP 캐시 확인
           → 미스 → 네트워크 요청

주의할 점은 Service Worker가 fetch로 네트워크 요청을 보내면, 그 요청도 HTTP 캐시를 거친다는 것이다. 이중 캐싱을 피하려면 fetch 옵션을 조정해야 한다.

// HTTP 캐시를 우회하고 항상 서버에서 가져오기
const response = await fetch(request, { cache: 'no-store' });

오프라인 지원

Service Worker를 활용하면 오프라인 지원도 구현할 수 있다.

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return (
        response ||
        fetch(event.request).catch(() => {
          // 네트워크 실패 시 오프라인 페이지 반환
          return caches.match('/offline.html');
        })
      );
    }),
  );
});

캐시 파티셔닝

2020년부터 주요 브라우저들은 캐시 파티셔닝(Cache Partitioning) 을 도입했다. 이는 캐싱의 동작 방식을 근본적으로 바꾼 중요한 변화다.

캐시 파티셔닝이란?

과거에는 브라우저 캐시가 전역적으로 공유되었다. 예를 들어 site-a.com에서 cdn.example.com/library.js를 로드하면, 나중에 site-b.com에서 같은 파일을 요청할 때 캐시를 재사용할 수 있었다.

[과거 - 공유 캐시]
site-a.com에서 cdn.example.com/lib.js 로드 → 캐시 저장
site-b.com에서 cdn.example.com/lib.js 로드 → 캐시 히트!

캐시 파티셔닝 이후에는 최상위 사이트(top-level site) 별로 캐시가 분리된다.

[현재 - 파티셔닝된 캐시]
site-a.com에서 cdn.example.com/lib.js 로드 → [site-a.com] 캐시에 저장
site-b.com에서 cdn.example.com/lib.js 로드 → [site-b.com] 캐시는 비어있음 → 다시 다운로드

왜 이런 변화가 생겼나?

프라이버시 보호가 주된 이유다.

공유 캐시는 사이트 간 추적에 악용될 수 있었다. 예를 들어, 악의적인 사이트가 특정 리소스의 로딩 시간을 측정해서 사용자가 특정 사이트를 방문했는지 추론할 수 있었다.

// 타이밍 공격 예시 (파티셔닝 이전)
const start = performance.now();
const img = new Image();
img.src = 'https://specific-site.com/logo.png';
img.onload = () => {
  const duration = performance.now() - start;
  if (duration < 10) {
    // 캐시에서 로드됨 → 사용자가 specific-site.com을 방문한 적 있음
  }
};

영향을 받는 캐시 종류

파티셔닝은 HTTP 캐시뿐만 아니라 여러 캐시에 적용된다.

프라이버시 vs 성능 트레이드오프

캐시 파티셔닝은 프라이버시를 강화하지만, 성능에는 부정적인 영향이 있다.

영향받는 케이스

예전에는 많은 사이트가 같은 CDN URL에서 jQuery를 로드했고, 한 번 캐시되면 다른 사이트에서도 재사용됐다. 이제는 각 사이트마다 다시 다운로드해야 한다.

대응 방안

  1. 셀프 호스팅 고려: 인기 있는 라이브러리를 자체 도메인에서 호스팅하면, 자신의 사이트 내에서는 캐시가 공유됨.

  2. 번들링 최적화: 외부 CDN 의존도를 줄이고, 번들에 포함시키는 것이 오히려 효율적일 수 있음.

  3. HTTP/2, HTTP/3 활용: 멀티플렉싱으로 여러 파일을 효율적으로 전송.

브라우저별 구현 현황

현재 모든 주요 브라우저에서 캐시 파티셔닝이 적용되어 있다.

프론트엔드 빌드와 캐싱

프론트엔드 빌드 도구(Webpack, Vite 등)는 캐싱을 최적화하기 위한 기능을 기본으로 제공한다.

contenthash를 활용한 캐시 버스팅

캐시 버스팅(Cache Busting) 은 파일이 변경되었을 때 브라우저가 새 버전을 다운로드하도록 강제하는 기법이다.

가장 효과적인 방법은 파일 내용 기반 해시를 파일명에 포함시키는 것이다.

Vite 설정 (기본 적용)

// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        // 기본적으로 contenthash가 적용됨
        entryFileNames: 'assets/[name]-[hash].js',
        chunkFileNames: 'assets/[name]-[hash].js',
        assetFileNames: 'assets/[name]-[hash].[ext]',
      },
    },
  },
};

Webpack 설정

// webpack.config.js
module.exports = {
  output: {
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].js',
  },
};

결과

// 파일 내용이 바뀌면 해시도 바뀜
bundle.a1b2c3d4.js  →  bundle.e5f6g7h8.js

// 변경되지 않은 파일은 해시가 유지됨
vendor.x1y2z3w4.js  →  vendor.x1y2z3w4.js (그대로)

HTML vs JS/CSS 파일의 캐시 정책 분리

파일 유형에 따라 다른 캐시 정책을 적용해야 한다.

HTML 파일

Cache-Control: no-cache

JS/CSS/이미지 (해시 포함)

Cache-Control: public, max-age=31536000, immutable

Nginx 설정 예시

# HTML 파일
location ~* \.html$ {
    add_header Cache-Control "no-cache";
}

# 해시가 포함된 정적 파일
location ~* \.[a-f0-9]{8}\.(js|css|png|jpg|gif|svg|woff2)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

캐시 트러블슈팅

캐시 관련 문제는 디버깅이 까다로울 수 있다. 자주 발생하는 문제와 해결 방법을 정리했다.

캐시가 안 풀릴 때 대처법

1. 브라우저 캐시 강제 무시

2. 개발자 도구에서 캐시 비활성화

Network 탭에서 “Disable cache” 체크. 개발자 도구가 열려있는 동안 캐시가 비활성화된다.

3. 시크릿/프라이빗 모드 사용

기존 캐시 없이 새로운 상태에서 테스트할 수 있다.

4. Application 탭에서 캐시 직접 삭제

개발자 도구 → Application → Cache Storage에서 Service Worker 캐시를 직접 확인하고 삭제할 수 있다.

개발 중 캐시 이슈 디버깅

Network 탭 활용

응답 헤더 확인

실제로 어떤 캐시 헤더가 적용되고 있는지 확인한다.

Cache-Control: max-age=3600
ETag: "abc123"
Age: 1200  (CDN 캐시에서 온 경우, 캐시된 지 1200초 경과)

curl로 직접 확인

curl -I https://example.com/bundle.js

브라우저를 거치지 않고 서버 응답을 직접 확인할 수 있다.

자주 하는 실수

1. HTML에 긴 max-age 설정

# 이렇게 하면 안 됨
Cache-Control: max-age=31536000

HTML 파일에 긴 캐시 시간을 설정하면, 배포 후에도 사용자가 구버전 HTML을 계속 보게 된다. HTML은 JS/CSS 파일의 경로를 포함하고 있기 때문에, HTML이 갱신되지 않으면 새로 배포한 bundle.e5f6g7h8.js가 아닌 예전 bundle.a1b2c3d4.js를 계속 요청하게 된다.

캐시가 만료되기 전까지는 브라우저가 서버에 아예 요청을 보내지 않기 때문에, 서버에서 할 수 있는 조치가 없다. 사용자가 직접 캐시를 지우거나 만료될 때까지 기다려야 한다.

# 권장
Cache-Control: no-cache

no-cache를 사용하면 매번 서버에 확인 요청을 보내면서도, 변경이 없으면 304 응답으로 대역폭을 절약할 수 있다.

예외 케이스: 변경 빈도가 낮은 정적 사이트(포트폴리오, 블로그, 문서 사이트 등)라면 긴 max-age를 설정해도 괜찮다. 어차피 자주 배포하지 않고, 배포할 때 CDN 캐시를 무효화하면 되기 때문이다. 또한 URL 자체에 버전이 포함된 페이지(예: /docs/v1.0/intro.html)도 안전하게 긴 캐시를 적용할 수 있다.

2. no-cache와 no-store 혼동

Cache-Control: no-cache   # 캐시하되 매번 재검증
Cache-Control: no-store   # 아예 캐시하지 않음

이름만 보면 no-cache가 “캐시하지 마”라는 뜻 같지만, 실제로는 “캐시해도 되지만 사용 전에 서버에 확인해라”라는 의미다. 브라우저는 응답을 저장해두고, 다음 요청 시 ETag나 Last-Modified로 재검증한다.

no-store는 진짜로 저장하지 말라는 뜻이다. 디스크에도, 메모리에도 남기지 않는다.

민감한 정보(계좌 잔액, 개인정보, 인증 토큰이 포함된 응답 등)에 no-cache만 설정하면, 데이터가 브라우저 캐시에 남아있게 된다. 공용 PC나 공유 기기에서 문제가 될 수 있다.

# 민감한 정보에는 이렇게
Cache-Control: no-store, private

예외 케이스: 민감하지 않지만 항상 최신이어야 하는 데이터(실시간 주식 시세, 알림 개수 등)는 no-cache가 적절하다. 변경이 없으면 304로 빠르게 응답받을 수 있다.

3. 배포 후 CDN 캐시 무효화 누락

CDN은 오리진 서버의 응답을 엣지 서버에 캐시한다. 배포로 오리진의 파일이 바뀌어도, CDN의 캐시가 만료되기 전까지는 구버전이 계속 서빙된다.

특히 HTML 파일을 CDN에서 캐시할 때 이 문제가 자주 발생한다. JS/CSS는 파일명에 해시가 있어서 자동으로 새 파일을 요청하지만, HTML은 같은 URL이므로 CDN 캐시를 명시적으로 무효화해야 한다.

# AWS CloudFront 무효화 예시
aws cloudfront create-invalidation \
  --distribution-id EXXXXX \
  --paths "/index.html" "/"

예외 케이스: 파일명에 해시가 포함된 정적 자산(JS, CSS, 이미지)은 무효화가 필요 없다. 내용이 바뀌면 파일명도 바뀌므로, CDN 입장에서는 완전히 새로운 파일이다. 무효화가 필요한 건 URL은 같지만 내용이 바뀌는 파일(주로 HTML, manifest.json, robots.txt 등)이다.

비용 고려: CloudFront 기준으로 월 1,000건의 무효화 경로는 무료지만, 그 이상은 경로당 비용이 발생한다. /* 같은 와일드카드 무효화는 편하지만 경로 하나로 계산되므로 자주 사용하면 비용 효율적이지 않을 수 있다.

참고 자료