HTML5에서 로컬 파일을 자유롭게 읽을 수 있는 File API

HTML5에서 제공하는 기능 중 File API가 있습니다. 로컬에 저장된 파일을 읽을 때 사용되는 API입니다. ArrayBuffer와 DataView와 함께 사용하여 바이너리(Binary) 데이터를 읽을 수 있습니다. 특히나 로컬에 저장된 물리적인 파일을 File이라는 클래스를 통해 접근할 수 있는데요. 이 File은 Blob를 상속받으며, 이 Blob의 slice 함수를 사용하면 대용량의 파일 전체 내용을 메모리에 올리지 않고도 필요한 부분만을 올려 사용할 수 있다는 매우 큰 장점을 갖습니다.

이 글은 이러한 HTML5의 기능 중 하나인 File API에 대한 설명을 예제를 통해 살펴보도록 하겠습니다. 먼저 File API를 사용하기 위해서는 읽을 파일을 지정해야 하는데, 보안상의 이유로 사용자가 특정한 행위를 통해 지정된 파일만을 제한하여 사용할 수 있습니다. 여기서 말하는 사용자의 특정 행위는 2가지로 다음과 같습니다.


  • 파일 선택 대화상자를 통해 읽을 파일을 사용자가 선택

  • 사용자가 드래그 앤 드랍(Drag & Drop)으로 읽을 파일을 선택

여기서는 파일 선택 대화상자를 통해 읽을 파일을 사용자가 선택하는 방법으로 File API에 대해 살펴보도록 하겠습니다. 파일 선택 대화상자를 웹에서 사용하기 위해서는 다음의 코드에서 2번째줄의 HTML Tag가 필요합니다.

<body onload="load()">
    <input type="file" id="inputFile" accept=".dbf" />
</body>

즉, TYPE 속성이 file인 INPUT 테그를 사용하는데요, accept 속성을 이용해 선택할 파일의 확장자에 대한 필터링을 지정할 수 있습니다. 아울러 multiple 속성을 지정하면 파일을 여러개를 다중으로 선택할 수 있습니다. 실행해 보면 다음과 같은 결과를 볼 수 있는데요, 찾아보기 버튼을 클릭하여 파일 선택 대화상자까지 표시한 상태입니다.

앞서 코드에서 1번 줄을 보면 BODY에 onload 이벤트에 대해 load() 함수가 지정된 것을 볼 수 있습니다. 이 load() 함수에 대한 코드는 다음과 같습니다.

function load() {
    if(!(File && FileReader && FileList && Blob)) {
        alert("Not Supported File API");
    }

    document.getElementById("inputFile").onchange = function () {
        var file = this.files[0];
        var name = file.name;
        var size = file.size;
        var reader = new FileReader();

        reader.onload = function () {
            var aBuf = this.result; // ArrayBuffer
            var dView = new DataView(aBuf);

            var validFlag = dView.getUint8(0);
            var year = dView.getUint8(1);
            var month = dView.getUint8(2);
            var day = dView.getUint8(3);
            var numRecords = dView.getInt32(4, true);
            var numHeaders = dView.getInt16(8, true);


        };

        var blob = file.slice(0, 1000);
        reader.readAsArrayBuffer(blob);
    };
}

코드는 짧지만 상당히 많은 내용을 담고 있습니다. 하나 하나 살펴보면 다음과 같습니다.

2번 코드는 해당 웹브라우저가 File API를 지원하는지 검사합니다. 전역 객체로써 window에 대해 File API에서 제공하는 속성은 File, FileReader, FileList, Blob 정도입니다. 그리고 6번 코드는 앞서 파일 선택 대화상자를 통해 파일을 지정했을때 발생하는 이벤트인 onchange에 대한 함수를 지정하고 있습니다. 7번 코드에서는 사용자가 선택한 파일을 File 타입의 객체를 얻습니다. 그리고 이 객체를 통해 파일명, 파일크기를 각각 8번과 9번 코드를 통해 얻을 수 있습니다. 10번 코드에서 생성한 FileReader 객체는 실제 파일의 내용을 읽어와 해석(Parsing)하기 위해 사용하는 객체입니다. 12번 코드에서는 이 FileReader 객체의 onload 이벤트 함수를 지정하고 있는데요. 바로 이 onload 이벤트 함수가 실행되는 시점은 실제 물리적인 파일로부터 데이터를 메모리에 모두 성공적으로 올릴 때입니다. 여기서 중요한 부분은 실제 물리적인 파일로부터 데이터를 메모리에 올릴때, 파일의 내용 전체를 올릴 수도 있고 위의 예제처럼 파일의 시작에서부터 총 1000바이트만을 올릴 수도 있다는 것입니다. 26번 코드가 바로 파일의 시작에서부터 총 1000바이트를 올리기 위해 BLOB 객체를 생성한 것입니다. 27번 코드는 이 BLOB 객체를 통해 실제 메모리에 올리라는 함수 호출인데요, 이 함수의 이름(readAsArrayBuffer)처럼 메모리에 올린 데이터를 ArrayBuffer 타입으로 처리할 수 있도록 합니다. 이미 언급했듯이 성공적으로 메모리에 지정한 데이터를 올리게 되면 12번 코드에서 지정한 onload 이벤트가 호출됩니다. 이 onload 이벤트에 지정한 함수를 살펴보면, 먼저 13번 코드에서 메모리에 올린 데이터(청크(Chunk)라고 함)를 ArrayBuffer 타입의 객체로 얻습니다. 그리고 이 ArrayBuffer 객체를 이용하여 DataView 타입의 객체를 생성합니다. 이처럼 ArrayBuffer에서 DataView로 변경하는 이유는 DataView가 메모리 덩어리로부터 쉽게 원하는 타입의 값을 읽어올 수 있는 함수를 제공하기 때문입니다. 바로 16번 코드에서부터 21번 코드에서 보이는 getUint8이나 getInt32 그리고 getInt16인데요. 이 함수는 1개 또는 2개의 인자를 갖습니다. 공통적으로 첫번째 인자는 메모리에 올라간 데이터에서 읽어올 위치이고 두번째는 2바이트 이상의 데이터를 읽어왔을 경우에 엔디안(Endian) 처리를 어떻게 할 것인가 입니다. 이 인자를 지정하지 않거나 false로 지정하면 Big Endian인데, 위의 예제에서는 true를 주어 Little Endian으로 해석하라고 지시하고 있습니다.

이처럼 HTML5에서 제공하는 File API를 이용하여 바이너리(Binary) 차원에서 원하는 데이터를 원시 타입 단위로 읽어올 수 있는 것을 볼 수 있습니다. HTML5의 File API, 참으로 멋진 기능입니다.

웹에서 마우스 휠(Wheel) 이벤트

마우스 이벤트 중 마우스 휠 이벤트는 아직까지도 각각의 웹 브라우저에서 다른 형태로 제공되고 있습니다. 아래는 HTML Element에 대한 휠 이벤트를 할당하는 코드입니다.

// for IE, Chrome, Opera
this._div.addEventListener('mousewheel', this._mouseWheel);

// for FireFox
this._div.addEventListener('DOMMouseScroll', this._mouseWheel, false);

IE와 크롬, 오페라에서는 mousewheel이라는 이벤트 이름으로 제공하고 파이어폭스에서는 DOMMouseScroll이라는 이벤트 이름으로 제공됩니다. 이 휠 이벤트를 처리하는 함수의 예(위의 예제의 경우 this._mouseWheel 함수)는 다음과 같습니다.

_mouseWheel: function(e) {
    var delta = 0;

    /* For IE */
    if (!e) e = window.event;
            
    if (e.wheelDelta) delta = e.wheelDelta / 120; /* IE/Chrome/Opera */
    else if (e.detail) delta = -e.detail/3; /* Mozilla case */

    e.preventDefault();
}

휠 이벤트에서 휠에 대한 정도를 얻는 속성도 다릅니다. 파이어폭스에서는 detail이고 그 외에서는 wheelDelta입니다.

[JavaScript] array의 splice 함수 정리

자바스크립트의 배열(Array) 객체에서 제공되는 함수인 splice를 이용하면 원하는 위치에 요소를 추가하거나 삭제할 수 있습니다. 먼저 splice 함수를 사용해 원하는 위치에 요소를 추가하는 것을 정리해보겠습니다. 예를 들어, 먼저 a라는 배열 객체가 다음과 같은 상태라고 하겠습니다.

사용자 삽입 이미지

이 상태에서 다음 코드를 실행하면 다음의 상태가 됩니다.

a.splice(2, 0, "7");

사용자 삽입 이미지

그럼 다시 a의 처음 상태로 돌아가 다음 코드를 실행하면 다음의 상태가 됩니다.

a.splice(0, 0, "7");

사용자 삽입 이미지

이제 다시 a의 처음 상태로 돌아가 다음 코드를 실행하면 다음의 상태가 됩니다.

a.splice(5, 0, "7");

참고로 위의 코드에서 인덱스 5 대신 5이상의 값을 사용해도 결과는 동일합니다.

사용자 삽입 이미지

이제는 splice를 이용해 추가가 아닌 삭제에 대한 예를 살펴보겠습니다. 다음 코드를 실행하면 다음의 상태가 됩니다.

a.splice(2, 1);

사용자 삽입 이미지

[C++11] decltype

decltype은 주어진 표현식에 대한 결과의 타입을 컴파일러가 추론하라는 키워드(keyword)입니다. 예를 들어서 다음의 코드를 보면..

#include "stdafx.h"
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    cout << typeid(decltype(10)).name() << endl;

    return 0;
}

위 코드에서 8번을 보면 decltype(10)이라는 코드가 있는데, 표현식 10에 해당하는 type을 컴파일러가 추론을 하여 해당 타입의 이름을 출력하게 되는 것으로 결과는 'int'를 출력합니다.

또 다른 예를 살펴보면..

#include "stdafx.h"
#include 

using namespace std;

auto func(int a, int b) -> decltype(a + b)
{
    return a + b;
}

int _tmain(int argc, _TCHAR* argv[])
{
    cout << func(100, 200) << endl;

    return 0;
}

6번 코드를 보면 함수의 결과값에 auto를 사용할 수 없음에도 func라는 함수의 반환값이 auto를 사용하고 있습니다. 이것이 가능한 이유는 바로 함수의 정의 뒤에 오는 -> decltype(a+b)에 의해서 입니다. 즉, a+b에 대한 결과값의 타입을 추론하여 ->에 의해 함수의 반환값 추론에 대한 힌트를 제공하는 것입니다. a와 b는 int 타입이고 이 int 값들의 합 역시 int이므로 쉽게 함수의 결과값의 타입은 int라는 것을 추론할 수 있는 것입니다.

[C++11] 람다(Lambda) 표현식

C++11은 람다 표현식 기능을 제공합니다. 람다 표현식은 함수를 미리 정의하지 않고 필요한 시점에서 사용하고 바로 버리는 것으로 생각할 수 있습니다. 람다가 제공되기 이전에 어떤 함수를 사용하고자 했다면, 그 함수를 미리 정의하고 사용하게 됩니다. 사용하고 난 뒤에 그 함수는 계속 존재합니다.아래는 람다 표현식을 이용해 만들어진 함수 예입니다.

#include "stdafx.h"
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    auto func = []() {
        cout << "Hello World" << endl;
    };

    func();

    return 0;
}

8번 코드에서 람다 표현식을 이용해 함수를 정의하고 있습니다. 정의된 함수는 예제처럼 어떤 변수에 함수를 할당해 놓고 12번 코드에서 실행할 수 도 있지만 어떤 변수에 할당하지 않고 바로 실행할 수 도 있습니다. 즉, 아래처럼 말입니다.

#include "stdafx.h"
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    []() {
        cout << "Hello World" << endl;
    }();

    return 0;
}

첫번째 예제와는 다르게 이 두번째 예제는 필요한 시점에 함수를 정의하고 바로 실행하고 함수를 폐기하고 있습니다. 8번 코드에서 보는 것처럼 람다 표현식은 2가지로 구성됩니다. []과 {}이며 []는 람다 소개자(Lambda Introducer) 또는 Capture Clause라고 하며 {}는 람다 몸체(Lambda Body)입니다. 람다 몸체에 함수 구현 코드가 존재하며 람다 소개자에 외부 변수를 어떤 식으로 참조할지에 대한 Capture 지정자과 함수에 입력될 인자(Parameter) 리스트가 지정됩니다. 예를 들어 람다 표현식으로 정의한 함수의 인자로 int를 받아 출력하는 함수는 다음과 같습니다.

#include "stdafx.h"
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    [](int a) {
        cout << a << endl;
    }();

    return 0;
}

이제 람다 함수(람다 표현식으로 정의된 함수)에 대해 몇가지 예를 통해 좀더 정리해 보겠습니다. 다음 코드를 살펴 보겠습니다.

#include "stdafx.h"
#include 
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    vector v { 1, 2, 3, 4, 5 };

    for_each(v.begin(), v.end(), [](int v) {
        cout << v << endl;
    });

    return 0;
}

위의 코드 중 12번의 for_each 함수는 컨테이너의 각 요소를 인자로 어떤 함수(또는 함수객체)를 호출하는 것으로 for_each의 세번째 인자가 바로 호출되는 함수(또는 함수객체)가 됩니다. 일반적으로 호출할 함수를 미리 정의해 두어야 하나 여기서는 람다 함수를 이용해 미리 함수를 정의하지 않고 필요한 시점에 함수를 정의하고 사용하고 있습니다. 다음 예제를 보겠습니다.

#include "stdafx.h"
#include 
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    vector v { 1, 2, 3, 4, 5 };

    auto it = find_if(v.begin(), v.end(), [](int v)->bool {
        return (v % 2) == 1;
    });

    cout << *it << endl;

    return 0;
}

위의 코드 중 12번에 있는 find_if 함수는 컨테이너를 구성하는 요소 중 어떤 조건을 만족하는 첫번째 요소의 값을 반환하는 함수인데, 조건을 지정하기 위해 함수 또는 함수 객체를 세번째 인자에 지정됩니다. 여기서는 람다 함수를 통해 조건 함수가 지정되었는데 이 함수의 반환 타입(Type)을 지정하기 위해서 ->bool를 사용함으로써 반환 타입이 bool이라고 명시하고 있습니다. 엄격하게는 이 ->bool 코드를 생략할 수 있는데, 이는 컴파일러가 이 람다함수의 반환타입을 추론할 수 있기 때문입니다. 컴파일러의 추론 대신 직접 반환 타입을 알려주기 위해 ->(type) 연산자를 사용할 수 있다는 것에 대한 예제입니다.

#include "stdafx.h"
#include 
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    vector v { 1, 2, 3, 4, 5 };
    int sum = 0;

    for_each(v.begin(), v.end(), [&](int v) {
       if (v % 2 == 0) sum += v;
    });
    
    cout << sum << endl;

    return 0;
}

위 코드 중 13번의 람다 함수를 보면 []에 해당하는 람다소개자의 형태가 [&]처럼 되어 있습니다. 이 [&]의 의미는 람다 함수 앞단에 모든(All) 외부 변수를 참조 타입(Reference Type)으로 잡아(Capture) 사용하겠다는 의미입니다. 실제 람다 함수 구현부를 보면 외부에 존재하는 sum 변수를 사용하고 있습니다. 참조 형태로 사용하겠다고 했으므로 람다 함수 내부에서 변수값을 변경하면 외부에서도 변경되는 것을 알 수 있습니다. 람다 소개자가 []일 경우에는 어떤한 외부 변수도 사용하지 않겠다는 의미이며 [&]는 모든 외부 변수를 참조로 사용하겠다는 것이고, [=]는 모든 외부 변수를 상수값(const)으로 값 자체를 복사(Copy)하여 사용하겠다는 의미입니다. [&a]는 외부 변수 중 a만을 참조값으로 사용하겠다는 의미이고 [&a, =b]는 a는 참조값으로 b는 상수값으로 사용하겠다는 의미입니다.