Post

[HTTP] 06. 성능 - 17장. 캐싱

[HTTP] 06. 성능 - 17장. 캐싱

06. 성능


17장. 캐싱

캐시는 데이터를 미리 복사해 놓는 별도의 저장소를 말한다.
반복 작업을 줄이고 어플리케이션의 성능을 높여주는 역할을 한다.

17-1. 시간 기반 캐싱

  • 서버가 파일 수정일을 Last-Modified 응답 헤더에 싣는다.
  • 브라우저가 파일을 캐싱하고 If-Modified-Since 요청 헤더에 싣는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const modified = stat.mtime;

if (req.headers['if-modified-since']) {
  const modifiedSince = new Date(req.headers['if-modified-since']);

  const isFresh = !(Math.floor(modifiedSince.getTime() / 1000) < Math.floor(modified.getTime() / 1000));

  if (isFresh) {
    res.statusCode = 304; // Not Modified
    res.end();
    return;
  }
}

res.setHeader('Last-Modified', modified.toUTCString());

17-2. 내용 기반 캐싱

  • 시간 기반의 캐시는 한계가 있다.
  • 파일 내용을 비교하는 방법을 ETag라고 한다.
  • 서버가 해당 패시값을 ETag 응답 헤더에 싣는다.
  • 브라우저가 파일을 캐싱하고 If-None-Match 요청 헤더에 싣는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const etag = `${stat.mtime.getTime().toString(16)}-${stat.size.toString(16)}`;

if (req.headers['if-none-match']) {
  const noneMatch = req.headers['if-none-match'];

  const isFresh = noneMatch === etag;

  if (isFresh) {
    res.statusCode = 304; // Not Modified
    res.end();
    return;
  }
}

res.setHeader('ETag', etag);

17-3. 캐시 제어

  • 서버는 더 세밀한 캐시 정책을 Cache-Control 응답 헤더에 싣는다.
    • max-age: 브라우저가 자원을 일정기간 캐싱하고 서버에 접속하지 않는다.
    • no-cache: 브라우저는 캐시가 신선한지 매번 서버에 접속해 확인한다.
    • no-store: 브라우저는 이 자원을 캐싱하지 않는다.
1
2
3
4
< Cache-Control: max-age=5  # 5초
< Cache-Control: max-age=0
< Cache-Control: no-cache
< Cache-Control: no-store # 캐싱하지 마

17-4. 기타 캐싱 헤더

  • Expires: 서버가 파일의 캐시 만료일을 지정하는 응답 헤더 (서버에 접속하지 않고 브라우저에 캐싱한 정보만 사용한다.)
    • 다른 시간 컨트롤 헤더와 비교
      • Last-Modified: 매번 서버에 접속해서 캐시 신선도를 검증한다.
      • Cache-Control: max-age=xxx: 초단위 값 사용
1
2
# 서버가 파일의 유효기간을 Expires라는 응답 헤더에 싣는다.
< Expires: Tue, 31 Dec 2024 15:00:00 GMT
  • Vary: 서버가 클라이언트에게 캐시 식별자를 전달하는 헤더
1
2
# accept-language별로 응답을 구상하라
< Vary: accept-language

17-5. 캐싱 활용 전략

  • HTML이 아닌 파일: 최대한 길게 캐싱한다. (1년 설정 권장, Cache-Control: max-age=315360000)
  • HTML 파일: 서버에 캐시 신선도를 확인한다.
  • 파일별로 Cache-Control 캐시 정책을 전달해 브라우저가 네트웍 요청을 최소화 하도록 유도한다.
1
2
3
4
5
6
7
8
9
const ext = path.extname(filepath).toLowerCase();

if (ext === '.js') {
  // 자바스크립트는 한 번 다운로드 하면 1년 동안 네트워크 요청을 하지 않고 브라우저 캐시에 있는 값을 사용하도록 설정
  res.setHeader('Cache-Control', 'max-age=315360000'); // 1년
} else if (ext === '.html') {
  // 캐시는 저장하지만 브라우저가 매번 이 캐시가 유효한지를 서버한테 확인하라는 정책
  res.setHeader('Cache-Control', 'no-cache');
}

17-6. 중간 정리

  • 브라우저와 서버 간의 캐시 관련 HTTP 헤더
  • 브라우저와 서버 간의 HTTP 캐싱 매커니즘 정리
  • 캐시를 설정할 때는 무척 신중해야 한다.

출처


예제

파일구조

  • /ch17
    • public
      • favicon.ico
      • index.html
      • script.hash-1.js
    • shared
      • serve-static.js
    • server.js
1
console.log('script.hash-1.js');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
const fs = require('fs');
const path = require('path');

const serveStatic = (root) => {
  return (req, res) => {
    const filepath = path.join(root, req.url === '/' ? '/index.html' : req.url);

    fs.stat(filepath, (err, stat) => {
      if (err) {
        if (err.code === 'ENOENT') {
          res.statusCode = 404;
          res.write('Not Found\n');
          res.end();
          return;
        }

        res.statusCode = 500;
        res.write('Internal Server Error\n');
        res.end();
        return;
      }

      const etag = `${stat.mtime.getTime().toString(16)}-${stat.size.toString(16)}`;
      const modified = stat.mtime;

      if (req.headers['if-none-match']) {
        const noneMatch = req.headers['if-none-match'];

        const isFresh = noneMatch === etag;

        if (isFresh) {
          res.statusCode = 304; // Not Modified
          res.end();
          return;
        }
      }

      if (req.headers['if-modified-since']) {
        const modifiedSince = new Date(req.headers['if-modified-since']);

        const isFresh = !(Math.floor(modifiedSince.getTime() / 1000) < Math.floor(modified.getTime() / 1000));

        if (isFresh) {
          res.statusCode = 304; // Not Modified
          res.end();
          return;
        }
      }

      res.setHeader('ETag', etag);
      res.setHeader('Last-Modified', modified.toUTCString());

      fs.readFile(filepath, (err, data) => {
        if (err) {
          if (err.code === 'ENOENT') {
            res.statusCode = 404;
            res.write('Not Found\n');
            res.end();
            return;
          }

          res.statusCode = 500;
          res.write('Internal Server Error\n');
          res.end();
          return;
        }

        const ext = path.extname(filepath).toLowerCase();
        let contentType = 'text/html';
        switch (ext) {
          case '.html':
            contentType = 'text/html';
            break;
          case '.js':
            contentType = 'text/javascript';
            break;
          case '.css':
            contentType = 'text/css';
            break;
          case '.png':
            contentType = 'image/png';
            break;
          case '.json':
            contentType = 'application/json';
            break;
          case '.otf':
            contentType = 'font/otf';
            break;
          default:
            contentType = 'application/octet-stream';
        }
        res.setHeader('Content-Type', contentType);

        if (ext === '.js') {
          // 자바스크립트는 한 번 다운로드 하면 1년 동안 네트워크 요청을 하지 않고 브라우저 캐시에 있는 값을 사용하도록 설정
          res.setHeader('Cache-Control', 'max-age=315360000'); // 1년
        } else if (ext === '.html') {
          // 캐시는 저장하지만 브라우저가 매번 이 캐시가 유효한지를 서버한테 확인하라는 정책
          res.setHeader('Cache-Control', 'no-cache');
        }

        res.write(data);
        res.end();
      });
    });
  };
};

module.exports = serveStatic;
1
2
3
4
5
6
7
8
9
10
11
const http = require('http');
const path = require('path');
const static = require('./shared/serve-static');

const handler = (req, res) => {
  static(path.join(__dirname, 'public'))(req, res);
};

const server = http.createServer(handler);
const port = process.env.PORT || 3000;
server.listen(port, () => console.log(`server is running ::${port}`));
This post is licensed under CC BY 4.0 by the author.

Trending Tags