[참조] https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest
XMLHttpRequest
- XMLHttpRequest (XHR) 객체는 서버와 상호작용할 때 사용한다. XHR을 사용하면 페이지의 새로고침 없이도 URL에서 데이터를 가져올 수 있다. 사용자 작업을 방해하지 않고 페이지 일부를 업데이트할 수 있다.
- XMLHttpRequest는 AJAX 프로그래밍에 많이 사용한다.
- XML 뿐만 아니라 JSON 등 모든 종류의 데이터를 가져올 수 있다.
- 이벤트 데이터나 메시지 데이터를 서버에서 가져와야 하는 통신의 경우, EventSource 인터페이스를 통한 서버발 이벤트(Server-sent events)의 사용을 고려하자.
- 완전한 양방향 통신은 WebSocket이 더 좋은 선택일 수 있다.
1. 생성자
XMLHttpRequest( )
const request = new XMLHttpRequest();
2. 표준 속성
XMLHttpRequestEventTarget과 EventTarget의 속성을 상속한다.
- XMLHttpRequest.readyState : (Read only) 요청 상태를 나타내는 숫자 반환
- XMLHttpRequest.response (Read only) : XMLHttpRequest.responseType 값에 따라, 응답 개체 본문을 포함하는 ArrayBuffer, Blob, Document, JavaScript 객체 또는 DOMString 반환.
- XMLHttpRequest.responseText (Read only) : 요청에 대한 응답을 텍스트 string을 반환한다. 실패했거나, 아직 전송하지않은 경우에 null 반환.
- XMLHttpRequest.responseType : 응답 유형을 지정한다.
- XMLHttpRequest.responseURL (Read only) : 응답 URL을 직렬화한 값으로 반환한다. URL이 null이면 빈 문자열을 반환한다.
- XMLHttpRequest.responseXML (Read only) : 요청에 대한 응답을 포함한 Document를 반환한다. 실패했거나, 아직 전송하지 않았거나, 응답을 XML/HTML로 파싱할 수 없는 경우 null을 반환한다. Web Worker에서 사용할 수 없다.
- XMLHttpRequest.status (Read only) : 응답 HTTP 상태 코드를 반환한다.
- XMLHttpRequest.statusText (Read only) : HTTP 서버가 반환한 응답 문자열 string을 반환한다. XMLHttpRequest.status와 달리, statusText는 "OK" 같은 응답 상태 메시지를 반환한다
[참고] HTTP/1.1 상태 텍스트처럼 프로토콜 버전과 응답 이유를 알릴 방법이 HTTP/2 명세에 없다(RFC 7540, section 8.1.2.4: Response Pseudo-Header Fields)
- XMLHttpRequest.timeout : 요청을 자동 종료하기 전에 대기할 시간 (밀리초 단위)
- XMLHttpRequest.upload (Read only) : 업로드 과정을 나타내는 XMLHttpRequestUpload이다.
- XMLHttpRequest.withCredentials : 쿠키 또는 권한 부여 헤더와 같은 자격 증명을 사용하여 사이트 간 Access-Control 요청을 만들어야 하는 경우 true를 반환한다. 그렇지 않으면 false를 반환한다.
3. 비표준 속성
- XMLHttpRequest.channel (Read only) : 요청을 수행할 때 객체에서 사용하는 채널이다.
- XMLHttpRequest.mozAnon (Read only) : true인 경우 쿠키 및 인증 헤더 없이 요청이 전송된다.
- XMLHttpRequest.mozSystem (Read only) : true인 경우 same origin policy가 요청에 적용되지 않는다.
- XMLHttpRequest.mozBackgroundRequest : 객체가 백그라운드에서 서비스 요청을 나타내는지 여부.
4. 메서드
- XMLHttpRequest.abort() : 이미 요청을 전송한 경우, 그 요청을 중단.
- XMLHttpRequest.getAllResponseHeaders() : 모든 응답 헤더를 CRLF로 구분해서 하나의 문자열로 반환한다. 응답을 받지 못한 경우 null을 반환한다.
- XMLHttpRequest.getResponseHeader() : 지정한 헤더의 텍스트를 담은 문자열을 반환한다. 응답을 받지 못했거나, 지정한 헤더가 응답에 존재하지 않으면 null을 반환한다.
- XMLHttpRequest.open() : 요청을 초기화
- XMLHttpRequest.overrideMimeType() : 서버가 반환한 MIME 유형을 재정의
- XMLHttpRequest.send() : 요청을 전송한다. 비동기 요청(기본)인 경우, send()는 요청을 전송하는 즉시 반환한다.
- XMLHttpRequest.setRequestHeader() : HTTP 요청 헤더 값을 설정한다. 반드시 send()보다 먼저, 그러나 open()보다 뒤에 호출한다. open() > XMLHttpRequest.setRequestHeader() > send()
5. 이벤트
abort : XMLHttpRequest.abort() 호출 등, 요청이 중단되면 발생한다. onabort 속성으로도 받을 수 있다.
error : 요청이 오류를 만나면 발생한다. onerror 속성으로도 수신할 수 있다.
load : XMLHttpRequest 트랜잭션이 성공하면 발생한다. onload 속성으로도 수신할 수 있다.
loadend : 요청이 성공하거나(load 이벤트), 실패(abort) 또는 error 이벤트 후 발생한다. onloadend 속성으로도 수신할 수 있다.
loadstart : 응답 데이터 로딩을 시작했을 때 발생한다. onloadstart 속성으로도 수신할 수 있다.
progress : 요청이 데이터를 수신하는 동안 주기적으로 발생한다. onprogress 속성으로도 수신할 수 있다.
readystatechange : readyState 속성이 바뀌면 발생한다. onreadystatechange 속성으로도 수신할 수 있다.
timeout : 응답에 소요된 시간이 사전에 지정한 값을 초과해서 요청이 취소될 때 발생한다. ontimeout 속성으로도 수신할 수 있다.
6. 기본 예제
function reqListener () { # 이벤트 처리 함수
console.log(this.responseText);
}
const req = new XMLHttpRequest(); // 생성자
req.addEventListener("load", reqListener); // 이벤트(load)에 대한 처리 함수 등록
req.open("GET", "http://www.example.org/example.txt"); // 웹 요청 열기
req.send(); // 요청 전송
7. 이진 데이터 예제
(1) overrideMimeType 이용하기 (예전 방식)
const req = new XMLHttpRequest();
req.open("GET", url);
req.overrideMimeType("text/plain; charset=x-user-defined"); // 데이터를 이진 문자열로 처리
(2) responseType 이용하기 (요즘 방식)
const req = new XMLHttpRequest();
req.onload = (e) => {
const arraybuffer = req.response; // not responseText
/* ... */
}
req.open("GET", url);
req.responseType = "arraybuffer";
req.send();
8. 모니터링 예제
const req = new XMLHttpRequest();
// 각 상태 이벤트 발생을 처리하는 함수 설정
req.addEventListener("progress", updateProgress);
req.addEventListener("load", transferComplete);
req.addEventListener("error", transferFailed);
req.addEventListener("abort", transferCanceled);
req.open();
// ...
// progress on transfers from the server to the client (다운로드)
function updateProgress (event) {
if (event.lengthComputable) {
const percentComplete = event.loaded / event.total * 100;
// ...
} else {
// Unable to compute progress information since the total size is unknown
}
}
function transferComplete(evt) {
console.log("The transfer is complete.");
}
function transferFailed(evt) {
console.log("An error occurred while transferring the file.");
}
function transferCanceled(evt) {
console.log("The transfer has been canceled by the user.");
}
9. abort, load, error 이벤트를 loadend로 한번에 처리하기
req.addEventListener("loadend", loadEnd);
function loadEnd(e) {
console.log("The transfer finished (although we don't know if it succeeded or not).");
}
10. HTML <form> 네 가지 전송 방식
- POST 메서드 사용, enctype 속성을 application/x-www-form-urlencoded(기본값)로 설정
[전송 예]
Content-Type: application/x-www-form-urlencoded
foo=bar&baz=The+first+line.%0D%0AThe+second+line.%0D%0A
- POST 메서드 사용, enctype 속성을 text/plain으로 설정
[전송 예]
Content-Type: text/plain
foo=bar
baz=The first line.
The second line.
- POST 메서드 사용, enctype 속성을 multipart/form-data로 설정
[전송 예]
Content-Type: multipart/form-data; boundary=---------------------------314911788813839
-----------------------------314911788813839
Content-Disposition: form-data; name="foo"
bar
-----------------------------314911788813839
Content-Disposition: form-data; name="baz"
The first line.
The second line.
-----------------------------314911788813839--
- GET 메서드 사용(이 경우 enctype 속성은 무시됨)
[전송 예]
?foo=bar&baz=The%20first%20line.%0AThe%20second%20line.
11. 네 가지 방식, 파일 업로드 예제
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Sending forms with pure AJAX – MDN</title>
<script>
"use strict";
// :: XHR Form Submit Framework ::
//
// https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/Using_XMLHttpRequest
//
// This framework is released under the GNU Public License, version 3 or later.
// https://www.gnu.org/licenses/gpl-3.0-standalone.html
//
// Syntax:
// XHRSubmit(HTMLFormElement);
const XHRSubmit = (function () {
function xhrSuccess() {
console.log(this.responseText);
// you can get the serialized data through the "submittedData" custom property:
// console.log(JSON.stringify(this.submittedData));
}
function submitData(data) {
const req = new XMLHttpRequest();
req.submittedData = data;
req.onload = xhrSuccess;
if (data.technique === 0) {
// method is GET
req.open(
"get",
data.receiver.replace(
/(?:\?.*)?$/,
data.segments.length > 0 ? `?${data.segments.join("&")}` : ""
),
true
);
req.send(null);
} else {
// method is POST
req.open("post", data.receiver, true);
if (data.technique === 3) {
// enctype is multipart/form-data
const boundary =
"---------------------------" + Date.now().toString(16);
req.setRequestHeader(
"Content-Type",
`multipart\/form-data; boundary=${boundary}`
);
req.sendAsBinary(
`--${boundary}\r\n` +
data.segments.join(`--${boundary}\r\n`) +
`--${boundary}--\r\n`
);
} else {
// enctype is application/x-www-form-urlencoded or text/plain
req.setRequestHeader("Content-Type", data.contentType);
req.send(data.segments.join(data.technique === 2 ? "\r\n" : "&"));
}
}
}
function processStatus(data) {
if (data.status > 0) {
return;
}
// the form is now totally serialized! do something before sending it to the server…
// doSomething(data);
// console.log("XHRSubmit - The form is now serialized. Submitting...");
submitData(data);
}
function pushSegment(segment) {
this.owner.segments[this.segmentIdx] +=
segment.target.result + "\r\n";
this.owner.status--;
processStatus(this.owner);
}
function plainEscape(text) {
// How should I treat a text/plain form encoding?
// What characters are not allowed? this is what I suppose…:
// "4\3\7 - Einstein said E=mc2" ----> "4\\3\\7\ -\ Einstein\ said\ E\=mc2"
return text.replace(/[\s\=\\]/g, "\\$&");
}
function SubmitRequest(target) {
const isPost = target.method.toLowerCase() === "post";
this.contentType =
isPost && target.enctype
? target.enctype
: "application\/x-www-form-urlencoded";
this.technique = isPost
? this.contentType === "multipart\/form-data"
? 3
: this.contentType === "text\/plain"
? 2
: 1
: 0;
this.receiver = target.action;
this.status = 0;
this.segments = [];
const filter = this.technique === 2 ? plainEscape : escape;
for (const field of target.elements) {
if (!field.hasAttribute("name")) {
continue;
}
const fieldType =
field.nodeName.toUpperCase() === "INPUT" &&
field.hasAttribute("type")
? field.getAttribute("type").toUpperCase()
: "TEXT";
if (fieldType === "FILE" && field.files.length > 0) {
if (this.technique === 3) {
// enctype is multipart/form-data
for (const file of field.files) {
const segmReq = new FileReader();
// Custom properties:
segmReq.segmentIdx = this.segments.length;
segmReq.owner = this;
segmReq.onload = pushSegment;
this.segments.push(
'Content-Disposition: form-data; name="' +
field.name +
'"; filename="' +
file.name +
'"\r\nContent-Type: ' +
file.type +
"\r\n\r\n"
);
this.status++;
segmReq.readAsBinaryString(file);
}
} else {
// enctype is application/x-www-form-urlencoded or text/plain or
// method is GET: files will not be sent!
for (const file of field.files) {
this.segments.push(
`${filter(field.name)}=${filter(file.name)}`
);
}
}
} else if (
(fieldType !== "RADIO" && fieldType !== "CHECKBOX") ||
field.checked
) {
// NOTE: this will submit _all_ submit buttons. Detecting the correct one is non-trivial.
// field type is not FILE or is FILE but is empty.
if (this.technique === 3) {
// enctype is multipart/form-data
this.segments.push(
`Content-Disposition: form-data; name="${field.name}"\r\n\r\n${field.value}\r\n`
);
} else {
// enctype is application/x-www-form-urlencoded or text/plain or method is GET
this.segments.push(
`${filter(field.name)}=${filter(field.value)}`
);
}
}
}
processStatus(this);
}
return (formElement) => {
if (!formeElement.action) {
return;
}
new SubmitRequest(formElement);
};
})();
</script>
</head>
<body>
<h1>Sending forms with XHR</h1>
<h2>Using the GET method</h2>
<form
action="register.php"
method="get"
onsubmit="AJAXSubmit(this); return false;">
<fieldset>
<legend>Registration example</legend>
<p>
<label>First name: <input type="text" name="firstname" /></label><br />
<label>Last name: <input type="text" name="lastname" /></label>
</p>
<p>
<input type="submit" value="Submit" />
</p>
</fieldset>
</form>
<h2>Using the POST method</h2>
<h3>Enctype: application/x-www-form-urlencoded (default)</h3>
<form
action="register.php"
method="post"
onsubmit="AJAXSubmit(this); return false;">
<fieldset>
<legend>Registration example</legend>
<p>
<label>First name:
<input type="text" name="firstname" />
</label><br />
<label>Last name:
<input type="text" name="lastname" />
</label>
</p>
<p>
<input type="submit" value="Submit" />
</p>
</fieldset>
</form>
<h3>Enctype: text/plain</h3>
<form
action="register.php"
method="post"
enctype="text/plain"
onsubmit="AJAXSubmit(this); return false;">
<fieldset>
<legend>Registration example</legend>
<p>
<label>Your name:
<input type="text" name="user" />
</label>
</p>
<p>
<label>Your message:<br />
<textarea name="message" cols="40" rows="8"></textarea>
</label>
</p>
<p>
<input type="submit" value="Submit" />
</p>
</fieldset>
</form>
<h3>Enctype: multipart/form-data</h3>
<form
action="register.php"
method="post"
enctype="multipart/form-data"
onsubmit="AJAXSubmit(this); return false;">
<fieldset>
<legend>Upload example</legend>
<p>
<label>First name: <input type="text" name="firstname" /></label><br />
<label>Last name: <input type="text" name="lastname" /></label><br />
Sex:
<input id="sex_male" type="radio" name="sex" value="male" />
<label for="sex_male">Male</label>
<input id="sex_female" type="radio" name="sex" value="female" />
<label for="sex_female">Female</label><br />
Password: <input type="password" name="secret" /><br />
<label>What do you prefer:
<select name="image_type">
<option>Books</option>
<option>Cinema</option>
<option>TV</option>
</select>
</label>
</p>
<p>
<label>Post your photos:
<input type="file" multiple name="photos[]" />
</label>
</p>
<p>
<input
id="vehicle_bike"
type="checkbox"
name="vehicle[]"
value="Bike" />
<label for="vehicle_bike">I have a bike</label><br />
<input
id="vehicle_car"
type="checkbox"
name="vehicle[]"
value="Car" />
<label for="vehicle_car">I have a car</label>
</p>
<p>
<label>Describe yourself:<br />
<textarea name="description" cols="50" rows="8"></textarea>
</label>
</p>
<p>
<input type="submit" value="Submit" />
</p>
</fieldset>
</form>
</body>
</html>
12. 네가지 방식 파일 업로드 : FormData 객체 이용하기
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>Sending forms with FormData – MDN</title>
<script>
"use strict";
function xhrSuccess () {
console.log(this.responseText);
}
function XHRSubmit (formElement) {
if (!formElement.action) { return; }
const req = new XMLHttpRequest();
req.onload = xhrSuccess;
if (fFormElement.method.toLowerCase() === "post") {
req.open("post", formElement.action);
req.send(new FormData(formElement));
} else {
let search = "";
for (const field of formElement.elements) {
if (!field.hasAttribute("name")) { continue; }
const fieldType = field.nodeName.toUpperCase() === "INPUT" && oField.hasAttribute("type")
? field.getAttribute("type").toUpperCase()
: "TEXT";
if (fieldType === "FILE") {
for (const file of field.files) {
search += `&${escape(field.name)}=${escape(file.name)}`;
} else if ((fieldType !== "RADIO" && fieldType !== "CHECKBOX") || field.checked) {
search += `&${escape(field.name)}=${escape(field.value)}`;
}
}
req.open("get", formElement.action.replace(/(?:\?.*)?$/, search.replace(/^&/, "?")), true);
req.send(null);
}
}
</script>
</head>
<body>
<h1>Sending forms with FormData</h1>
<h2>Using the GET method</h2>
<form
action="register.php"
method="get"
onsubmit="AJAXSubmit(this); return false;">
<fieldset>
<legend>Registration example</legend>
<p>
First name: <input type="text" name="firstname" /><br />
Last name: <input type="text" name="lastname" />
</p>
<p>
<input type="submit" value="Submit" />
</p>
</fieldset>
</form>
<h2>Using the POST method</h2>
<h3>Enctype: application/x-www-form-urlencoded (default)</h3>
<form
action="register.php"
method="post"
onsubmit="AJAXSubmit(this); return false;">
<fieldset>
<legend>Registration example</legend>
<p>
First name: <input type="text" name="firstname" /><br />
Last name: <input type="text" name="lastname" />
</p>
<p>
<input type="submit" value="Submit" />
</p>
</fieldset>
</form>
<h3>Enctype: text/plain</h3>
<p>The text/plain encoding is not supported by the FormData API.</p>
<h3>Enctype: multipart/form-data</h3>
<form
action="register.php"
method="post"
enctype="multipart/form-data"
onsubmit="AJAXSubmit(this); return false;">
<fieldset>
<legend>Upload example</legend>
<p>
First name: <input type="text" name="firstname" /><br />
Last name: <input type="text" name="lastname" /><br />
Sex:
<input id="sex_male" type="radio" name="sex" value="male" />
<label for="sex_male">Male</label>
<input id="sex_female" type="radio" name="sex" value="female" />
<label for="sex_female">Female</label><br />
Password: <input type="password" name="secret" /><br />
What do you prefer:
<select name="image_type">
<option>Books</option>
<option>Cinema</option>
<option>TV</option>
</select>
</p>
<p>
Post your photos:
<input type="file" multiple name="photos[]" />
</p>
<p>
<input
id="vehicle_bike"
type="checkbox"
name="vehicle[]"
value="Bike" />
<label for="vehicle_bike">I have a bike</label><br />
<input
id="vehicle_car"
type="checkbox"
name="vehicle[]"
value="Car" />
<label for="vehicle_car">I have a car</label>
</p>
<p>
Describe yourself:<br />
<textarea name="description" cols="50" rows="8"></textarea>
</p>
<p>
<input type="submit" value="Submit" />
</p>
</fieldset>
</form>
</body>
</html>
13. 마지막 수정 날짜 가져오기
function getHeaderTime() {
console.log(this.getResponseHeader("Last-Modified")); // 유효한 GMTString 날짜 또는 null
}
const req = new XMLHttpRequest();
req.open("HEAD" /* use HEAD when you only need the headers! */, "yourpage.html");
req.onload = getHeaderTime;
req.send();
14. 마지막 수정 날짜가 변경되면 작업 수행
function getHeaderTime() {
const lastVisit = parseFloat(window.localStorage.getItem(`lm_${this.filepath}`));
const lastModified = Date.parse(this.getResponseHeader("Last-Modified"));
if (isNaN(lastVisit) || lastModified > lastVisit) {
window.localStorage.setItem(`lm_${this.filepath}`, Date.now());
isFinite(lastVisit) && this.callback(lastModified, lastVisit);
}
}
function ifHasChanged(URL, callback) {
const req = new XMLHttpRequest();
req.open("HEAD" /* use HEAD - we only need the headers! */, URL);
req.callback = callback;
req.filepath = URL;
req.onload = getHeaderTime;
req.send();
}
테스트하기:
// 테스트 파일e "yourpage.html"
ifHasChanged("yourpage.html", function (modified, visit) {
console.log(`The page '${this.filepath}' has been changed on ${(new Date(nModified)).toLocaleString()}!`);
});
[참고] 페이지가 변경되었는가? document.lastModified 참조
15. Cross-site XMLHttpRequest
최신 브라우저는 CORS(Cross-Origin Resource Sharing) 표준을 구현하여 사이트 간 요청을 지원한다. 서버가 웹 응용 프로그램의 원본에서 오는 요청을 허용하도록 구성되어 있는 한 XMLHttpRequest가 작동한다. 그렇지 않으면 INVALID_ACCESS_ERR 예외가 발생한다.
16. 캐시 우회
캐시를 우회하는 브라우저 간 호환 접근 방식은 URL에 타임스탬프를 추가하고 "?" 또는 "&"를 적절하게 사용하는 것이다.
http://foo.com/bar.html -> http://foo.com/bar.html?12345
http://foo.com/bar.html?foobar=baz -> http://foo.com/bar.html?foobar=baz&12345
로컬 캐시는 URL로 인덱싱되므로 모든 요청이 다르게 처리되어 캐시를 우회한다.
다음 코드를 사용하여 URL을 자동으로 조정할 수 있다.
const req = new XMLHttpRequest();
req.open("GET", url + (/\?/.test(url) ? "&" : "?") + new Date().getTime());
req.send(null);
17. 보안
cross-site scripting을 활성하려면 XMLHttpRequest. 응답에서 Access-Control-Allow-Origin HTTP 헤더를 사용하자.
XMLHttpRequests 중지 중:
상태=0 및 statusText=null을 수신하는 XMLHttpRequest로 결론나면 요청을 수행할 수 없음을 의미한다. 이것은 UNSENT이다. 이에 대한 원인은 XMLHttpRequest가 이후에 open() 될 때 XMLHttpRequest 원본(XMLHttpRequest 생성 시)이 변경된 경우이다. 이 경우는 예를 들어 창에 대한 onunload 이벤트에서 시작되는 XMLHttpRequest가 있는 경우 닫힐 창이 여전히 있을 때 예상되는 XMLHttpRequest가 생성되고 마지막으로 요청을 보내는 경우(즉, open()) 이 창이 초점을 잃었고 다른 창이 초점을 얻었을 때. 이 문제를 피하는 가장 효과적인 방법은 종료된 창에서 언로드 이벤트가 트리거되면 설정되는 새 창의 DOMActivate 이벤트에 리스너를 설정하는 것이다.
'AJAX, JSON' 카테고리의 다른 글
날씨 정보 API 이용 예제 ( $getJSON ) (0) | 2022.09.23 |
---|---|
Ajax 기능별 웹브라우저 및 버전 호환성 (0) | 2022.09.23 |
Ajax가 뭐야 (0) | 2022.06.09 |
JSON 가이드 (TCP School) (0) | 2022.06.06 |
Ajax 가이드 (TCP School) (0) | 2022.06.06 |