티스토리 뷰

책/자바스크립트 완벽 가이드

22장 HTML5 API

안양사람 2020. 11. 30. 22:32
728x90
SMALL

Geolocation

Geolocation API는 잦바스크립트 프로그램이 사용자의 실제 위치를 브라우저에게 요청할 수 있도록 해준다. 브라우저에서는 물리적인 위치 정보에 접근하기 전에 자바스크립트 프로그래밍 항상 사용자의 허락을 받도록 하고 있다.

navigator.geolocation.getCurrentPosition()

사용자의 현재 위치를 요청한다.

navigator.geolocation.watchPosition()

현재 위치를 요청하는 것은 동일하지만, 지속적으로 확인하여 사용자의 위치가 변경될 때마다 지정된 콜백 함수를 호출

navigator.geolocation.clearWatch()

사용자의 위치 정보를 수집하는 작업을 중단한다. 이 메서드의 전달인자는 watchPosition()을 호출한 다음 반환받은 숫자 값이어야 한다.

 

위치를 요청하는 가장 간단한 형태는 다음과 같다.

navigator.geolocation.getCurrentPosition(function(pos){
  var latitude=pos.coords.latitude;
  var longitude=pos.coords.longitude;
  alert("현재 위치는: "+latitude+", "+longitude);
});

성공적인 모든 geolocation 요청은 위도와 경도 외에도 미터 단위의 정확도 값을 반환한다. 이 정확도 값은 해당 좌표와 얼마나 가까운지를 나타낸다.

ex) 지도를 출력하기 위해 geolocation 활용하기

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="hi"></div>
</body>
<script>
  document.getElementById("hi").appendChild(getmap());
  // Return a newly created <img> element that will (once geolocation succeeds)
// be set to display a Google map of the current location. Note that the caller
// must insert the returned element into the document in order to make it 
// visible. Throws an error if geolocation is not supported in the browser
function getmap() {
    // Check for geolocation support
    if (!navigator.geolocation) throw "Geolocation not supported";

    // Create a new <img> element, start a geolocation request to make the img
    // display a map of where we are, and then return the image.
    var image = document.createElement("img");
    navigator.geolocation.getCurrentPosition(setMapURL);
    return image;

    // This function will be invoked after we return the image object, when
    // (and if) the geolocation request succeeds.
    function setMapURL(pos) {
        // Get our position information from the argument object
        var latitude = pos.coords.latitude;    // Degrees N of equator
        var longitude = pos.coords.longitude;  // Degrees E of Greenwich
        var accuracy = pos.coords.accuracy;    // Meters

        // Construct a URL for a static Google map image of this location
        var url = "http://maps.google.com/maps/api/staticmap" +
            "?center=" + latitude + "," + longitude + 
            "&size=640x640&sensor=true";
        
        // Set the map zoom level using a rough heuristic
        var zoomlevel=20;     // Start zoomed in almost all the way
        if (accuracy > 80)    // Zoom out for less accurate positions
            zoomlevel -= Math.round(Math.log(accuracy/50)/Math.LN2);
        url += "&zoom=" + zoomlevel;  // Add zoom level to the URL

        // Now display the map in the image object. Thanks, Google!
        image.src = url;
    }
}
</script>
</html>

getCurrentPosition()과 watchPosition()은 첫 번째 콜백 인자뿐만 아니라, geolocation 요청이 실패했을 때 호출될 두 번째 콜백 인자를 선택적으로 사용할 수 있다.

이 두 메서드는 성공 콜백과 오류 콜백 뿐만 아니라 세 번째 선택적 인자로 옵션 객체도 사용할 수 있다. 이 객체의 프로퍼티로 높은 수준의 위치 정확도가 요구되는지, 어떤 위치 '상태'를 허용할지, 시스템이 얼마나 오랜 시간 동안 위치 추적을 허용할지를 지정할 수 있다.

성공 콜백이 전달되는 객체는 타임 스탬프도 포함하며 몇몇 기기에서는 고도나 속도, 북족 기준 각과 같은 추가적인 정보를 포함하기도 한다.

ex) 모든 geolocation 기능 실험하기

// Determine my location asynchronously and display it in the specified element.
function whereami(elt) {
    // Pass this object as the 3rd argument to getCurrentPosition()
    var options = {
        // Set to true to get a higher accuracy reading (from GPS, for example)
        // if available. Note, however that this can affect battery life.
        enableHighAccuracy: false, // Approximate is okay: this is the default

        // Set this property if a cached location is good enough.
        // The default is 0, which forces location to be checked anew.
        maximumAge: 300000,        // A fix from the last 5 minutes is okay

        // How long are you willing to wait to get the location?
        // The default is Infinity and getCurrentPosition() never times out
        timeout: 15000             // Don't take more than 15 seconds
    };

    if (navigator.geolocation) // Request position, if supported
        navigator.geolocation.getCurrentPosition(success, error, options); 
    else 
        elt.innerHTMl = "Geolocation not supported in this browser";

    // This function will be invoked if geolocation fails
    function error(e) {
        // The error object has a numeric code and a text message. Code values:
        // 1: the user did not give permission to share his or her location
        // 2: the browser was unable to determine the position
        // 3: a timeout occurred
        elt.innerHTML = "Geolocation error " + e.code + ": " + e.message;
    }

    // This function will be invoked if geolocation succeeds
    function success(pos) {
        // These are the fields that we always get. Note that the timestamp
        // is in the outer object, not the inner, coords object.
        var msg = "At " +
            new Date(pos.timestamp).toLocaleString() + " you were within " + 
            pos.coords.accuracy + " meters of latitude " +
            pos.coords.latitude + " longitude " + 
            pos.coords.longitude + ".";

        // If our device returns altitude, add that information.
        if (pos.coords.altitude) {
            msg += " You are " + pos.coords.altitude + " ± " +
                pos.coords.altitudeAccuracy + "meters above sea level.";
        }
        
        // if our device returns speed and heading, add that, too.
        if (pos.coords.speed) {
            msg += " You are travelling at " + 
                pos.coords.speed + "m/s on heading " +
                pos.coords.heading + ".";
        }

        elt.innerHTML = msg;  // Display all the position information
    }
}

히스토리 관리

단순한 히스토리 관리 기술에는 location.hashhashchange 이벤트가 필요하다. location.hash 프로퍼티를 설정하여 주소 표시줄에 출력되는 URL을 업데이트하고 브라우저의 히스토리에 새로운 항목을 추가할 수 있다. hash 프로퍼티는 URL의 부분 식별자를 설정하며, 전통적으로 문서의 특정 위치에 ID를 지정하여 해당 위치로 스크롤하는데 사용되었다. 그러나 location.hash에는 요소 ID가 존재하지 않는다.

location.hash 프로퍼티를 설정함으로써, 사용자는 뒤로 가기와 앞으로 가기 버튼을 사용해 문서의 상태를 이동할 수 있다. 이 기능이 제대로 동작하기 위해서는 애플리케이션이 상태의 변화를 감지하여 부분 식별자에 저장된 상태를 읽거나 수정할 수 있어야 한다. HTML5에서는 부분 식별자가 변경될 때마다 브라우저가 Window의 hashchange 이벤트를 발생시킨다. 부분 식별자가 변경될 때마다 호출되는 이벤트 핸들러 함수를 window.onhashchange 프로퍼티에 설정하여 히스토리 이동의 결과를 처리할 수 있다. 이 핸들러가 호출되면 해당 함수는 location.hash의 값을 해석하고 해당 값에 포함된 상태 정보를 이용하여 애플리케이션을 다시 출력할 것이다.

HTML5에는 다소 복잡하지만 강력한 히스토리 관리 메서드인 history.pushState()popstate 이벤트도 존재한다. 웹 애플리케이션이 새로운 상태로 진입하면 history.pushState() 메서드를 호출하여 브라우징 히스토리에 상태를 추가한다. 첫번째 인자는 문서의 현재 상태를 복원하는 데 필요한 정보를 포함한 객체다. 어떠한 객체라도 JSON.stringify()를 활용해서 문자열로 변환 할 수 있으며, 다른 종류의 원시 타입 객체 또한 문자열로 변환할 수 있다. 두번째 인자는 부가적으로 브라우저가 브라우징 히스토리에 저장된 상태를 구분하는 데 사용할 수 있는 일반 텍스트 문자열 형태의 제목이다. 세번째 인자는 부가적으로 현재 상태의 위치를 출력하는 URL이다. 상대 URL은 문서의 현재 위치에 대응되는 절대 위치로 변환되며, 일반적으로 #state와 같이 간단하게 URL의 해시 또는 부분 식별자를 지정한다. 각 상태를 조합한 URL은 해당 애플리케이션의 내부 상태를 사용자가 즐겨찾기에 추가할 수 있게 하고, URL에 충분한 정보가 포함되어 있다면 즐겨찾기를 통해 애플리케이션이 해당 상태를 복원할 수도 있다.

History 객체에는 pushState() 메서드 외에 replaceState()도 정의되어 있다. 이는 pushState()와 동일한 전달인자를 사용하지만 브라우징 히스토리에 새 상태를 추가하는 대신 현재 히스토리의 상태를 교체하는 역할을 한다.

사용자가 뒤로 가기 또는 앞으로 가기 버튼을 사용항 저장된 히스토리의 상태를 이동할 때 브라우저는 Window 객체에 popstate 이벤트를 발생시킨다. 이벤트 객체에는 특정 상태의 이름이 프로퍼티로 존재하며 pushState()에 전달된 상태의 복사본을 포함한다.

ex) pushState()를 활용한 히스토리 관리

<!DOCTYPE html>
<html><head><title>I'm thinking of a number...</title>
<script>
window.onload = newgame;        // Start a new game when we load
window.onpopstate = popState;   // Handle history events
var state, ui;                  // Globals initialized in newgame()

function newgame(playagain) {   // Begin a new game of guess-the-number
    // Set up an object to hold document elements we care about
    ui = {
        heading: null, // The <h1> at the top of the document.
        prompt: null,  // Ask the user to enter a guess.
        input: null,   // Where the user enters the guess.
        low: null,     // Three table cells for the visual representation
        mid: null,     // ...of the range of numbers to guess.
        high: null
    };
    // Look up each of these element ids
    for(var id in ui) ui[id] = document.getElementById(id);

    // Define an event handler for the input field
    ui.input.onchange = handleGuess;

    // Pick a random number and initialize game state
    state = {
        n: Math.floor(99 * Math.random()) + 1,  // An integer: 0 < n < 100
        low: 0,            // The lower bound (exclusive) on guesses
        high: 100,         // The upper bound (exclusive) on guesses
        guessnum: 0,       // How many guesses have been made
        guess: undefined   // What the last guess was
    };
    
    // Modify document content to display this initial state
    display(state);  

    // This function is called as the onload event handler, and is also called
    // by the Play Again button displayed at the end of a game. The playagain
    // argument will be true in that second case. If it is true, then we save
    // the new game state. But if we were called in response to a load event,
    // we don't save the state. This is because load events will also occur
    // when we step backwards through the browser history from some other 
    // document into the existing state of a game. If we were to save a new
    // initial state, in that case we would overwrite the acutal historical 
    // state of the game. In browsers that support pushState(), the load event
    // is always followed by a popstate event. So rather than saving state here,
    // we wait for the popstate. If it gives us a state object, we just use 
    // that. Otherwise, if the popstate has a null state, we know this is
    // really a new game and we use replaceState to save the new game state.
    if (playagain === true) save(state);
}

// Save game state into browser history with pushState(), if it is supported
function save(state) {  
    if (!history.pushState) return; // Do nothing if pushState() not defined

    // We'll associate a URL with the saved state. This URL displays the 
    // guess number, but does not encode the game state, so it is not useful
    // to bookmark. We can't easily put game state in the URL because it would 
    // make the secret number visible in the location bar. 
    var url = "#guess" + state.guessnum;
    // Now save the state object and the URL
    history.pushState(state,  // State object to save
                      "",     // State title: current browsers ignore this
                      url);   // State URL: not useful to bookmark
}

// This is the onpopstate event handler that restores historical states.
function popState(event) {
    if (event.state) {  // If the event has a state object, restore that state
        // Note that event.state is a deep copy of the saved state object
        // so we can modify it without altering the saved value.
        state = event.state;    // Restore the historical state
        display(state);         // Display the restored state
    }
    else {
        // When we load the page for the first time, we'll get a popstate event
        // with no state. Replace that null state with our real state: see the
        // comment in newgame(). No need to call display() here.
        history.replaceState(state, "", "#guess" + state.guessnum);
    }
};

// This event handler is invoked each time the user guesses a number.
// It updates the game state, saves it, and displays it.
function handleGuess() {
    // Get the user's guess from the input field
    var g = parseInt(this.value);
    // If it is a number and is in the right range
    if ((g > state.low) && (g < state.high)) { 
        // Update the state object based on this guess
        if (g < state.n) state.low = g;          
        else if (g > state.n) state.high = g;
        state.guess = g;
        state.guessnum++;
        // Now save the new state in the browser's history
        save(state);
        // Modify the document to respond to the user's guess
        display(state);
    }
    else {  // An invalid guess: don't push a new history state
        alert("Please enter a number greater than " + state.low +
              " and less than " + state.high);
    }
}

// Modify the document to display the current state of the game.
function display(state) { 
    // Display document heading and title
    ui.heading.innerHTML = document.title =
        "I'm thinking of a number between " +
        state.low + " and " + state.high + ".";

    // Display a visual representation of the range of numbers using a table
    ui.low.style.width = state.low + "%";
    ui.mid.style.width = (state.high-state.low) + "%";
    ui.high.style.width = (100-state.high) + "%";

    // Make sure the input field is visible, empty, and focused
    ui.input.style.visibility = "visible"; 
    ui.input.value = "";
    ui.input.focus();

    // Set the prompt based on the user's most recent guess
    if (state.guess === undefined)
        ui.prompt.innerHTML = "Type your guess and hit Enter: ";
    else if (state.guess < state.n)
        ui.prompt.innerHTML = state.guess + " is too low. Guess again: ";
    else if (state.guess > state.n)
        ui.prompt.innerHTML = state.guess + " is too high. Guess again: ";
    else {
        // When correct, hide the input field and show a Play Again button.
        ui.input.style.visibility = "hidden";  // No more guesses now
        ui.heading.innerHTML = document.title = state.guess + " is correct! ";
        ui.prompt.innerHTML =
            "You Win! <button onclick='newgame(true)'>Play Again</button>";
    }
}
</script>
<style>  /* CSS styles to make the game look good */
#prompt { font-size: 16pt; }
table { width: 90%; margin:10px; margin-left:5%; }
#low, #high { background-color: lightgray; height: 1em; }
#mid { background-color: green; }
</style>
</head>
<body><!-- The HTML elements below are the game UI -->
<!-- Game title and textual representation of the range of numbers -->
<h1 id="heading">I'm thinking of a number...</h1>
<!-- a visual representation of the numbers that haven't been ruled out -->
<table><tr><td id="low"></td><td id="mid"></td><td id="high"></td></tr></table>
<!-- Where the user enters their guess -->
<label id="prompt"></label><input id="input" type="text">
</body></html>

교차 출처 메시징

다른 출처의 스크립트 호출을 허용하는 Window 객체의 메서드인 postMessage()를 활용하면, 다른 출처의 스크립트 사이에서 제한적으로 일종의 비동기 메시지 통신이 가능하다. 이런 통신 기술은 HTML5에서 문서 메시징이라고 알려져 있지만 , 해당 API는 document 대신 Window 객체에 정의되어 있으므로, '인터 윈도 메시지 전달' 또는 '교차 출처 메시징'이라고 부르는게 더 나았을 것이다.

postMessage() 메서드는 두 개의 전달인자를 필요로 한다. 첫 번째는 전송할 메시지다. 메시지로 객체나 배열을 사용하고 싶다면 JSON.stringify()를 사용하여 해당 객체를 직렬화해야 한다.

두번째 인자는 메시지를 보낼 대상이 되는 창의 출처를 지정하는 문자열이다. 이 문자열은 URL 구성 요소인 프로토콜과 호스트명, 포트를 포함한다. 만약 창에 지정한 출처와 다른 문서가 포함되어 있으면 postMessage()가 메시지를 전송하지 않도록 할 것이다. 출처와 상관없이 메시지를 보내려면, 출처 문자열 대신 와일드카드 '*'를 전달하면 된다. 현재 창의 출처와 동일하게 지정하려면, 간단히 '/'을 사용하면 된다.

출처가 일치하면 postMessage()를 호출하고, 그 결과로 해당 메시지를 받은대상 Window 객체에서는 message 이벤트가 발생한다. message 이베느를 통보받기 위해 해당 창의 스크립트에 이벤트 핸들러를 정의할 수있다. 이 핸들러에는 다음의 프로퍼티가 포함된 이벤트 객체가 전달된다.

 

data

postMessage()의 첫번째 인자로 전달된 메시지의 복사본이다.

source

메시지를 보낸 Window 객체다.

origin

메시지를 보낸 곳에서 출처를 나타내기 위해 지정한 URL 문자열이다.

 

onmessage() 핸들러는 대부분 이벤트 객체의 origin 프로퍼티로부터 먼저 확인하여 뜻밖의 도메인으로부터온 메시지를 무시해야 한다.

postMessage()를 통한 교차 출처 간 메시지 교환과 message 이벤트는 웹 페이지에 다른 사이트의 모듈이나 가젯을 포함시키려 할 때 유용할 것이다.(가젯 : 특별한 이름이 붙어 있지 않은 작은 기계장치나 도구, 부속품) 가젯이 간단하며 스스로 동작 가능하다면, <iframe> 요소에 넣어서 손쉽게 격리시킬 수 있다. 가젯이 <script> 요소로 정의된다면 일반적인 자바스크립트 API로 사용할 수 있겠지만, 동시에 가젯이 해당 페이지와 내용을 완전히 제어할 수 있게 되기도 한다.

아래 예제는 이 가젯으로 무언가를 검색하려면, 해당 페이지에서 간단히 검색어를 메시지로 전송하기만 하면 된다.

ex) postMeesage()에 의해 제어되는 트위터 검색 가젯

<!DOCTYPE html>
<!--
  This is a Twitter search gadget. Include it in any webpage, inside an
  iframe, and ask it to search for things by sending it a query string with
  postMessage(). Since it is in an <iframe> and not a <script>, it can't
  mess around with the containing document.
-->
<html>
<head>
<style>body { font: 9pt sans-serif; }</style>
<!-- Use jQuery for its jQuery.getJSON() utility -->
<script src="http://code.jquery.com/jquery-1.4.4.min.js"/></script>
<script>
// We ought to just be able to use window.onmessage, but some older browsers
// (e.g., Firefox 3) don't support it, so we do it this way instead.
if (window.addEventListener)
    window.addEventListener("message", handleMessage, false);
else 
    window.attachEvent("onmessage", handleMessage);   // For IE8

function handleMessage(e) {
    // We don't care what the origin of this message is: we're willing
    // to search Twitter for anyone who asks us. We do expect the message
    // to come from the window that contains us, however.
    if (e.source !== window.parent) return;

    var searchterm = e.data;  // This is what we were asked to search for

    // Use jQuery Ajax utlities and the Twitter search API to find
    // tweets matching the message.
    jQuery.getJSON("http://search.twitter.com/search.json?callback=?",
                   { q: searchterm },
                   function(data) {   // Called with request results
                       var tweets = data.results;
                       // Build an HTML document to display these results
                       var escaped = searchterm.replace("<", "<");
                       var html = "<h2>" + escaped + "</h2>";
                       if (tweets.length == 0) {
                           html += "No tweets found";
                       }
                       else {
                           html += "<dl>"; // <dl> list of results
                           for(var i = 0; i < tweets.length; i++) {
                               var tweet = tweets[i];
                               var text = tweet.text;
                               var from = tweet.from_user;
                               var tweeturl = "http://twitter.com/#!/" +
                                   from + "/status/" + tweet.id_str;
                               html += "<dt><a target='_blank' href='" +
                                   tweeturl + "'>" + tweet.from_user +
                                   "</a></dt><dd>" + tweet.text + "</dd>";
                           }
                           html += "</dl>";
                       }
                       // Set the <iframe> document
                       document.body.innerHTML = html;
                   });
}

$(function() {
    // Let our container know we're here and ready to search.
    // The container can't send any messages to us before it gets this message
    // from us because we won't be here to receive the message yet.
    // Normally, containers can just wait for an onload event to know that all
    // of their <iframe>s have loaded. We send this message for containers that
    // want to start searching Twitter even before they get their onload event.
    // We don't know the origin of our container, so use * so that the browser
    // will deliver it to anyone.
    window.parent.postMessage("Twitter Search v0.1", "*");
});
</script>
</head>
<body>
</body>
</html>

문서에 가젯을 삽입하며, 문서 내 모든 링크에 이벤트 핸들러를 추가하므로 특정 링크에 마우스를 올리면 해당 URL을 검색하기 위해 postMessage()를 호출한다. 이 기능은 사용자가 사이트를 방문하기 전에 해당 사이트에 대한 사람들의 트윗을 볼 수 있게 한다.

ex) postMessage()를 활용한 트위터 검색 가젯 사용하기

// This file of JS code inserts the Twitter Search Gadget into the document
// and adds an event handler to all links in the document so that when the
// use moves the mouse over them, the gadget searches for the link's URL.
// This allows the user to see what people are tweeting about the link
// destination before clicking on it.
window.addEventListener("load", function() {       // Won't work in IE < 9
    var origin = "http://davidflanagan.com";       // Gadget origin
    var gadget = "/demos/TwitterSearch.html";      // Gadget path
    var iframe = document.createElement("iframe"); // Create the iframe
    iframe.src = origin + gadget;                  // Set its URL
    iframe.width = "250";                          // 250 pixels wide
    iframe.height = "100%";                        // Full document height
    iframe.style.cssFloat = "right";               // Flush right

    // Insert the iframe at the start of the document
    document.body.insertBefore(iframe, document.body.firstChild);
    
    // Now find all links and hook them up to the gadget
    var links = document.getElementsByTagName("a");
    for(var i = 0; i < links.length; i++) {
        // addEventListener doesn't work in IE8 and before
        links[i].addEventListener("mouseover", function() {
            // Send the url as the search term, and only deliver it if the
            // iframe is still displaying a document from davidflanagan.com
            iframe.contentWindow.postMessage(this.href, origin);
        }, false);
    }
}, false);

 

웹 워커

웹 워커 규격에서는 매우 조심스럽게 클라이언트 측 자바스크립트의 단일 스레드를 느슨하게 만들었다. 워커는 효과적으로 병렬 스레드를 실행하도록 정의되어 있다. 웹 워커는 자체적으로 생성한 실행 환경에 존재한다. 그러나 Window 또는 Document 객체에 접근하지는 않고, 오직 비동기 메시지 전달을 통해서만 메인 스레드와 통신할 수 있다.

모든 스레드 API와 마찬가지로 웹 워커 명세도 두 부분으로 나뉜다. 첫번째는 Worker 객체로써, 생성된 스레드를 외부에서 바라보는 모습이다. 두번째는 WorkerGlobalScope다. WorkerGlobalScope는 새 워커를 위한 전역 객체이며 워커 스레드 자체를 내부에서 보는 모습과도 같다.

Worker 객체

새 워커를 만들려면 Worker 생성자에 워커가 실행되는 자바스크립트 코드 URL을 지정하면 된다.

var loader=new Worker("utils/loader.js");

상대 URL을 지정하면 해당 URL은 Worker() 생성자를 호출하는 스크립트를 포함한 문서를 기준으로 해석된다. 절대 URL을 지정할 때는 해당 URL이 문서의 출처와 반드시 일치해야 한다.

Worker 객체를 한번 생성하면 postMessage()를 활용하여 데이터를 전송할 수 있다. postMessage()에 전달하는 값은 복제되며 해당 복사본은 message 이벤트를 통해 워커에 전달될 것이다.

loader.postMessage("file.txt");

워커의 postMessage() 메서드에는 Window 객체의 postMessage() 메서드와 같은 출처에 대한 전달인자가 존재하지 않는다.

worker.onmessage=function(e){

  var message=e.data;

  console.log("URL contents: "message);

}

워커가 예외를 발생시켰으나 내부적으로 처리되지는 않았다면 , 해당 예외는 구독 가능한 다른 이벤트로 전파된다.

worker.oneror=function(e){

  console.log(e.filename+"파일의: "+e.lineno+": 에서 "+e.message+" 오류가 발생하였습니다.");

}

addEventListener를 사용하면 다중 이벤트 핸들러를 관리할 수 있다.

이 밖에 다른 Worker 객체의 메서드는 terminate() 하나뿐이다. 이는 워커 스레드를 강제 종료한다.

Worker의 범위

Worker() 생성자를 사용하여 새 워커를 생성할 때는 해당 자바스크립트 코드를 포함하는 파일의 URL을 지정해야 한다. 해당 워커 코드는 워커를 새엉한 스크립트로부터 완전히 독립적인 새로운 순수 자바스크립트 실행 환경에서 구동된다. 이 새로운 실행 환경의 전역 객체는 WorkerGlobalScope 객체다. WorkerGlobalScope는 순수 자바스크립트 전역 객체에는 없는 프로퍼티들을 포함하고 있지만 클라이언트 측 Window 객체 전역의 크기보다는 작다.

WorkerGlobalScope 객체에는 postMessage() 메서드와 onmessage 이벤트 핸들러 프로퍼티가 존재한다.

close() 함수는 워커가 스스로 종료되게 하며, Worker 객체의 terminate() 메서드와 비슷한 역할을 한다. 워커에 자체적으로 close()를 호출하는 코드가 있을 때에는, 대체로 종료되기 전에 일종의 'closing' 메시지를 전송하는 것이 좋은 방법이다.

WorkerGlobalScope에서 정의한 함수 중 가장 흥미로운 것은 importScripts()이다. 워커에서는 필요한 라이브러리 코드를 불러들이는데 이 함수를 활용한다.

importScripts("collections/Set.js", "collections/Map.js", "utils/base64.js");

한 파일이 로드된 다음 실행되면, 지정된 순서에 따라 다음 파일을 불러들인다.

importScripts() 함수는 동기 방식으로 동작하므로 지정된 모든 스크립트가 로드되고 실행되기 전까지는 반환되지 않는다. importScripts() 함수가 반환되면 로드된 스크립트를 즉시 사용할 수 있다. 

WorkerGlobalScope는 워커의 전역 객체이므로, 자바스크립트의 모든 주요 전역 객체도 존재한다.

 

WorkerGlobalScope 객체에는 클라이언트 측의 중 자바스크립트 생성자 객체들이 존재한다. 따라서 HTTP 스크립팅이 가능하고, Worker() 생성자도 사용할 수 있으므로 해당 워커 고유의 또다른 워커 스레드를 생성할 수도 있다.

웹 워커 예제

ex) 이미지 처리를 위한 웹 워커 만들기 SmearImage.js

// Asynchronously replace the contents of the image with a smeared version.
// Use it like this: <img src="testimage.jpg" onclick="smear(this)"/>
function smear(img) {
    // Create an offscreen <canvas> the same size as the image
    var canvas = document.createElement("canvas");
    canvas.width = img.width; 
    canvas.height = img.height;

    // Copy the image into the canvas, then extract its pixels
    var context = canvas.getContext("2d"); 
    context.drawImage(img, 0, 0);          
    var pixels = context.getImageData(0,0,img.width,img.height)

    // Send the pixels to a worker thread
    var worker = new Worker("SmearWorker.js");      // Create worker
    worker.postMessage(pixels);                     // Copy and send pixels

    // Register a handler to get the worker's response
    worker.onmessage = function(e) {
        var smeared_pixels = e.data;                // Pixels from worker
        context.putImageData(smeared_pixels, 0, 0); // Copy them to the canvas
        img.src = canvas.toDataURL();               // And then to the img
        worker.terminate();                         // Stop the worker thread
        canvas.width = canvas.height = 0;           // Don't keep pixels around
    }
}

ex) 웹 워커에서의 이미지 처리 SmearWorker.js

// Get an ImageData object from the main thread, process it, send it back
onmessage = function(e) { postMessage(smear(e.data)); }

// Smear the ImageData pixels to the right, producing a motion blur.
// For large images, this function does a lot of computation and would
// cause UI responsiveness issues if it was used on the main thread.
function smear(pixels) {
    var data = pixels.data, width = pixels.width, height = pixels.height;
    var n = 10, m = n-1;  // Make n bigger for more smearing
    for(var row = 0; row < height; row++) {            // For each row
        var i = row*width*4 + 4;                       // 2nd pixel offset
        for(var col = 1; col < width; col++, i += 4) { // For each column
            data[i] =   (data[i] + data[i-4]*m)/n;     // Red pixel component
            data[i+1] = (data[i+1] + data[i-3]*m)/n;   // Green
            data[i+2] = (data[i+2] + data[i-2]*m)/n;   // Blue
            data[i+3] = (data[i+3] + data[i-1]*m)/n;   // Alpha component
        }
    }
    return pixels;
}

ex) 웹 워커에서 동기 방식의 XMLHttpRequest 생성하기 AjaxWorker.js

// This file will be loaded with new Worker(), so it runs as an independent
// thread and can safely use the synchronous XMLHttpRequest API.
// Messages are expected to be arrays of URLs. Synchronously fetch the
// contents of each URL as a string and send back an array of those strings.
onmessage = function(e) {
    var urls = e.data;   // Our input: the URLs to fetch
    var contents = [];   // Our output: the contents of those URLs

    for(var i = 0; i < urls.length; i++) { 
        var url = urls[i];                 // For each URL
        var xhr = new XMLHttpRequest();    // Begin an HTTP request 
        xhr.open("GET", url, false);       // false makes this synchronous
        xhr.send();                        // Blocks until response is complete
        if (xhr.status !== 200)            // Throw an error if request failed
            throw Error(xhr.status + " " + xhr.statusText + ": " + url);
        contents.push(xhr.responseText);   // Otherwise, store the URL contents
    }

    // Finally, send the array of URL contents back to the main thread
    postMessage(contents);
}

타입 배열과 배열 버퍼

타입 배열은 정규 배열과는 몇 가지 중요한 차이가 있는 유사 배열 객체다.

타입 배열의 원소는 모두 숫자다. 생성장 숫자의 타입과 크기를 사용하여 타입 배열을 생성한다.

타입 배열은 길이가 고정된다.

배열이 생성된 시점에 타입 배열의 원소들은 항상 0으로 초기화된다.

타입 배열의 종류는 총 여덟 가지이며 원소의 타입은 각기 다르다. 해당 배열은 다음의 생성자를 사용하여 생성할 수 있다.

생성자 원소 타입
Int8Array() 부호 있는 바이트
Uint8Array() 부호 없는 바이트
Int16Array() 부호 있는 작은 16비트 정수
Uint16Array() 부호 없는 작은 16비트 정수
Int32Array() 부호 있는 작은 32비트 정수
Uint32Array() 부호 없는 작은 32비트 정수
Float32Array() 32비트 부동 소수점 값
Float64Array() 64비트 부동 소수점 값(정규 자바스크립트 숫자형)

n보다 더 작은 수 중 가장 큰 소수 찾기

function sieve(n){
    var a=new Int8Array(n+1);
    var max=Math.floor(Math.sqrt(n));
    var p=2;
    while(p<=max){
        for(var i=2*p; i<=n; i+=p)
            a[i]=1;
        while(a[++p]){
            /*empty*/
        }
    }
    while(a[n]) n--;
    return n;
}

set() 메서드는 일반 또는 타입 배열의 원소를 타입 배열 내부로 복사한다.

var bytes=new Unit8Array(1024);
var pattern=new Unit8Array([0,1,2,3]);
bytes.set(pattern);
bytes.set(pattern,4);
bytes.set([0,1,2,3],8);

타입 배열에는 지정한 위치에 해다오디는 배열의 일부분을 반환하는 subarray 메서드도 존재한다.

var ints=new Int16Array([1,2,3,4,5,6,7,8,9]);
var last3=ints.subarray(ints.length-3,ints.length);  //그 중 마지막 3개
last3[0]  //7

ints[9]=-1;
last3[2]  //-1: 서브배열의 값 변경된다.

배열의 모든 뷰는 ArrayBuffer로 알려진 바이트 데이터들을 기반으로 한다.

모든 타입 배열에는 기반이 되는 버퍼와 관련 있는 세 개의 프로퍼티가 존재한다.

last3.bueer  //ArrayBuffer 객체 반환
last3.buffer==ints.buffer  //true
last3.byteOffset  //14 이 뷰는 버퍼의 14바이트부터 시작한다.
last3.byteLength  //6: 이 뷰의 길이는 6바이트(3개의 16비트 정수)이다.

ArrayBuffer 객체에는 길이를 반환하는 단 하나의 프로퍼티만이 존재한다.

last3.byteLength  //6: 이 뷰의 길이는 6바이트이다.
last3.buffer.byteLength  //20: 그러나 이 뷰의 기반인 버퍼의 길이는 29바이트이다.

ArrayBuffer는 단지 불투명한 바이트 더미일 뿐이다.

var bytes=new Unit8Array(8);
bytes[0]=1;
bytes.buffer[0] //undefined
bytes.buffer[1]=255;
bytes[1] //0

ArrayBuffer 생성자를 활용하면 직접적으로 ArrayBuffer를 생성할 수 있으며, ArrayBuffer 객체를 활용하면 해당 버퍼의 타입 배열 뷰를 무한정 생성할 수 있다.

var buf=new ArrayBuffer(1024*1024);
var asbytes=new Uint8Array(buf);
var asints=new Int32Array(buf);
var lastK=new Uint8Array(buf,1023*1024);  //바이트로써의 마지막 킬로바이트
var ints2=new Int32Array(buf,1024,256);  //256 정수로써의 두번째 킬로바이트

일반적으로 외부 데이터를 활용할 때는 데이터의 각 바이트를 배열로 다루기 위해 Int8Array와 Uint8Array를 사용할 수 있지만, 다른 타입 배열을 사용해서 다중 바이트의 데이터를 다루어서는 안된다. 대신, 바이트 배열 방식을 명시적으로 지정하여 ArrayBuffer의 값을 읽고 쓰는 메서드를 정의하고 있는 DataView 클래스를 사용하면 된다.

var data;  //네트워크로부터 받은 ArrayBuffer라고 가정한다.
var view=DataView(data);  //이 데이터의 뷰를 생성한다.
var int=view.getInt32(0);  //0번째 바이트로부터 부호가 있는
                           //32비트 빅 엔디언 정수를 가져온다.
int=view.getInt32(4,false);  //다음 32비트 정수도 빅 엔디언으로 가져온다.
int=view.getInt32(8,false);  //다음 4바이트는 리틀 엔디언으로 가져온다.
view.setInt32(8,int,false);  //이 데이터를 다시 빅 엔디언 형식으로 기록한다.

DataView는 여덟 가지의 타입 배열 형식을 위한 각각의 get 메서드를 정의한다. 이 메서드의 이름은 getInt16()이나 getUint32(), getFloat64() 같은 형태다.

첫번째 인자는 ArrayBuffer의 몇번째 값으로부터 시작할지를 지정하는 바이트 오프셋이다. getInt8()과 getUint8()을 제외한 모든 게터 메서드들은 선태적으로 두번째 인자를 불리언 값으로 받아들인다. 두번째 인자가 생략되거나 false이면 빅 엔디언 바이트 배열이 사용된다. 두번째 인자가 true이면 리틀 엔디언 배열이 사용된다.

DataView는 8가지의 set 메서드 또한 정의한다. 첫번째 인자는 기록을 시작할 값의 위치 오프셋이다. 두번째 인자는 기록할 값이다. setInt8()과 setUint8()을 제외한 메서드들은 세번째 인자를 선택적으로 받아들인다. 이 인자가 생략되거나 false이면 높은 번지의 바이트를 먼저 배열하는 빅 엔디언 형식으로 값을 기록한다. true이면 낮은 번지의 바이트를 먼저 배열하는 리틀 엔디언 형식으로 값을 기록한다.

Blob

Blob은 일련의 데이터를 처리하거나 간접 참조하는 객체다. 대형 이진 객체(Binary Large Object)를 의미한다. 자바스크립트에서 Blob은 흔히 이진 데이터를 나타내며 해당 데이터의 크기가 매우 클 수 있지만, 두 가지 특징 모두 강제된 사항은 아니다. 즉, 작은 텍스트 파일의 내용도 Blob으로 나타낼 수 있다. Blob은 대개 바이트의 크기를 알아내거나, 해당 MIME 타입이 무엇인지 요청하며, 데이터를 작은 Blob으로 잘게 나누는 등의 작업에 사용된다. 즉, 데이터 자체라기보다는 데이터를 간접적으로 접근하기 위한 객체인 것이다.

var blob= ... //Blob을 획득하는 방법은 나중에
blob.size  //Blob의 바이트 단위 크기
blobtype  //Blob의 MIME 타입을 저장하거나, 알 수 없으면 ""을 저장
var subblob=blob.slice(0,1024,"text/plain");  //Blob의 첫 1킬로바이트를 텍스트로 가져온다.
var last=blob.slice(blob.size-1024,1024);  //Blob의 마지막 1킬로바이트를 타입없이 가져온다.

 

웹 브라우저는 메모리 또는 디스크에 Blob을 저장할 수 있으며, Blob은 매우 크기 때문에 비동기 방식으로 동작한다.

Blob 그 자체는 그리 흥미롭지 않지만, 이진 데이터를 활용하는 다양한 자바스크립트 API에서는 데이터 교환 메커니즘의 중요한 매개체로써 제공된다. 

· Blob은 구조체 복제 알고리즘의 지원을 받는다. 즉, 다른 창이나 스레드로부터 message 이벤트를 통해 Blob을 획득할 수 있다.

· 클라이언트 측 데이터베이스로부터 Blob을 가져올 수도 있다.

· 웹에서 Blob을 다운로드할 수 있다.

· BlobBuilder 객체를 사용하면 문자열이나 ArrayBuffer 객체 또는 기타 다른 Blob 외에 사용자 Blob을 만들 수도 있다.

Blob 객체로 할 수 있는 일은 매우 다양한데, 그 중 다수가 앞의 내용과 대칭된다.

· postMessage()를 활용하여 Blob을 다른 창이나 워커 스레드에 전송할 수 있다.

· Blob을 클라이언트 측 데이터베이스에 저장할 수 있다.

· Blob을 XMLHttpRequest 객체의 send() 메서드로 전달하면 서버에 해당 데이터를 업로드할 수 있다. 

· createObjectURL() 함수를 사용하면, 특수 URL blob://이 가리키는 Blob의 내용을 가져올 수 있으며, 이 URL은 DOM이나 CSS를 활용할 수도 있다.

· FileReader 객체를 사용하면 Blobㅇ의 내용을 비동기적으로 문자열이나 ArrayBuffer로 추출할 수 있다.

· Filesystem API와 FileWriter 객체를 사용하여 Blob을 로컬 파일로 기록하는 방법에 대해 설명한다.

Blob으로써의 파일

최근에는 사용자가 선택한 파일을 클라이언트 측에서 접근할 수 있도록 이 요소를 확장하였다. 클라이언트 측 스크립트가 선택된 파일의 내용을 읽을 수 있다는 말은 서버로 업로드할 파일들이 더이상 보안되지 않거나 최소한의 보안만을 보장한다는 의미다. 

로컬 파일 접근을 허용하는 브라우저에서 <input tyep="file"> 요소의 files 프로퍼티는 FileList 객체일 것이다. 이 유사 배열 객체는 사용자가 선택한 0개 이상의 File 객체 목록이다. File 객체는 name과 lastModifiedDater 프로퍼티가 존재하는 Blob 객체다.

<script>
  //선택된 파이르이 목록에 관한 로그 정보
  function fileinfo(files){
    for(var i=0;i<files.length;i++){
      var f=files[i];
      console.log(f.name,  // 경로를 제외한 파일명
                  f.size, f.type,  // size
                  f.lastModifiedDate);  //lastModifiedDate는 File 프로퍼티다.
    }
  }
</script>
<!--여러개의 이미지 파일을 선택할 수 있고, 선택 목록을 fileinfo()로 전달한다. -->
<input type="file" accept="image/*" multiple onchange="fileinfo(this.files)"/>

<input> 요소를 사요야한 파일 선택 뿐만 아니라 사용자가 브라우저에 드래그-앤-드롭한 로컬 파일을 스크립트를 통해 접근하는 방법도 있다.

Blob 다운로드

// GET the contents of the url as a Blob and pass it to the specified callback.
// This code is untested: no browsers supported this API when it was written.
function getBlob(url, callback) {
    var xhr = new XMLHttpRequest();  // Create new XHR object
    xhr.open("GET", url);            // Specify URL to fetch
    xhr.responseType = "blob"        // We'd like a Blob, please
    xhr.onload = function() {        // onload is easier than onreadystatechange
        callback(xhr.response);      // Pass the blob to our callback
    }                                // Note .response, not .responseText
    xhr.send(null);                  // Send the request now
}

Blob 만들기

Blob은 흔히 로컬 파일이나 URL, 데이터베이스와 같이 외부 출처로부터 가져온 데이터들을 나타낸다. 그러나 웹에 업로드하거나 파일 혹은 데이터베이스에 저장하거나 다른 스레드로 전달하기 위해서 웹 애플리케이션 고유의 Blob을 생성해야 할 수도이 있다. 데이터에서 Blob을 생성하려면 BlobBuilder를 사용하면 된다.

//새 BlobBulder를 생성한다.
var bb=new BlobBuilder();

//blob에 문자열을 덧붙이고 NUL 문자를 활용하여 문자열의 끝을 표시한다.
bb.append("이 blob은 이 텍스트와 부호가 있는 32비트 빅 엔디언 정수 10개를 포함한다.");
bb.append("\0");  // 문자열의 끝부분을 표시하는 NUL 문자로 종료한다.

//ArrayBuffer에 몇 가지 데이터를 저장한다.
var ab=new ArrayBuffer(4*10)
var dv=new DataView(ab);

for(var i=0;i<10;i++) dv.setInt32(i*4,i);

//Blob에 이 ArrayBuffer를 덧붙인다.
bb.append(ab);

//이제 빌더로부터 blob을 가져온 다음, MIME 타입을 지정한다.
var blob=bb.getBlob("x-optional/mine-type-here");

Blob URL

이제 가져오거나 생성한 Blob을 활용하여 실제로 무엇을 할 수 있는지 이야기해 보자.가장 간단한 형태 중 하나로 Blob을 가리키는 URL을 생성할 수 있다. 이러한 URL을 생성해 두면, DOM이나 스타일시트 또는 XMLHttpRequest의 목적 URL과 같이 일반 URL을 사용하는 곳 어디라도 Blob URL을 사용할 수 있다.

Blob URL은 createObjectURL() 함수를 활용하여 생성한다. Blob URL을 브라우저와 관계없이 편리하게 사용하기 위해서는 다음과 같이 정의하자

var getBlobURL=(window.URL && URL.createObjectURL.bind(URL)) ||
    (window.webkitURL && webkitURL.createObjectURL.bind(webkitURL)) ||
    window.createObjectURL;

웹 워커에서도 이 API를 사용할 수 있으며, 마찬가지로 동일한 함수를 URL 또는 webkitURL 객체를 통해 접근할 수도 있다.

createObjectURL()에 blob을 전달하면 URL을 일반적인 문자열 형태로 반환한다. URL은 blob://으로 시작하고 짧은 문자열이 뒤따른다. 해당 문자열은 Blob을 식별하기 위한 일종의 유일 식별자다. 해당 Blob 자체의 내용을 인코딩하는 data:// URL과는 매우 많은 차이가 있다. Blob URL은 간단히 브라우저가 메모리나 디스크에 저장한 Blob을 가리킨다. 또한 blob:// URL은 로컬 파일 시스템의 파일을 직접적으로 가리키고 파일의 경로를 노출하며 디렉터리 브라우징을 허용하므로, 그 밖의 보안적인 이슈들에 대해서는 file:// URL과 많은 차이가 있다.

ex) Blob URL을 사용하여 드롭된 이미지 파일 보여주기

<!DOCTYPE html>
<html><head>
<script>// At the time of this writing, Firefox and Webkit disagree on the
// name of the createObjectURL() function
var getBlobURL = (window.URL && URL.createObjectURL.bind(URL)) ||
    (window.webkitURL && webkitURL.createObjectURL.bind(webkitURL)) ||
    window.createObjectURL;
var revokeBlobURL = (window.URL && URL.revokeObjectURL.bind(URL)) ||
    (window.webkitURL && webkitURL.revokeObjectURL.bind(webkitURL)) ||
    window.revokeObjectURL;

// When the document is loaded, add event handlers to the droptarget element
// so that it can handle drops of files
window.onload = function() {
    // Find the element we want to add handlers to.
    var droptarget = document.getElementById("droptarget");

    // When the user starts dragging files over the droptarget, highlight it.
    droptarget.ondragenter = function(e) {
        // If the drag is something other than files, ignore it.
        // The HTML5 dropzone attribute will simplify this when implemented.
        var types = e.dataTransfer.types;
        if (!types ||
            (types.contains && types.contains("Files")) ||
            (types.indexOf && types.indexOf("Files") != -1)) {
            droptarget.classList.add("active"); // Highlight droptarget
            return false;                       // We're interested in the drag
        }
    };
    // Unhighlight the drop zone if the user moves out of it
    droptarget.ondragleave = function() {
        droptarget.classList.remove("active");
    };

    // This handler just tells the browser to keep sending notifications
    droptarget.ondragover = function(e) { return false; };

    // When the user drops files on us, get their URLs and display thumbnails.
    droptarget.ondrop = function(e) {
        var files = e.dataTransfer.files;            // The dropped files
        for(var i = 0; i < files.length; i++) {      // Loop through them all
            var type = files[i].type;
            if (type.substring(0,6) !== "image/")    // Skip any nonimages
                continue;
            var img = document.createElement("img"); // Create an <img> element
            img.src = getBlobURL(files[i]);          // Use Blob URL with <img>
            img.onload = function() {                // When it loads
                this.width = 100;                    // adjust its size and
                document.body.appendChild(this);     // insert into document.
                revokeBlobURL(this.src);             // But don't leak memory!
            }
        }

        droptarget.classList.remove("active");       // Unhighlight droptarget
        return false;                                // We've handled the drop
    }
};
</script>
<style> /* Simple styles for the file drop target */
#droptarget { border: solid black 2px; width: 200px; height: 200px; }
#droptarget.active { border: solid red 4px; }
</style>
</head>
<body> <!-- The document starts off with just the file drop target -->
<div id="droptarget">Drop Image Files Here</div>
</body>
</html>

 

너무어렵다... 하지마 1년뒤에나 하자

파일 시스템 API

 

클라이언트 측 데이터베이스

 

웹 소켓

 

728x90
LIST

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

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