page-cover
📜
개발
Node에서 ETag/If-None-Match를 이용한 캐싱 방법
sooros5132-avatarsooros513211/30/2023적용기 Next.js Node.js JavaScript React
Next.js App Router의 환경에서 ETag(Entity tags)/If-None-Match를 이용한 캐싱, 트래픽을 아끼는 방법이다. 밑에 코드를 보면 알겠지만 Node에서도 적용이 가능하다.
서버에서 HeaderETag이나 Cache-Control을 설정하면 캐싱을 할 수 있다.
짧게 설명하자면
  • ETag - 매번 서버에 요청을 하고 서버와 클라이언트의 ETag값을 비교해 캐싱한 값을 계속 사용할 지 항상 물어본다.
  • Cache-Control - 클라이언트에서 메모리 또는 디스크에 저장된 값을 불러오므로 실제론 서버에 요청을 하지 않는다. 따라서 서버에서 변경이 생겨도 캐시 무효화를 시킬 수 없다. 추가로 장점이자 단점으론 서버가 죽어있어도 실제론 성공으로 뜬다.
    첫 요청 이후 서버를 종료했음에도 200으로 나온다.
물론 둘 다 같이 써도 된다.
문서 또는 API 데이터의 변경이 있지만 자주 일어나지 않는 곳에 적합하다. 무의미 하게 트래픽 낭비하는 것을 줄이기 위해 캐싱하면 된다.
서버에서 ETag를 기억해둘 곳(메모리)에 저장을 하고 Header에 넣어서 보내주면 된다. 이후 클라이언트에선 HeaderIf-None-MatchETag를 넣어서 보내준다.
무효화 시키고 싶을 땐(리스트에 새로운 아이템이 생긴 경우) ETag를 변경시켜주면 된다.
typescript
import crypto from 'crypto';
import { NextResponse } from 'next/server';

function generateETag() {
  return crypto.randomBytes(10).toString('hex');
}

function getETag(){
	return ETag
}

let ETag = generateETag();

export async function GET(request: Request) {
  const clientETagInHeader = request.headers.get('If-None-Match')?.trim?.() || '';
  const responseOptions: ResponseInit = {};

	if (ETag && clientETagInHeader) {
	  let serverETag = ETag;
	  const executed = /^([wW]\/)?"?([^"]+)"?$/.exec(clientETagInHeader);
	
	  if (executed) {
	    let [, isWeak, clientETag] = executed;

	    if (isWeak) {
	      serverETag = serverETag.toLowerCase();
	      clientETag = clientETag.toLowerCase();
	    }
	    if (clientETag === serverETag) {
	      return new Response(null, {
	        status: 304
	      });
	    }
	  }
	}

  const videoList = await getVideoList();

  return NextResponse.json(videoList, {
    'Cache-Control': 'public, max-age=0, must-revalidate',
    ETag: `"${lastModified.ETag}"`
  });
}
예시이다 보니 하나의 파일에 작성했지만 필요한 위치에 분리해서 사용하면 된다.
💡
클라이언트 코드가 없는 이유는 브라우저에선 ETag를 받으면 다음 요청부터는 자동으로 If-None-Match를 넣어서 요청하기 때문이다.
💡
Cache-Controlmax-age를 설정해서 n초 동안은 캐싱을 유지하게 할 수 도 있다. 이렇게 하면 응답을 받고 n초는 Cache-Control을 이용한 캐싱, 이후는 must-revalidate의 값으로 인해 ETag로 캐싱을 결정하게 된다.
그리고 ETag가 문자열 양식이 정해져 있는 것은 아니다. 데이터가 변경되지 않았단 걸 증명할 아무문자나 넣어주면 된다. 필자는 crypto 라이브러리를 이용해 랜덤 16진수 문자 20개를 사용했다.
/^([wW]\/)?"?([^"]+)"?$/ 정규표현식이 잘 되는지 테스트이다.
예상되는 4가지 문자열 모두 성공
https://github.com/sooros5132/yt-dlp-web 프로젝트에 적용했다.
HTTP Status 304가 나오면 잘 작동하는 것이다. 용량을 보면 평소엔 33~34kB가 응답으로 오지만 캐싱을 이용 하므로 Header만 필요하니 180B만 오는것을 볼 수 있다. 응답 크기가 약 186배 차이
부연 설명을 하자면
  1. 첫 요청 이후 캐싱돼서 이후엔 status304로 나오고
  1. /api/d에 요청을 하면 ETag를 변경시켜서 캐시가 무효화 된다.(사이드 작업이 있어서 오래 걸린다.)
  1. /api/d 요청 이후 계속된 작업으로 인해 ETag가 계속 변한다. 따라서 200으로 오게 된다.
  1. 수정이 끝나면 ETag가 다시 일정해지니 304를 보내게 되는 것이다.
ETag는 HTTP Status 304를 이용한 캐싱 방법이다. 304를 보낼땐 Body값이 없으니 처음 접속한 클라이언트 한테 보내면 안된다.

ETag - HTTP | MDN

ETag HTTP 응답 헤더는 특정 버전의 리소스를 식별하는 식별자입니다. 웹 서버가 내용을 확인하고 변하지 않았으면, 웹 서버로 full 요청을 보내지 않기 때문에, 캐쉬가 더 효율적이게 되고, 대역폭도 아낄 수 있습니다. 허나, 만약 내용이 변경되었다면, "mid-air collisions" 이라는 리소스 간의 동시 다발적 수정 및 덮어쓰기 현상을 막는데 유용하게 사용됩니다.

ETag - HTTP | MDN-faviconhttps://developer.mozilla.org/ko/docs/Web/HTTP/Headers/ETag

Cache-Control - HTTP | MDN

Cache-Control 일반 헤더 필드는 요청과 응답 내의 캐싱 메커니즘을 위한 디렉티브를 정하기 위해 사용됩니다. 캐싱 디렉티브는 단방향성이며, 이는 요청 내에 주어진 디렉티브가 응답 내에 주어진 디렉티브와 동일하다는 것을 뜻하지는 않는다는 것을 의미합니다.

Cache-Control - HTTP | MDN-faviconhttps://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Cache-Control

ETag와 비슷한 원리로 HeaderLast-Modified를 설정해서 하는 캐싱 방법이 있다.

Last-Modified - HTTP | MDN

Last-Modified 응답 HTTP 헤더에는 원본 서버가 리소스가 마지막으로 수정되었다고 생각하는 날짜와 시간이 포함되어 있습니다. 이 헤더는 리소스가 이전에 저장된 리소스와 동일한지 확인하기 위한 유효성 검사기로 사용됩니다. ETag 헤더보다 정확하진 않지만 이 태그는 대비책으로 사용합니다. If-Modified-Since 또는 If-Unmodified-Since헤더를 포함하는 조건부 요청은 이 필드를 사용합니다.

Last-Modified - HTTP | MDN-faviconhttps://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Last-Modified