티스토리 뷰

728x90
SMALL

HTTP(Hypertext Transfer Protocol)는 웹브라우저가 서버로부터 문서를 전송받거나 폼의 내용을 보내는 방법, 그리고 해당 요청의 내용에 대해 서버가 응답하는 방법을 명세한다. 웹브라우저는 매우 많은 양의 HTTP를 처리한다. 일반적으로 HTTP는 스크립트의 제어를 받지 않는 대신 사용자가 링크를 클릭하거나 폼을 전송하거나 URL을 직접 입력할 때 발생한다.

Ajax는 HTTP를 조작하는데 특화된 웹 애플리케이션 설계 방식을 가리키는 용어다. Ajax 애플리케이션의 핵심적인 특징은 HTTP를 조작하여, 페이지를 다시 불러오지 않고도 웹 서버와 데이터를 교환할 수 있도록 하는 것이다. 웹 애플리케이션에 Ajax 기술을 활용하면 사용자 반응 데이터를 서버에 저장하거나, 처음에 간단한 페이지만 먼저 불러들인 후 추가적인 데이터와 필요한 페이지 구성 요소를 나중에 내려받는 식으로 초기 로딩 시간을 개선할 수 있다. Comet도 HTTP를 조작하는 웹 애플리케이션 설계와 관련된 용어다. 어떤 의미로 Comet은 Ajax와 반대 개념이라고 할 수 있다. Comet은 비동기적으로 메시지를 클라이언트로 보내는 방식의 통신을 가능하게 하는 웹 서버다.

XMLHttpRequest 사용하기

브라우저는 XMLHttpRequest 클래스에 HTTP API를 정의한다. 이 클래스의 각 인스턴스는 요청과 응답의 한 쌍을 나타내며, 이 객체의 프로퍼티와 메서드는 요청에 대한 세부 사항을 설정하고 응답 데이터를 추출할 수 있도록 한다.

이 HTTP API를 사용하기 위해 제일 먼저 해야 할 일은 XMLHttpRequest 객체의 인스턴스를 생성하는 것이다.

var request=new XMLHttpRequest();

HTTP 요청은 네 부분으로 구성되어 있다.

1. HTTP 요청 방법 또는 동사

2. 요청된 URL

3. 인증 정보를 포함하는 부속 요청 헤더

4. 요청 본문(선택 사항)

서버로부터 받은 HTTP 응답은 세 부분으로 구성되어 있다.

1. 요청의 성공과 실패를 구분할 수 있는 숫자와 문자 상태 코드

2. 응답 헤더의 집합

3. 응답 본문

요청 설정하기

XMLHttpRequest 객체를 생성했다면, 다음 단계로 XMLHttpRequest 객체의 open() 메서드를 사용하여 HTTP 요청을 만드는데 필요한 두 요소를 지정한다.

request.open("GET","data.csv");

open() 메서드의 첫번째 인자는 HTTP 요청 방식 또는 동사다. GET, POST 요청 방식은 보편적으로 지원된다. GET은 가장 일반적인 요청이며 POST는 추가적인 데이터를 요청 본문에 포함할 수 있고, 해당 데이터는 서버의 데이터베이스에 저장되기도 한다.

open()의 두번째 인자는 요청할 목적 URL이다.

요청 과정의 다음 단계는 요청 헤더를 설정하는 것이다. 예를 들어 POST 요청을 보낼 때 본문의 MIME 타입으로 Content-Type 헤더를 지정할 필요가 있다면

request.setRequestHeader("Content-Type", "text/plain");

같은 헤더를 여러 번 설정하더라도 이전 값을 바꿀 수 없다. 하나 이상의 값이 지정된 헤더가 포함될 뿐이다.

요청시 Authorization 헤더를 지정할 수도 있지만, 일반적으로는 필요하지 않다. 암호 보안을 적용한 URL을 요청할 때 open() 메서드의 네 번째와 다섯번째 전달인자를 각각 사용자명과 암호로 지정하면, XMLHttpRequest가 적절한 헤더를 설정해준다.

XMLHttpRequest를 사용해서 HTTP 요청을 만드는 마지막 단계는, 부가적인 요청 본문을 지정하고 서버로 전송하는 것이다. 이 작업은 send() 메서드로 수행한다. request.send(null);

ex) POST 방식을 사용하여 서버로 문자열 보내기

function postMessage(msg) {
    var request = new XMLHttpRequest();      // New request
    request.open("POST", "/log.php");        // POST to a server-side script
    // Send the message, in plain-text, as the request body
    request.setRequestHeader("Content-Type", // Request body will be plain text
                             "text/plain;charset=UTF-8");
    request.send(msg);                       // Send msg as the request body
    // The request is done. We ignore any response or any error.    
}

send() 메서드는 요청을 보낸 즉시 반환한다. 즉 서버의 응답을 기다리는 동안 브라우저를 중단시키지 않는다.

응답 데이터 가져오기

완료된 HTTP 응답에는 상태  코드와 응답 헤더, 응답 본문이 존재한다. 이런 응답에 대한 구성 요소에는 XMLHttpRequest 객체의 프로퍼티와 메서드로 접근할 수 있다.

· status와 statusText 프로퍼티는 HTTP의 상태를 숫자와 문자 형태로 반환한다. 이 프로퍼티에는 성공적인 요청을 나타내는 200이나 "OK"와 같은 값 또는 서버에서 일치하는 리소스를 찾지 못했음을 나타내는 404나 "NOT Found"와 같은 표준 HTTP 값이 존재한다.

· 응답 헤더는 getResponseHeader()와 getAllResponseHeader() 메서드를 통해 조회할 수 있다. XMLHttpRequest는쿠키를 자동으로 처리하기 때문에 getAllResponseHeader()는 쿠키 관련 헤더 값들을 제거한 후 반환하고, getResponseHeader() 메서드는 "Set-Cookie"나 "Set-Cookie2"를 전달하면 null을 반환한다.

· 응답 본문은 responseText 프로퍼티를 통해 텍스트 형태로 반환되거나 responseXML 프로퍼티를 통해 문서 형태로 반환될 수 있다. responseXML 프로퍼티는 이름과는 달리 XML 문서 뿐만 아니라 XHTML로도 사용할 수 있고, XHR2에서는 보통의 HTML 문서로도 동작한다.

XMLHttpRequest 객체는 보통 비동기 방식으로 사용한다. 응답 메서드와 프로퍼티들은 응답 결과를 받기 전까지는 유효하지 않다. 응답이 준비되었음을 통보받기 위해서는, XMLHttpRequest 객체의 readystatechange 이벤트를 사용하거나 XHR2의 새로운 이벤트인 progress를 사용해야 한다. readyState는 HTTP 요청 상태를 나타내는 정수 값이다.

이론적으로 readystatechange 이벤트는 readyState 프로퍼티의 값이 변경될 때마다 발생해야 하지만, 0이나 1로 변경될때는 발생하지 않는다.

readystatechange 이벤트를 구독하기 위해서는, XMLHttpRequest 객체의 onreadystatechange 프로퍼티를 이벤트 핸들러 함수로 설정해야 한다.

ex) onreadystatechange 이벤트 핸들러를 통해 HTTP 응답 받기

// Issue an HTTP GET request for the contents of the specified URL.
// When the response arrives successfully, verify that it is plain text
// and if so, pass it to the specified callback function
function getText(url, callback) {
    var request = new XMLHttpRequest();         // Create new request
    request.open("GET", url);                   // Specify URL to fetch
    request.onreadystatechange = function() {   // Define event listener
        // If the request is compete and was successful
        if (request.readyState === 4 && request.status === 200) {
            var type = request.getResponseHeader("Content-Type");
            if (type.match(/^text/))            // Make sure response is text
                callback(request.responseText); // Pass it to callback
        }
    };
    request.send(null);                         // Send the request now
}

동기 방식으로 응답 가져오기 

open() 메서드의 세번째 인자를 false로 지정하면, send() 메서드는 요청이 완료될 때까지 브라우저를 중단시킬 것이다. 이때는 이벤트 핸들러를 사용할 필요 없이, send() 메서드의 실행이 종료된 후 XMLHttpRequest 객체의 status와 responseText 프로퍼티를 확인하기만 하면 된다.

응답 해석하기

ex) 응답 해석하기

// Issue an HTTP GET request for the contents of the specified URL.
// When the response arrives, pass it to the callback function as a 
// parsed XML Document object, a JSON-parsed object, or a string.
function get(url, callback) {
    var request = new XMLHttpRequest();         // Create new request
    request.open("GET", url);                   // Specify URL to fetch
    request.onreadystatechange = function() {   // Define event listener
        // If the request is compete and was successful
        if (request.readyState === 4 && request.status === 200) {
            // Get the type of the response
            var type = request.getResponseHeader("Content-Type");
            // Check type so we don't get HTML documents in the future
            if (type.indexOf("xml") !== -1 && request.responseXML) 
                callback(request.responseXML);              // Document response
            else if (type === "application/json")
                callback(JSON.parse(request.responseText)); // JSON response
            else 
                callback(request.responseText);             // String response
        }
    };
    request.send(null);                         // Send the request now
}

서버의 응답을 올바르게 해석하기 위해서는, 서버가 "Content-Type" 헤더에서 지정한 것과 일치하는 MIME 타입으로 응답을 보내주어야 한다. 이 문제를 해결하기 위해 overrideMimeType() 메서드에 올바른 타입을 지정할 수 있다.

//응답을 XML 문서로 처리하지 않기로 한다.

request.overrideMimeType("text/plain; charset=utf-8") 

요청 본문 인코딩

HTTP 요청에 좀더 복잡한 데이터를 보내야 할 때도 있다. 이런 복잡한 데이터를 전송하는 방법을 설명한다.

폼 인코딩 요청

폼 데이터의 인코딩 스키마는 상대적으로 간단하다. 각 폼 요소의 이름과 값을 일반 URI 인코딩 방식(특수 문자를 16진수 형태로 치환)으로 암호화하고 이름과 값을 '=' 기호로 묶은 다음, 이 한 쌍의 이름과 값을 엠퍼센트(&)로 구분한 형태다.

find=pizza&zipcode=02134&radius=1km

이 폼 데이터 인코딩 형식의 MIME 타입은 다음과 같다.

application/x-www-form-urlencoded

{
  find: "pizza",
  zipcode: 02134,
  radius: "1km"
}

폼 인코딩은 웹에서 널리 사용되는 방식이며, 모든 서버 측 프로그래밍 언어에서 잘 지원하고 있다.

ex) HTTP 요청에 사용할 객체를 인코딩하기

/**
 * Encode the properties of an object as if they were name/value pairs from
 * an HTML form, using application/x-www-form-urlencoded format
 */
function encodeFormData(data) {
    if (!data) return "";    // Always return a string
    var pairs = [];          // To hold name=value pairs
    for(var name in data) {                                  // For each name
        if (!data.hasOwnProperty(name)) continue;            // Skip inherited
        if (typeof data[name] === "function") continue;      // Skip methods
        var value = data[name].toString();                   // Value as string
        name = encodeURIComponent(name.replace(" ", "+"));   // Encode name
        value = encodeURIComponent(value.replace(" ", "+")); // Encode value
        pairs.push(name + "=" + value);   // Remember name=value pair
    }
    return pairs.join('&'); // Return joined pairs separated with &
}

ex) 폼 인코딩 데이터를 보내는 HTTP POST 요청 만들기

function postData(url, data, callback) {
    var request = new XMLHttpRequest();            
    request.open("POST", url);                    // POST to the specified url
    request.onreadystatechange = function() {     // Simple event handler
        if (request.readyState === 4 && callback) // When response is complete
            callback(request);                    // call the callback.
    };
    request.setRequestHeader("Content-Type",      // Set Content-Type
                             "application/x-www-form-urlencoded");
    request.send(encodeFormData(data));           // Send form-encoded data
}

ex) 폼 인코딩 데이터를 보내는 HTTP GET 요청 만들기

function getData(url, data, callback) {
    var request = new XMLHttpRequest(); 
    request.open("GET", url +                     // GET the specified url
                 "?" + encodeFormData(data));     // with encoded data added
    request.onreadystatechange = function() {     // Simple event handler
        if (request.readyState === 4 && callback) callback(request);
    };
    request.send(null);                           // Send the request
}

JSON 인코딩 요청

ex) JSON 형식으로 인코딩한 데이터를 본문으로 보내는 HTTP POST 요청 만들기

function postJSON(url, data, callback) {
    var request = new XMLHttpRequest();            
    request.open("POST", url);                    // POST to the specified url
    request.onreadystatechange = function() {     // Simple event handler
        if (request.readyState === 4 && callback) // When response is complete
            callback(request);                    // call the callback.
    };
    request.setRequestHeader("Content-Type", "application/json");
    request.send(JSON.stringify(data));
}

XML 인코딩 요청

<query>
  <fiind zipcode="02134" radius="1km">
    pizza
  </find>
</query>

지금까지 살펴본 예제는 sned() 메서드에 전달되는 인자가 문자열이거나 null이었다. 사실은 이 인자로 XML Document 객체를 전달할 수도 있다.

ex) 요청 본문으로 XML 문서를 사용하는 HTTP POST 요청

// Encode what, where, and radius in an XML document and post them to the 
// specified url, invoking callback when the response is received
function postQuery(url, what, where, radius, callback) {
    var request = new XMLHttpRequest();            
    request.open("POST", url);                  // POST to the specified url
    request.onreadystatechange = function() {   // Simple event handler
        if (request.readyState === 4 && callback) callback(request);
    };

    // Create an XML document with root element <query>
    var doc = document.implementation.createDocument("", "query", null);
    var query = doc.documentElement;            // The <query> element
    var find = doc.createElement("find");       // Create a <find> element
    query.appendChild(find);                    // And add it to the <query>
    find.setAttribute("zipcode", where);        // Set attributes on <find>
    find.setAttribute("radius", radius);
    find.appendChild(doc.createTextNode(what)); // And set content of <find>

    // Now send the XML-encoded data to the server.
    // Note that the Content-Type will be automatically set.
    request.send(doc); 
}

 

파일 업로드하기

File() 객체는 생성자가 존재하지 않는다. 자바스크립트에서는 사용자가 선택한 파일을 가리키는 File 객체만을 사용할 수 있다. 예제는 파일 업로드 요소에 change 이벤트 핸들러를 추가하여, 사용자가 선택한 파일을 지정한 URL로 자동 POST 전송하는 비간섭 자바스크립트함수다.

ex) HTTP POST 요청으로 파일 업로드 하기

// Find all <input type="file"> elements with a data-uploadto attribute
// and register an onchange handler so that any selected file is 
// automatically POSTED to the specified "uploadto" URL. The server's
// response is ignored.
whenReady(function() {                        // Run when the document is ready
    var elts = document.getElementsByTagName("input"); // All input elements
    for(var i = 0; i < elts.length; i++) {             // Loop through them
        var input = elts[i];
        if (input.type !== "file") continue;  // Skip all but file upload elts
        var url = input.getAttribute("data-uploadto"); // Get upload URL
        if (!url) continue;                   // Skip any without a url

        input.addEventListener("change", function() {  // When user selects file
            var file = this.files[0];         // Assume a single file selection
            if (!file) return;                // If no file, do nothing
            var xhr = new XMLHttpRequest();   // Create a new request
            xhr.open("POST", url);            // POST to the URL
            xhr.send(file);                   // Send the file as body
        }, false);
    }
});

multipart/form-data 요청

HTML 폼에 업로드할 요소와 기타 다른 요소들이 포함되어 있을 때는 브라우저가 보통의 폼 인코딩 방식을 사용할 수 없으므로, "multipart/form-data"라는 특정 content-type을 설정하여 POST 요청을 보내야 한다. 이 인코딩은 여러 부분으로 나뉜 요청 본문을 구분하기 위해 사용하는 긴 경계 문자열을 포함한다.

XHR2는 멀티 파트 요청 본문을 간단하게 만들 수 있는 FormData API를 새롭게 정의한다. FormData 객체를 사용하려면 먼저, FormData() 클래스 생성자를 사용해서 FormData 객체를 생성한다. 두번째, 요청에 추가할 개별 부분 데이터의 개수만큼 append() 메서드를 반복 호출한다. 마지막으로, FromData 객체를 send() 메서드에 전달한다. send() 메서드는 적당한 경계 문자열이 정의된 "Content-Type" 헤더를 설정하여 요청을 전송한다.

ex) multipart/form-data 요청 본문을 POST 요청으로 보내기

function postFormData(url, data, callback) {
    if (typeof FormData === "undefined")
        throw new Error("FormData is not implemented");

    var request = new XMLHttpRequest();            // New HTTP request
    request.open("POST", url);                     // POST to the specified url
    request.onreadystatechange = function() {      // A simple event handler.
        if (request.readyState === 4 && callback)  // When response is complete
            callback(request);                     // ...call the callback.
    };
    var formdata = new FormData();
    for(var name in data) {
        if (!data.hasOwnProperty(name)) continue;  // Skip inherited properties
        var value = data[name];
        if (typeof value === "function") continue; // Skip methods
        // Each property becomes one "part" of the request.
        // File objects are allowed here
        formdata.append(name, value);              // Add name/value as one part
    }
    // Send the name/value pairs in a multipart/form-data request body. Each
    // pair is one part of the request. Note that send automatically sets
    // the Content-Type header when you pass it a FormData object
    request.send(formdata);  
}

HTTP 진행 이벤트

XHR2에서 XMLHttpRequest 객체는 요청 단계별로 서로 다른 이벤트를 발생시키므로 더이상 readyState 프로퍼티를 확인할 필요가 없다. 새 이벤트를 지원하는 브라우저에서는 이벤트를 다음과 같이 발생시킨다. send() 메서드가 호출되었을 때, loadstart 이벤트가 한 번 발생된다. 서버의 응답을 내려받는 중에는, XMLHttpRequest 객체가 progress 이벤트를 발생시키는데, 이 이벤트는 보통 50밀리초마다 발생하므로 요청의 진행 상황을 사용자에게 알려주기 위해 해당 이벤트를 활용할 수 있다. 요청이 매우 빠르게 완료되면 progress 이벤트가 발생하지 않을지도 모른다. 요청이 완료되면, load 이벤트가 발생한다.

완료된 요청이 반드시 성공한 요청이라고는 볼 수 없으므로, load 이벤트 핸들러에서는 XMLHttpRequest 객체의 상태 코드를 확인해야 한다.

HTTP 요청이 실제로 완료되는 상황은 세 가지이며, 세 상황에 일치하는 이벤트가 각각 발생한다. 요청 시간이 초과되면 timeout 이벤트가 발생된다. 요청이 취소되면, abort 이벤트가 발생된다. 마지막으로, 너무 빈번한 연결 재설정과 같은 기타 네트워크 오류일 때는, 요청이 완료되지 않도록 보호한 다음 error 이벤트가 발생된다.

브라우저는 어떤 요청이든지 load, abort, timeout, error 이벤트 중 하나만을 발생시킨다. 이 중 하나가 발생하면 브라우저가 loadend 이벤트를 발생시킨다.

각 진행 이벤트 핸들러를 등록하기 위해서 XMLHttpRequest 객체의 addEventListener() 메서드를 사용할 수 있다. onprogress, onload도 가능하다.

이 진행 이벤트들의 이벤트 객체에는 type이나 timestamp와 같이 유용한 프로퍼티가 세 가지 추가되어 있다. loaded 프로퍼티는 전송된 정보량이다. total 프로퍼티는 "Content-Length" 헤더의 값을 기반으로 한 전송받을 데이터의 총량 또는 알 수 없을 경우 0이다. 마지막으로 lengthComputable 프로퍼티는 내용의 크기를 알고 있으면 true이고 그렇지 않다면 false이다.

request.onprogress=function(e){
  if(e.lengthComputable)
    progress.innerHTML=Math.round(100*e.loaded/e.total)+"% Complete";
}

업로드 진행 이벤트

ex) HTTP 업로드 진행상황 모니터링

// Find all elements of class "fileDropTarget" and register DnD event handlers
// to make them respond to file drops.  When files are dropped, upload them to 
// the URL specified in the data-uploadto attribute.
whenReady(function() {
    var elts = document.getElementsByClassName("fileDropTarget");
    for(var i = 0; i < elts.length; i++) {
        var target = elts[i];
        var url = target.getAttribute("data-uploadto");
        if (!url) continue;
        createFileUploadDropTarget(target, url);
    }

    function createFileUploadDropTarget(target, url) {
        // Keep track of whether we're currently uploading something so we can
        // reject drops. We could handle multiple concurrent uploads, but 
        // that would make progress notification too tricky for this example.
        var uploading = false; 

        console.log(target, url);

        target.ondragenter = function(e) {
            console.log("dragenter");
            if (uploading) return;  // Ignore drags if we're busy
            var types = e.dataTransfer.types;
            if (types && 
                ((types.contains && types.contains("Files")) ||
                 (types.indexOf && types.indexOf("Files") !== -1))) {
                target.classList.add("wantdrop");
                return false;
            }
        };
        target.ondragover = function(e) { if (!uploading) return false; };
        target.ondragleave = function(e) {
            if (!uploading) target.classList.remove("wantdrop");
        };
        target.ondrop = function(e) {
            if (uploading) return false;
            var files = e.dataTransfer.files;
            if (files && files.length) {
                uploading = true;
                var message = "Uploading files:<ul>";
                for(var i = 0; i < files.length; i++) 
                    message += "<li>" + files[i].name + "</li>";
                message += "</ul>";
                
                target.innerHTML = message;
                target.classList.remove("wantdrop");
                target.classList.add("uploading");
                
                var xhr = new XMLHttpRequest();
                xhr.open("POST", url);
                var body = new FormData();
                for(var i = 0; i < files.length; i++) body.append(i, files[i]);
                xhr.upload.onprogress = function(e) {
                    if (e.lengthComputable) {
                        target.innerHTML = message +
                            Math.round(e.loaded/e.total*100) +
                            "% Complete";
                    }
                };
                xhr.upload.onload = function(e) {
                    uploading = false;
                    target.classList.remove("uploading");
                    target.innerHTML = "Drop files to upload";
                };
                xhr.send(body);

                return false;
            }
            target.classList.remove("wantdrop");
        }
    }
});

요청 취소와 타임아웃

XMLHttpRequest 객체의 abort() 메서드를 호출하면 진행 중인 HTTP 요청을 중단할 수 있다. 이 메서드를 호출하는 주된 이유는, 응답이 완료될 때까지 거릴는 시간이 너무 길어 요청 시간이 초과되었거나(timeout), 부적절한 응답을 받을 경우 해당 요청을 중단하기 위해서다. XHR2에서는 timeout 프로퍼티를 정의하여 지정한 밀리초의 시간이 지나면 자동으로 요청을 중단시키고, 해당 시점에 timeout 이벤트가 발생한다.

교차 출처(Cross-Origin) HTTP 요청

XMLHttpRequest 객체는 교차 출처 보안 정책의 일부로써 오직 해당 문서가 위치한 서버로만 요청을 보낼 수 있다. 이 제한 사항으로 보안 허점을 메울 수 있지만, 올바른 관계의 교차 출처 요청까지도 보호되므로 너무 제한적인 면이 있다. <script> 요소를 통해 자유롭게 교차 출처 요청을 만드는 것은 XMLHttpRequest를 대체할 만한 매력적인 Ajax 전송 수단이다.

XHR2에서는 HTTP 응답에 부가적으로 적절한 CORS(Cross-Origin Resource Sharing) 헤더를 보냄으로써 교차 출처 요청을 부분적으로 허용한다. CORS를 사용하기 위해서 웹 프로그래머가 트결히 해야할 일은 없다. 브라우저가 XMLHttpRequest를 위한 cors를 지원하고 웹 사이트에서 CORS를 사용하여 교차 출처 요청을 보내기만 하면, 동일 출처 정책이 느슨하게 적용되어 교차 출처 요청이 제대로 동작할 것이다.

ex) HEAD와 CORS를 사용하여 링크의 세부 사항 요청하기

/**
 * linkdetails.js
 *
 * This unobtrusive JavaScript module finds all <a> elements that have an href
 * attribute but no title attribute and adds an onmouseover event handler to 
 * them. The event handler makes an XMLHttpRequest HEAD request to fetch 
 * details about the linked resource, and then sets those details in the title
 * attribute of the link so that they will be displayed as a tooltip.
 */
whenReady(function() { 
    // Is there any chance that cross-origin requests will succeed?
    var supportsCORS = (new XMLHttpRequest()).withCredentials !== undefined;

    // Loop through all links in the document
    var links = document.getElementsByTagName('a');
    for(var i = 0; i < links.length; i++) {
        var link = links[i];
        if (!link.href) continue; // Skip anchors that are not hyperlinks
        if (link.title) continue; // Skip links that already have tooltips

        // If this is a cross-origin link
        if (link.host !== location.host || link.protocol !== location.protocol)
        {
            link.title = "Off-site link";  // Assume we can't get any more info 
            if (!supportsCORS) continue;   // Quit now if no CORS support
            // Otherwise, we might be able to learn more about the link
            // So go ahead and register the event handlers so we can try.
        }

        // Register event handler to download link details on mouse over
        if (link.addEventListener)
            link.addEventListener("mouseover", mouseoverHandler, false);
        else
            link.attachEvent("onmouseover", mouseoverHandler);
    }

    function mouseoverHandler(e) {
        var link = e.target || e.srcElement;      // The <a> element
        var url = link.href;                      // The link URL

        var req = new XMLHttpRequest();           // New request
        req.open("HEAD", url);                    // Ask for just the headers
        req.onreadystatechange = function() {     // Event handler
            if (req.readyState !== 4) return;     // Ignore incomplete requests
            if (req.status === 200) {             // If successful
                var type = req.getResponseHeader("Content-Type");   // Get
                var size = req.getResponseHeader("Content-Length"); // link
                var date = req.getResponseHeader("Last-Modified");  // details
                // Display the details in a tooltip. 
                link.title = "Type: " + type + "   \n" +  
                    "Size: " + size + "   \n" + "Date: " + date;
            }
            else {
                // If request failed, and the link doesn't already have an
                // "Off-site link" tooltip, then display the error.
                if (!link.title)
                    link.title = "Couldn't fetch details: \n" +
                        req.status + " " + req.statusText;
            }
        };
        req.send(null);
        
        // Remove handler: we only want to fetch these headers once.
        if (link.removeEventListener)
            link.removeEventListener("mouseover", mouseoverHandler, false);
        else
            link.detachEvent("onmouseover", mouseoverHandler);
    }
});

<Script> 요소를 활용한 HTTP : JSONP

<script> 요소가 아주 좋은 Ajax 전송 방식으로 사용되는 주요한 이유는, 동일 출처 정책에 영향을 받지 않아서 제3의 웹 서버로 데이터를 요청할 수 있다는 점 때문이다. 부가적인 이유로, 이 방식이 JSON으로 인코딩된 응답 본문을 자동으로 해석한다는 점도 있다. Ajax 전송 방식으로 <script> 요소를 사용하는 기술을 JSONP라고 부른다. 이 기술은 HTTP 요청에 대한 응답 본문이 JSON 형식이어야 한다. P는 내용을 메우다라는 의미의 padding 또는 prefix를 뜻하는 약자다.

JSON  형식의 데이터를 실행하면 자바스크립트 객체가 생성되지만, 아직까지는 데이터 객체일 뿐 아무것도 수행하지 않았다. 이때 JSONP에서 P에 해당되는 작업이 필요하다. 서비스에서 데이터를 괄호로 감싸서 자바스크립트 함수에 밀어넣은(pad) 형태로 응답을 보내주어야 한다.

handleResponse([1,2,{"buckle": "my shoe"}])

<script> 요소의 본문으로 응답을 함수에 밀어 넣은 형태를 사용함으로써 얻을 수 있는 몇 가지 이득이 있다. 첫번째, 이 방식을 사용하면 eval() 함수를 사용하지 않아도 JSON 형식의 데이터를 즉시 사용할 수 있다. 두번째, 이 데이터가 handleResponse() 함수로 전달되므로, 해당 데이터를 처리하는 함수가 문서 내에 미리 정의되어 있다고 가정할 수 있다.

이 예제는 JSON 요청을 만드는 함수를 정의하낟. 까다로워서 몇 가지 알아두자.

첫 번째, 해당 함수는 <script> 요소를 만들고 URL을 지정한 후 문서에 삽입한다. 이 작업은 HTTP 요청을 발생시킨다. 두번째, 각 요청이 발생할 때마다 콜백 함수는 내부적으로 새롭게 생성되어 getJSONP()의 프로퍼티로 저장된다. 마지막으로 콜백 함수는 script 요소와 콜백 자신을 스스로 제거하는 등의 필요한 후처리 작업을 한다.

ex) script 요소를 활용한 JSONP 요청 만들기

// Make a JSONP request to the specified URL and pass the parsed response
// data to the specified callback. Add a query parameter named "jsonp" to
// the URL to specify the name of the callback function for the request.
function getJSONP(url, callback) {
    // Create a unique callback name just for this request
    var cbnum = "cb" + getJSONP.counter++; // Increment counter each time
    var cbname = "getJSONP." + cbnum;      // As a property of this function
    
    // Add the callback name to the url query string using form-encoding
    // We use the parameter name "jsonp".  Some JSONP-enabled services 
    // may require a different parameter name, such as "callback".
    if (url.indexOf("?") === -1)   // URL doesn't already have a query section
        url += "?jsonp=" + cbname; // add parameter as the query section
    else                           // Otherwise, 
        url += "&jsonp=" + cbname; // add it as a new parameter.

    // Create the script element that will send this request
    var script = document.createElement("script");

    // Define the callback function that will be invoked by the script
    getJSONP[cbnum] = function(response) {
        try {
            callback(response); // Handle the response data
        }
        finally {               // Even if callback or response threw an error
            delete getJSONP[cbnum];                // Delete this function
            script.parentNode.removeChild(script); // Remove script
        }
    };

    // Now trigger the HTTP request
    script.src = url;                  // Set script url
    document.body.appendChild(script); // Add it to the document
}

getJSONP.counter = 0;  // A counter we use to create unique callback names

Server-Sent 이벤트를 활용한 Comet

Server-Sent 이벤트에서는 Comet 애플리케이션을 손쉽게 만들 수 있게 해주는 EventSource 객체를 정의한다. 간단히, EventSource() 생성자에 URL을 전달하고, 해당 인스턴스 객체의 message 이벤트를 구독하기만 하면 된다.

var ticker=new EventSource("stockprices.php");
ticker.onmessage=function(e){
  var type=e.type;
  var data=e.data;
  //이벤트 타입과 데이터 문자열을 처리한다.
}

message 이벤트의 이벤트 객체에는 서버가 보낸 데이터 문자열을 포함하는 data 프로퍼티가 존재한다. message 이벤트 객체에도 모든 이벤트 객체가 그렇듯 type 프로퍼티가 존재한다. 기본 값은 "message"지만, 이벤트를 발생시킨 출처에 따라 해당 프로퍼티의 값이 다르게 지정될 수도 있다. 단일 onmessage 이벤트 핸들러는 해당 이벤트의 출처인 서버로부터 전달받은 이벤트를 모두 처리하며, 필요할 경우 type 프로퍼티에 기반을 둔 새로운 이벤트를 발생시키기도 한다.

Server-sent 이벤트 프로토콜은 다음과 같다. 클라이언트가 서버에 연결되면 서버는 해당 연결을 유지한다. 이벤트가 발생하면 서버는 해당 연결에 여러 행의 텍스트를 기록한다. 해당 이벤트 발생 시 기록되는 형태는 다음과 같다.

event: bid

data: G00G

data: 999  //추가적인 데이터

온라인 채팅은 Comet 구조에 알맞는 애플리케이션이다. 채팅 클라이언트가 XMLHttpRequest를 사용해 새 메시지를 채팅 방에 전송하고, 메시지를 받는 사용자 측에서는 EventSource 객체를 사용해 메시지를 구독할 수 있다.

ex) EventSource를 사용한 간단한 채팅 클라이언트

<script>
window.onload = function() {
    // Take care of some UI details
    var nick = prompt("Enter your nickname");     // Get user's nickname
    var input = document.getElementById("input"); // Find the input field
    input.focus();                                // Set keyboard focus

    // Register for notification of new messages using EventSource
    var chat = new EventSource("/chat");
    chat.onmessage = function(event) {            // When a new message arrives
        var msg = event.data;                     // Get text from event object
        var node = document.createTextNode(msg);  // Make it into a text node
        var div = document.createElement("div");  // Create a <div>
        div.appendChild(node);                    // Add text node to div
        document.body.insertBefore(div, input);   // And add div before input
        input.scrollIntoView();                   // Ensure input elt is visible
    }

    // Post the user's messages to the server using XMLHttpRequest
    input.onchange = function() {                 // When user strikes return
        var msg = nick + ": " + input.value;      // Username plus user's input
        var xhr = new XMLHttpRequest();           // Create a new XHR
        xhr.open("POST", "/chat");                // to POST to /chat.
        xhr.setRequestHeader("Content-Type",      // Specify plain UTF-8 text 
                             "text/plain;charset=UTF-8");
        xhr.send(msg);                            // Send the message
        input.value = "";                         // Get ready for more input
    }
};
</script>
<!-- The chat UI is just a single text input field -->
<!-- New chat messages will be inserted before this input field -->
<input id="input" style="width:100%"/>

 

ex) 커스텀 Server-Sent 이벤트 채팅 서버

// This is server-side JavaScript, intended to be run with NodeJS.
// It implements a very simple, completely anonymous chat room.
// POST new messages to /chat, or GET a text/event-stream of messages
// from the same URL. Making a GET request to / returns a simple HTML file
// that contains the client-side chat UI.
var http = require('http');  // NodeJS HTTP server API

// The HTML file for the chat client. Used below.
var clientui = require('fs').readFileSync("chatclient.html");
var emulation = require('fs').readFileSync("EventSourceEmulation.js");

// An array of ServerResponse objects that we're going to send events to
var clients = [];

// Send a comment to the clients every 20 seconds so they don't 
// close the connection and then reconnect
setInterval(function() {
    clients.forEach(function(client) {
        client.write(":ping\n");
    });
}, 20000);

// Create a new server
var server = new http.Server();  

// When the server gets a new request, run this function
server.on("request", function (request, response) {
    // Parse the requested URL
    var url = require('url').parse(request.url);

    // If the request was for "/", send the client-side chat UI.
    if (url.pathname === "/") {  // A request for the chat UI
        response.writeHead(200, {"Content-Type": "text/html"});
        response.write("<script>" + emulation + "</script>");
        response.write(clientui);
        response.end();
        return;
    }
    // Send 404 for any request other than "/chat"
    else if (url.pathname !== "/chat") {
        response.writeHead(404);
        response.end();
        return;
    }

    // If the request was a post, then a client is posting a new message
    if (request.method === "POST") {
        request.setEncoding("utf8");
        var body = "";
        // When we get a chunk of data, add it to the body
        request.on("data", function(chunk) { body += chunk; });

        // When the request is done, send an empty response 
        // and broadcast the message to all listening clients.
        request.on("end", function() {
            response.writeHead(200);   // Respond to the request
            response.end();

            // Format the message in text/event-stream format
            // Make sure each line is prefixed with "data:" and that it is
            // terminated with two newlines.
            message = 'data: ' + body.replace('\n', '\ndata: ') + "\r\n\r\n";
            // Now send this message to all listening clients
            clients.forEach(function(client) { client.write(message); });
        });
    }
    // Otherwise, a client is requesting a stream of messages
    else {
        // Set the content type and send an initial message event 
        response.writeHead(200, {'Content-Type': "text/event-stream" });
        response.write("data: Connected\n\n");

        // If the client closes the connection, remove the corresponding
        // response object from the array of active clients
        request.connection.on("end", function() {
            clients.splice(clients.indexOf(response), 1);
            response.end();
        });

        // Remember the response object so we can send future messages to it
        clients.push(response);
    }
});

// Run the server on port 8000. Connect to http://localhost:8000/ to use it.
server.listen(8000);
728x90
LIST

' > 자바스크립트 완벽 가이드' 카테고리의 다른 글

21장 미디어와 그래픽 스크립팅  (0) 2020.11.30
20장 클라이언트 스토리지  (0) 2020.11.28
17장 이벤트 다루기  (0) 2020.11.23
16장 CSS 다루기  (0) 2020.11.22
15장 문서 스크립팅  (0) 2020.11.21
댓글
공지사항