Post

[HTTP] 03. AJAX - 7장. 진행율과 취소

[HTTP] 03. AJAX - 7장. 진행율과 취소

03. AJAX


7장. 진행율과 취소

7-1. 응답 진행율

업로드나 다운로드 시 사용자에게 진행율을 알려줄 수 있다.
https://developer.mozilla.org/ko/docs/Web/API/Response#%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4_%EB%A9%94%EC%84%9C%EB%93%9C

1
$ curl http://localhost:3000/chunk -v

7-2. 응답 취소

7-3. 요청 진행율

  • XHR 객체로 요청 진행율 계산
  • progres 이벤트 활용

7.4 중간정리

  • fetch로 응답 진행율을 계산할 수 있다.
  • fetch로 응답을 취소할 수 있다.
  • XHR 객체로 요청 진행율을 계산할 수 있다.

참고


예제

파일구조

  • /ch07
    • public
      • favicon.ico
      • index.html
      • script.js
    • shared
      • serve-static.js
    • server.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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
class Downloader {
  constructor(controller) {
    this.controller = controller;
  }

  render() {
    const downloadButton = document.createElement('button');
    downloadButton.textContent = 'Download';
    downloadButton.addEventListener('click', () => this.downloadWithAbort());
    document.body.appendChild(downloadButton);
  }

  async downloadWithAbort() {
    try {
      const response = await fetch('/chunk', {
        signal: this.controller.signal,
      });

      const totalLength = Number(response.headers.get('content-length'));
      const chunks = [];
      let receivedLength = 0;

      const render = response.body.getReader();
      while (true) {
        const { done, value } = await render.read();

        if (done) {
          this.renderResponseBody(chunks);
          break;
        }

        chunks.push(value);
        receivedLength += value.length;

        this.renderProgress(receivedLength, totalLength);
      }
    } catch (error) {
      console.error('다운로드 중 오류 발생:', error);
    }
  }

  renderProgress(receivedLength, totalLength) {
    const gaugeEl = document.createElement('div');
    gaugeEl.textContent = `[Progress] ${receivedLength}/${totalLength} byte downloaded.\n`;
    document.body.appendChild(gaugeEl);
  }

  renderResponseBody(chunks) {
    const textDecoder = new TextDecoder('utf-8');
    const responseText = chunks.map((chunk) => textDecoder.decode(chunk)).join('');

    const el = document.createElement('div');
    el.textContent = `[Response] ${responseText}`;
    document.body.appendChild(el);
  }
}

class Aborter {
  constructor(controller) {
    this.controller = controller;
  }

  render() {
    const abortButton = document.createElement('button');
    abortButton.textContent = 'abort';
    abortButton.addEventListener('click', () => {
      this.controller.abort();

      const cancelMsgEl = document.createElement('div');
      cancelMsgEl.textContent = 'Download is canceled.';
      cancelMsgEl.style.color = 'red';
      document.body.appendChild(cancelMsgEl);
    });
    document.body.appendChild(abortButton);
  }
}

class Uploader {
  render() {
    const uploadInput = document.createElement('input');
    uploadInput.type = 'file';
    uploadInput.addEventListener('change', (event) => {
      this.upload(uploadInput.files[0]);
    });
    document.body.appendChild(uploadInput);
  }

  upload(file) {
    const formData = new FormData();
    formData.append('file', file);

    const xhr = new XMLHttpRequest();
    xhr.upload.addEventListener('progress', (event) => {
      this.renderProgress(event);
    });
    xhr.open('POST', '/upload');
    xhr.send(formData);
  }

  renderProgress(event) {
    let uploadProgress = 0;

    if (event.lengthComputable) {
      uploadProgress = Math.round((event.loaded / event.total) * 100);

      const uploadGauge = document.createElement('div');
      uploadGauge.textContent = `[Progress] ${uploadProgress}% uploaded.`;
      document.body.appendChild(uploadGauge);
    }
  }
}

const init = () => {
  const controller = new AbortController();

  const downloader = new Downloader(controller);
  downloader.render();

  const aborter = new Aborter(controller);
  aborter.render();

  const uploader = new Uploader();
  uploader.render();
};

document.addEventListener('DOMContentLoaded', init);
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
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.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);

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

module.exports = serveStatic;
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
const http = require('http');
const path = require('path');
const static = require('./shared/serve-static');

const chunk = async (req, res) => {
  const totalChunks = 5;
  const delayMs = 1000;
  const chunkSize = 8;

  res.setHeader('Content-Type', 'text/plain');
  res.setHeader('Content-Length', totalChunks * chunkSize);

  for (let i = 0; i < totalChunks; i++) {
    res.write(`chunk ${i}\n`);
    await new Promise((resolve) => setTimeout(resolve, delayMs)); // 지연
  }

  res.end();
};

const upload = (req, res) => {
  res.setHeader('Content-type', 'text/plain');
  res.write('success\n');
  res.end();
};

const handler = (req, res) => {
  const { pathname } = new URL(req.url, `http://${req.headers.host}`);

  if (pathname === '/chunk') return chunk(req, res);
  if (pathname === '/upload') return upload(req, res);

  static(path.join(__dirname, 'public'))(req, res);
};

const server = http.createServer(handler);
server.listen(3000, () => console.log('server is running ::3000'));
This post is licensed under CC BY 4.0 by the author.