[HTTP] 03. AJAX - 7장. 진행율과 취소
[HTTP] 03. AJAX - 7장. 진행율과 취소
03. AJAX
- 🔗 https://github.com/jeonghwan-kim/lecture-http
🔗 https://jeonghwan-kim.github.io/2024/07/09/lecture-http-part3
직접 만들 수 있는 HTTP 요청
- 6장. AJAX 요청과 응답: fetch 함수로 AJAX 요청과 응답을 다루는 법에 대해
- 7장. AJAX 진행율과 취소: AJAX 진행율을 계산하는 방법과 요청을 취소하는 방법에 대해
- 8장. AJAX 라이브러리: fetch와 XHR 객체 기반의 주요 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
- Response.body 속성
- 서버 준비
- 응답 진행율 계산
1
$ curl http://localhost:3000/chunk -v
7-2. 응답 취소
- AbortController
- AbortSignal
- Request: signal 속성
- 응답 취소 구현
7-3. 요청 진행율
- XHR 객체로 요청 진행율 계산
- progres 이벤트 활용
7.4 중간정리
- fetch로 응답 진행율을 계산할 수 있다.
- fetch로 응답을 취소할 수 있다.
- XHR 객체로 요청 진행율을 계산할 수 있다.
참고
- Fetch Progress | JAVASCRIPT.INFO
- ReadableStream | MDN
- Fetch Abort | MDN
- AbortController | MDN
- AbortSignal | MDN
- XMLHttpRequst | JAVASCRIPT.INFO
예제
파일구조
- /ch07
- public
- favicon.ico
- index.html
- script.js
- shared
- serve-static.js
- server.js
- public
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.