본 포스팅은 여기에 올라온 게시글을 바탕으로 작성되었습니다.
파트와 카테고리 동일한 순서로 모든 내용을 소개하는 것이 아닌, 몰랐거나 새로운 내용 위주로 다시 정리하여 개인공부 목적으로 작성합니다.
중간중간 개인 판단 하에 필요하다고 생각될 시, 기존 내용에 추가로 보충되는 내용이 있을 수 있습니다.
웹 개발을 진행하다 보면 원시 데이터를 다루어야 할 때를 간혹 마주칠 수 있다. 브라우저에선 주로 파일 생성, 업로드, 다운로드 또는 이미지 처리와 관련이 깊고 만약 서버 사이드인 node.js
환경까지 고려한다면 더 많은 유형의 원시 데이터를 다루는 상황이 있을 수 있다.
자바스크립트를 이용해서도 이러한 원시 바이너리(이진) 데이터에 접근할 수 있다. 보통 이러한 바이너리 데이터에 접근하고 조작하는 작업은 고성능을 위해 필요한 경우가 많다.
앞서 몇몇 챕터에서 자바스크립트의 가비지 컬렉션에 관한 내용과 그 배경에 대해 여러 차례 언급한 바 있다. 주요 내용을 다시 한 번 설명하자면, 자바스크립트는 동적 언어이고 자바스크립트 엔진에 의해 메모리 관리가 이루어진다. 이는 다르게 말하면 개발자가 직접 메모리에 접근할 수 없으며, 따라서 메모리 최적화와 관련한 작업에 권한이 없거나 아주 작은 권한만을 가질 수 있다는 것을 의미한다. 개발자가 메모리를 관리하지 않음으로써 얻는 편리함은 막대하지만, 그로 인해 예상치 못한 시점에서 어떠한 성능 저하 이슈가 발생할 수 있다.
ArrayBuffer
를 이용한다면 어느정도 개발자가 수동으로 메모리를 관리할 수 있다. 즉 어느 정도 편리함은 포기하고, 대신 그에 대한 대가로 고성능을 목표하고자 하는 경우에 ArrayBuffer
를 이용할 수 있다. 하지만 고성능이라는 키워드가 주는 달콤함에 쉽사리 유혹되지 않도록 주의하자. 사실 대부분의 웹 페이지, 그리고 웹 애플리케이션에서 메모리를 수동으로 관리할 정도로 고성능을 요구하는 경우는 그렇게 흔한 경우가 아니기 때문이다. 혹자는 고성능의 기능 사항을 웹 브라우저에게 기대하는 것 자체가 잘못된 전제라고 이야기하기도 한다. 그렇지만 웹 생태계는 단순히 웹 페이지의 개념을 넘어, 웹 애플리케이션의 단계로 정착한 지 오래되었고 성능 이슈는 모든 엔지니어의 운명과도 같은 숙명이기 때문에 그저 외면하고 있을 수 만은 없다. 때문에 가장 중요한 것은 사용하고자 하는 기술이 오버 엔지니어링이 아닌지에 대한 깊은 고민과 이를 판단할 수 있는 넓은 안목이 필요하겠다.
사설이 길어졌지만 핵심을 요약하자면, ArrayBuffer
는 자바스크립트에서 원시 데이터(바이너리 데이터)를 다루는 수단으로 사용되며, 이는 메모리를 개발자가 수동으로 관리할 수 있는 대안을 제시한다. 특히 성능에 민감한 이슈를 다룬다거나, 아니면 추후에 학습할 Blob
등의 큰 용량의 파일 데이터를 다루는 경우에 ArrayBuffer
를 사용해 유연하고 효율적으로 작업할 수 있다.
따라서 이번 챕터에서는 ArrayBuffer
에 대한 개념과 간단한 사용방법에 대해 살펴보고, 추후 Blob
또는 파일과 관련한 데이터 유형을 살펴보도록 하자.
자바스크립트에서 바이너리 데이터는 다른 언어들과 비교하면 비표준 방식으로 구현되어 있다. 자바
나 C언어
를 떠올려보자. 가장 대표적인 예시로 각 언어가 숫자를 저장하는 형식을 떠올려볼 수 있을 것이다. 정수형 숫자를 저장하기 위해 int
, long
, unsigned int
등의 형식을 자바스크립트에서는 전혀 사용하지 않고 오직 number
형만을 사용하는 것은 이미 이전 챕터에서부터 살펴본 자바스크립트의 특징이다. 하지만 ArrayBuffer
에서는 다른 언어들과 유사하게 바이너리 데이터 접근을 위해 이러한 형을 사용할 수 있다.
자바스크립트에서 모든 이진 데이터는 기본적으로 모두 ArrayBuffer
객체에 포함된다. ArrayBuffer
는 연속된 공간의 메모리에 할당되는 고정 길이에 대한 참조 객체이다. 해당 객체는 다음과 같이 생성할 수 있다.
// 16 바이트 크기의 buffer를 생성
let buffer = new ArrayBuffer(16);
alert(buffer.byteLength); // 16
이때 생성된 buffer
객체는 연속된 16 바이트 메모리 공간을 차지하고 있으며, 초기값은 모두 0으로 채워져 있다. ArrayBuffer
자체는 그 이름에서 혼동이 생길 수 있지만 자바스크립트의 Array
가 아니다. 즉 배열과는 전혀 연관관계가 없다. ArrayBuffer
는 자바스크립트의 배열과 비교해 다음과 같은 차이점이 있다.
buffer[index]
와 같이 인덱스를 통해 접근할 수 없다. 대신에 view
라고 불리는 별도의 객체를 생성해서 접근해야 한다.ArrayBuffer
는 그저 메모리의 연속된 공간을 차지하고 있는 추상적인 메모리 계층과도 같다. 그리고 내부에는 연속된 원시 바이트 정보를 저장하고 있다. ArrayBuffer
에 대한 접근 및 조작은 항상 view
라고 불리는 객체를 생성해서 수행해야 한다.
view
객체는 마치 안경과도 같은 역할을 한다고 볼 수 있다. view
객체는 자기 스스로는 어떠한 데이터도 저장하고 있지 않고, 그저 ArrayBuffer
의 내부를 들여다보기 위한 수단으로 사용한다. 이러한 view
객체에는 다음과 같은 유형이 있고, 이들은 보통 TypedArray
라는 용어로 표현한다.
Uint8Array
: ArrayBuffer
에 있는 각 바이트를 개별적으로 다루는 view
객체이다. 숫자 8
은 비트를 의미하고 8비트 = 1바이트
이기 때문에 각 바이트별로 접근이 가능하다. 1바이트 크기이기 때문에 0 - 255
범위의 수를 다룰 수 있다. Uint8
의 의미는 Unsigned 8bit Int
와도 같다.Uint16Array
: 2바이트 단위 정수형으로 접근이 가능한 view
객체이다. 따라서 다룰 수 있는 범위는 0 - 65535
에 해당한다.Uint32Array
: 4바이트 단위 정수형으로 접근이 가능한 view
객체이다. 따라서 다룰 수 있는 범위는 0 - 4294967295
에 해당한다. Float64Array
: 8바이트 단위로 부동 소수점 방식으로 접근이 가능한 view
객체이다. 다룰 수 있는 범위는 5.0x10^-324
부터 1.8x10^308
까지이다.앞서 ArrayBuffer
의 크기를 16바이트로 생성했기 때문에, 해당 객체는 여러 크기를 가진 view
객체를 통해 접근할 수 있다. 이를 그림으로 나타내면 아래와 같다.
ArrayBuffer
는 단지 원시 바이너리 데이터를 다루기 위한 가장 최상위 핵심 객체이다. 하지만 이는 고정된 메모리 공간에 대한 추상화 계층에 해당하기 때문에 여기에 값을 새로 쓴다거나, 반복을 돌린다거나 하는 등의 작업은 항상 view
객체를 통해 수행해야 한다.
let buffer = new ArrayBuffer(16);
let view = new Uint32Array(buffer);
console.log(Uint32Array.BYTES_PER_ELEMENT); // 4 (32비트 = 4바이트 이므로)
console.log(view.length); // 4 (16바이트 크기에서 해당 view 형식으로 저장된 정수형 개수)
console.log(view.byteLength); // 16 (16바이트 크기)
// view를 통해 값을 입력
view[0] = 123456;
for (let num of view) {
console.log(num); // 123456, 0, 0, 0
}
이러한 view
객체를 일컫는 용어로 보통 TypedArray
라는 표현을 사용한다고 했다. 이는 자바스크립트의 객체는 아니고 일반적으로 모든 view
객체를 공통적으로 부르는 용어로만 사용할 뿐이다. 여기서도 view
객체들을 모두 TypedArray
유형이라고 부르도록 하겠다.
모든 view
객체는 TypedArray
유형에 속하기 때문에 같은 메서드와 프로퍼티를 가지고 있다. 또한 TypedArray
유형은 자바스크립트의 일반 배열과 거의 유사하게 동작한다. 이들은 인덱스를 가지고 있고 또한 이터러블 속성 역시 가지고 있다. TypedArray
유형의 생성자는 5가지의 인자를 받아 해당 유형의 TypedArray
를 생성할 수 있는데 그 목록은 다음과 같다.
new TypedArray(buffer, [byteOffset], [length]);
new TypedArray(object);
new TypedArray(typedArray);
new TypedArray(length);
new TypedArray();
// 실제로 TypeArray 라는 생성자함수(클래스)는 없다는 것에 주의하자!
// 여기서 TypeArray는 Uint8Array, Uint16Array 등을 의미한다!
ArrayBuffer
ArrayBuffer
를 인자로 생성하는 경우는 이미 위에서 가장 먼저 살펴본 경우이다. 해당 ArrayBuffer
를 커버하는 TypedArray
유형의 view
객체가 생성된다. 옵션값으로 byteOffset
과 length
를 설정할 수 있다. offset
은 별도로 지정하지 않는다면 0
이 기본값이며, 시작지점을 의미하고, length
는 오프셋으로부터 어디까지를 지정할 지를 나타낸다.
Array
만약 인자로 Array
또는 유사 배열 객체가 넘어오는 경우엔, 해당 객체와 같은 값을 가진 TypedArray
유형의 view
객체가 생성된다. 즉 생성된 TypedArray
유형의 view
가 바라보는 ArrayBuffer
는 전달된 배열의 값을 동일하게 저장한다.
let arr = new Uint8Array([0, 1, 2, 3]);
console.log( arr.length ); // 4 (4개의 값을 1바이트씩 저장 => 총 4바이트 크기)
console.log( arr[1] ); // 1
TypedArray
만약 또 다른 TypedArray
가 인자로 전달되면 이와 동일한 크기와 값을 지닌 지정된 TypedArray
유형의 view
객체를 생성한다. 이때 값은 지정된 TypedArray
유형에 따라 전환되는데, 만약 해당 유형에서 처리할 수 없는 큰 값의 경우에는 가용 범위내의 숫자로 변환된다.
let arr16 = new Uint16Array([1, 1000]);
let arr8 = new Uint8Array(arr16);
console.log( arr8[0] ); // 1
console.log( arr8[1] ); // 232 (1000은 8비트로 표현이 불가)
length
크기값 length
가 인자로 전달되면 해당 크기만큼의 요소를 포함하는 TypedArray
유형의 view
객체를 생성한다. 이때 ArrayBuffer
가 가지는 총 바이트는 length
에 지정된 숫자와 하나의 요소에 할당되는 크기의 곱과 같다. 즉 length x TypedArray.BYTES_PER_ELEMENT
와 동일하다.
let arr = new Uint16Array(4); // 4개 정수를 포함하는 TypedArray view 생성
console.log(Uint16Array.BYTES_PER_ELEMENT); // 2바이트
console.log(arr.byteLength); // 4x2 = 8
아무 인자도 없는 경우에는 모든 값이 0으로 초기화 된 해당 TypedArray
유형의 view
객체가 생성된다.
위에서 살펴본 것처럼 ArrayBuffer
를 먼저 생성하지 않고도 TypedArray
를 생성할 수 있다. 그러나 기본적으로 모든 TypedArray
는 ArrayBuffer
위에 생성되는 view
객체이기 때문에 첫 번째 경우를 제외하면, 자동으로 그에 대응하는 ArrayBuffer
가 생성된다. 이때 생성된 ArrayBuffer
에는 다음과 같이 접근할 수 있다.
arr.buffer
: ArrayBuffer
에 대한 참조arr.byteLength
: ArrayBuffer
의 바이트 크기따라서 다음과 같이 생성된 ArrayBuffer
를 기준으로 얼마든지 또다른 view
객체를 생성할 수 있다.
let arr8 = new Uint8Array([0, 1, 2, 3]);
let arr16 = new Uint16Array(arr8.buffer);
위에서 잠깐 살펴보았지만, 사용할 수 있는 모든 TypedArray
유형은 아래와 같다.
Uint8Array
, Uint16Array
, Uint32Array
: 정수형 숫자 (8bit, 16bit, 32bit)Uint8ClampedArray
: 8bit 정수형 숫자로 범위 내 숫자형태로만 값을 저장Int8Array
, Int16Array
, Int32Array
: 부호가 있는 정수형 숫자Float32Array
, Float64Array
: 부동소수점 방식의 32bit와 64bit 크기의 숫자
Array
라는 표현에 항상 현혹되지 말도록 하자.Int8Array
라는 클래스를 통해 간혹 자바스크립트에서도Int8
과 같은 데이터 유형을 지정할 수 있을 것이다라는 착각을 할 수 있다. 하지만 알다시피 자바스크립트에서는 정수형의 유형을 지정할 수 있는 타입은 지원하지 않는다.
앞서 각 TypedArray
유형에 해당하는 값이 넘어가는 경우 이상한 숫자가 저장되는 예시를 잠깐 살펴보았다. 그러한 숫자가 저장되는 원리는 매우 간단하다. 컴퓨터에서 모든 숫자는 2진법으로 표현이 가능하다. 따라서 바이너리 데이터는 비트(bit
)를 사용해 모두 표현할 수 있다.
가장 먼저 알아야 할 점은 범위를 벗어나는 숫자를 저장하는 것 자체는 어떠한 에러를 발생시키지 않는다는 점이다. 범위를 벗어나면 해당 벗어난 범위는 해당하는 비트 크기에 의해 자동으로 잘려나가 저장된다.
예를 들어 256
이라는 숫자를 Uint8Array
에 저장하려 한다고 가정해보자. 8bit
까지 저장할 수 있는 TypedArray
이지만 256
을 이진법으로 나타내면 100000000₂
와 같다. 이는 총 9bit
이기 때문에 가장 앞단에 있는 1은 자동으로 날라가게 되어 결국 0
이 저장된다. 10진법으로 생각하면 Uint8Array
의 범위는 0 - 255
이기 때문에 당연히 256
은 255
다음에 위치한 처음 숫자로 돌아가 0
으로 전환된다고 볼 수 있다. 이를 그림으로 나타내면 아래와 같다.
257
을 저장한다고 생각해보자. 이는 2진법으로 나타내면 100000001₂
이고 아래 그림과 같이 맨 앞의 1이 날라가기 때문에 1
로 저장된다.
다르게 표현하면 Uint8Array
에 저장되는 모든 값은 8-modulo
연산 (나머지연산)이 적용된다고 볼 수 있다.
위에서 또 하나의 TypedArray
유형 중에 Uint8ClampedArray
가 있었는데, 이는 위와 같이 숫자를 날리는 연산을 적용하지 않는다. 즉 최대값인 255
보다 큰 값이 들어오는 경우에는 그냥 최대값 255
를 저장한다. 만약 음수값이 들어오면 무조건 0
으로 값을 저장한다. 이와 같은 처리는 이미지 데이터를 처리할 때 유용하게 사용할 수 있다.
TypedArray
는 ArrayBuffer
와 달리 Array
와 상당 부분 유사하며 때문에 대부분의 메서드와 프로퍼티를 공유하고 있다. 따라서 TypedArray
는 배열처럼 이터러블하며, 그 외에 map
, slice
, find
, reduce
등과 같은 내장 메서드 역시 동일하게 사용할 수 있다.
그렇지만 사용할 수 없는 메서드 역시 존재하는데 대표적으로 다음과 같은 메서드가 있다.
splice
: 어떠한 값도 제거할 수 없다. 이는 당연한 소리인데, ArrayBuffer
의 가장 큰 특징 중에 하나는 바로 고정된 크기로 메모리 공간에 할당된다는 것이기 때문이다. 즉 크기 자체는 고정이기 때문에 크기를 조작하는 모든 메서드는 사용할 수 없다.concat
: 해당 메서드 역시 지원되지 않는다.또는 기본 배열에서 지원하지 않는 특수한 메서드를 사용할 수 있다.
arr.set(fromArr, [offset])
: fromArr
로 전달된 배열의 모든 요소를 arr
로 복사한다. 이때 시작 지점을 offset
으로 설정할 수 있다.arr.subarray([begin, end])
: 동일한 TypedArray
유형의 view
객체를 begin
에서 end
이전 범위까지로 지정해 생성한다. 해당 메서드는 slice(begin, end)
내장메서드와 동작하는 방식이 같지만, 얕은 복사가 아닌 새로운 view
객체를 생성한다는 점이 다르다.TypedArray
유형은 모두 해당하는 타입이 정해져있다. 이보다 조금 더 유연하게 ArrayBuffer
에 접근할 수 있는 view
객체가 있는데, 바로 DataView
유형이다. 해당 view
객체는 어떤 위치의 어떤 유형의 데이터라도 모두 접근할 수 있다.
예를 들어 TypedArray
의 경우에는 타입이 지정되어 있고, 그 타입에 해당하는 형태로만 값을 저장하고 접근할 수 있다. 따라서 전체 TypedArray
는 모두 동일한 유형의 값을 가진다. 즉 i
번째 값에 접근하고자 하면 arr[i]
로 접근이 가능하다.
그러나 DataView
는 기본적으로 지원되는 모든 TypedArray
유형으로 값에 개별적인 접근이 가능하다. 이때는 메서드를 이용해 접근하는데, 이는 .getUint8(i)
또는 .getUint16(i)
과 같이 그 유형으로 값을 가져올 수 있다. 형식은 해당 메서드를 호출하는 시점에 결정되기 때문에 TypedArray
와 달리 생성 시점에 결정되지 않는다.
DataView
를 생성하기 위한 문법은 아래와 같다.
new DataView(buffer, [byteOffest], [byteLength]);
buffer
: 기준이 되는 ArrayBuffer
로 TypedArray
와는 달리 스스로 생성할 수는 없다. 때문에 해당 ArrayBuffer
는 미리 준비되어 있어야 한다.byteOffset
: view
객체에서 시작할 위치byteLength
: view
객체에 할당할 바이트 크기 (기본값은 buffer
의 크기와 동일)아래 예시는 DataView
를 이용해 각각 다른 형식으로 동일한 ArrayBuffer
에서 값을 가져오는 코드이다.
// 4바이트 크기의 모두 255 값을 가진 ArrayBuffer 및 그에 대한 view 객체 생성
let buffer = new Uint8Array([255, 255, 255, 255]).buffer;
// 생성된 ArrayBuffer에 대한 dataView 객체 생성
let dataView = new DataView(buffer);
// offset 0에 해당하는 값을 8비트로 접근
console.log( dataView.getUint8(0) ); // 255
// offset 0에 해당하는 값을 16비트로 접근
// 2바이트를 한 번에 읽으므로 11111111 x 2
// 따라서 1111111111111111
console.log( dataView.getUint16(0) ); // 65535
// offset 0에 해당하는 값을 32비트로 접근
// 4바이트를 한 번에 읽으므로 11111111 x 4
// 따라서 1111111111111111
console.log( dataView.getUint32(0) ); // 4294967295
// 4바이트의 값을 0으로 설정
// 따라서 모든 값이 0이 됨
dataView.setUint32(0, 0);
DataView
는 동일한 ArrayBuffer
에 다양한 유형이 혼합된 값을 저장하는 경우에 유용하다. 예를 들어 하나의 버퍼에 16bit
와 32bit float
유형이 저장된 경우에 DataView
를 이용해 손쉽게 해당 값에 접근할 수 있다.
ArrayBufferView
와 BufferSource
ArrayBuffer
를 다룰때 흔히 사용하는 또 다른 용어가 있다.
ArrayBufferView
: 앞서 살펴본 TypedArray
유형의 view
와 동일한 개념이다.BufferSource
: ArrayBuffer
또는 ArrayBufferView
모두 포함하는 용어이다.해당 용어는 다음 챕터에서 등장할 것이다. BufferSource
는 가장 흔하게 사용되는 용어중에 하나로 보통 바이너리 데이터를 어떠한 형태로든 저장하고 있는 ArrayBuffer
또는 그에 대한 view
를 의미한다. 이를 그림으로 나타내면 다음과 같다.
이진 데이터가 문자열인 경우에는 어떻게 처리할 수 있을까? 예를 들어 텍스트 데이터가 있는 파일 자체를 수신했다고 가정해보자. 이때 내장 객체인 TextDecoder
를 이용하면 주어진 버퍼와 인코딩을 통해 값을 실제 자바스크립트 문자열로 변환해 읽을 수 있게 만들어준다.
먼저 디코딩을 위핸 객체를 생성해야 한다.
let decoder = new TextDecoder([label], [options]);
label
: 인코딩 방식을 지정하며, 기본적으로 utf-8
을 가장 많이 사용하지만 big5
, windows-1251
등의 인코딩 방식도 지원된다.options
: 선택 항목으로 다음 설정값이 있다.fatal
: boolean
값으로 true
인 경우 디코딩이 불가능한 잘못된 글자를 대상으로 예외를 던진다. false
라면 예외는 던지지 않고, 해당 문자를 \uFFFD
로 대체한다. ignoreBOM
: boolean
값으로 true
인 경우 사용되지 않는 바이트 순서 표식(Byte Order Mark, BOM
)을 무시한다.그리고 생성한 decoder
객체를 이용해서 디코딩을 진행할 수 있다. 디코딩을 위해 decode
라는 메서드를 지원한다.
let str = decoder.decode([input], [options]);
input
: 디코딩 할 BufferSource
options
: 선택 항목stream
: 많은 양의 데이터를 받아들여 decoder
를 반복적으로 호출할 때도 디코딩이 반복적으로 실행된다. 이런 경우에 멀티 바이트 문자가 많은 데이터로 분할될 수 있다. 해당 옵션은 데이터 분할을 방지하기 위해 TextDecoder
에 "unfinished"
문자를 입력시키고 다음 데이터가 오면 디코딩하도록 지시한다.let uint8Array = new Uint8Array([72, 101, 108, 108, 111]);
console.log( new TextDecoder().decode(uint8Array) ); // Hello
let unit8Array = new Uint8Array([228, 189, 160, 229, 165, 189]);
console.log( new TextDecoder().decode(uint8Array) ); // 你好
버퍼의 하위 배열 뷰를 생성하여 버퍼 일부만 디코딩하는 것도 가능하다.
let uint8Array = new Uint8Array([0, 72, 101, 108, 108, 111, 0]);
// 문자열을 나타내는 배열 요소는 중간에 존재
// subarray를 이용해 배열 복사없이 문자열 부분 추출
let binaryStr = uint8Array.subaray(1,-1);
console.log( new TextDecoder().decode(binaryStr) ); // Hello
TextEncoder
를 이용해 텍스트 디코더와 반대되는 기능인 인코딩을 진행할 수 있다. 이를 통해 문자열을 바이트로 변환한다. 문법은 아래와 같다.
let encoder = new TextEncoder();
TextEncoder
는 인코딩 시 오직 utf-8
만 지원한다. 이때 다음 2가지 메서드가 있다.
encode(str)
: Uint8Array
에 문자열을 이진 데이터로 변환하여 반환encodeInto(str, desination)
: Uint8Array
구조 형태로 문자열 str
을 destination
에 인코딩하여 반환let encoder = new TextEncoder();
let uint8Array = encoder.encode('Hello');
console.log(uint8Array); // 72,101,108,108,111
BLOB
은 Binary Large OBject
의 약자로 주로 파일과 같은 객체를 바이너리 형태로 저장한 것을 의미한다. 이처럼 바이너리 형태로 저장하는 이유는 이미지, 오디오, 비디오와 같은 멀티미디어 파일들은 용량이 큰 경우가 많고 이를 효과적으로 데이터베이스에 저장하기 위해 고안된 자료형이라 볼 수 있다. 멀티미디어 파일 그 자체를 데이터베이스에 그대로 저장하기는 어렵기 때문!
브라우저 환경에서도 자바스크립트를 이용해 이러한 BLOB
데이터에 접근하고 사용할 수 있다. 브라우저의 File
객체 역시 BLOB
의 확장형이다. 앞서 다룬 ArrayBuffer
를 통해서 바이너리 데이터를 BLOB
의 형태로 변환하고, 이를 다시 원본 데이터로 변환하는 등의 작업이 가능하다.
BLOB
은 옵션값으로 주로 MIME type
을 지정할 수 있고, blobParts
라고 불리는 BLOB
, ArrayBuffer
, ArrayBufferView
, BufferSource
, DOMString
등을 인자로 받아 BLOB
객체를 생성할 수 있다. 문법은 아래와 같다.
new Blob(blobParts, options)
blobParts
: Blob
, BufferSource
, DOMString
과 같은 배열options
type
: 변환될 Blob
의 타입으로 주로 image/png
와 같은 MIME Type
endings
: 현재 OS의 newlines에 맞게 end-of-line
을 지정하는 옵션 (\r\n
또는 \n
). 디폴트는 transparent
로 아무것도 하지 않고, native
값을 넘겨주면 변환// DOMString ---> Blob
let blob = new Blob(['<html>...</html>'], { type: 'text/html' } );
// Typed Array ---> Blob
let hello = new Uint8Array([72, 101, 108, 108, 111]);
let blob = new Blob([hello, ' ', 'world'], { type: 'text/plain' });
또한 특정 범위내에서만 추출하여 Blob
객체를 만들어줄 수 있다.
blob.slice([byteStart], [byteEnd], [contentType]);
byteStart
: 바이트 시작값 (디폴트는 0)byteEnd
: 마지막 바이트값 (디폴트는 바이트 끝값)contentType
: 새로운 type
값 (디폴트는 기존 type
값과 동일)Blob
객체 메서드의 slice
는 배열의 메서드와 유사하기 때문에 음수값으로도 범위 지정이 가능하다.
기본적으로 Blob
객체는 불변성을 유지한다. 때문에 해당 객체를 직접 조작하여 값을 바꾸는 행위는 불가한데, 이를 slice
메서드를 통해 일정 부분을 추출하고 새로운 값과 혼합하여 새 Blob
객체를 만드는 방식으로 해결할 수 있다. 이는 사실 자바스크립트의 string
객체와 유사한 매커니즘이다.
이렇게 만들어진 Blob
객체는 <a>
, <img>
태그등에 사용할 수 있는 URL로 쉽게 변환이 가능하다. Blob
객체에 별도로 type
을 명시하기 때문에 Blob
객체를 다운로드/업로드 하는 과정에서 네트워크 요청에서의 Content-Type
은 자연스레 명시된 type
으로 매칭된다. 다음 예시는 다운로드 문구를 클릭했을 때 동적으로 생성되는 Blob
객체를 hello World
라는 문구가 담긴 텍스트 파일로 다운받는 코드이다.
<a download="hello.txt" href="#" id="link">다운로드</a>
<script>
let blob = new Blob(['Hello, world!'], {type: 'text/plain'});
link.href = URL.createObjectURL(blob);
</script>
또는 클릭 이벤트를 통해 위와 동일한 기능의 코드를 작성할 수도 있다.
let link = document.creteElement('a');
link.download = 'hello.txt';
let blob = new Blob(['Hello, world!'], { type: 'text/plain'} );
link.href = URL.createObjectURL(blob);
link.click();
URL.revokeObjectURL(link.href);
URL.createObjectURL
메서드를 이용하면 Blob
객체를 가지고 고유한 URL을 생성할 수 있다. 이때 생성되는 URL의 형태는 blob:<origin>/<uuid>
의 형태를 띄게 된다. 그리고 변환된 URL은 source(src)
를 속성으로 가지는 모든 HTML 태그와 CSS 속성에서 사용 가능하다.
link.href = `blob:https://javascript.info/1e67e00e-860d-40a5-89ae-6ab0cbee6273`;
이때 변환된 URL은 현재 탭의 브라우저 메모리에 저장되고, 저장된 URL은 매핑된 Blob
객체를 참고하고 있는 형태이다. 따라서 짧은 문자열만으로도 원래의 Blob
객체에 접근이 가능하고 그에 따른 이미지 등의 파일을 가져올 수 있다.
때문에 변환된 URL은 항상 현재 문서에서만 유효하다. 변환된 URL을 현재 문서를 새로고침하거나 아니면 다른 페이지에서 사용하려고 한다면 제대로 사용할 수 없다. 다른 탭의 문서나 새로고침했을 때의 문서는 기존에 변환된 URL을 메모리에서 매핑된 형태로 저장하고 있지 않기 때문이다.
이와 관련한 이슈가 한 가지 더 있다. Blob
객체가 URL로 변환되어 매핑이 이루어진 채 메모리에 저장되게 되면, 명시적으로 해당 URL이 해제되기 전까지 브라우저는 해당 URL이 유효하다고 판단하기 때문에 자바스크립트 엔진에서 가비지 컬렉션이 이루어지지 않는다. 따라서 이러한 사이드 이펙트를 방지하기 위해서는 변환이 일어나고 해당 URL을 사용한 이후 더 이상 사용하지 않을 시점이라고 판단되면 명시적으로 해제해 주는 것이 좋다.
변환은 URL.createObjectURL
메서드를 통해 진행했고, 해제의 경우에는 URL.revokeObjectURL
메서드를 사용한다. 이는 내부적으로 매핑되어 있는 참조를 지우는 메서드로, 메모리에 상주하고 있는 Blob
객체를 지울 수 있다. 위에서 자바스크립트로 작성한 예시에서 마지막 라인에 해당 메서드를 통해 참조를 해제하고 있는 것을 볼 수 있다. 동적으로 생성한 Blob
객체는 오직 다운로드 클릭 순간에만 필요하고 그 이후엔 필요하지 않기 때문에 해제를 통해 메모리 누수를 방지할 수 있다.
반면 첫 번째 예시의 경우에는 해제할 수 없다는 것에 주의하자. 첫 번째 예시에서 동일하게 URL.rovokeObjectURL
메서드를 호출한다면 스크립트 실행 시점에서 더 이상 유효한 URL이 아니게 되므로 정상적으로 다운로드가 불가하다.
URL.createObjectURL
의 대안으로 또 다른 방법은 Blob
객체를 base64
방식으로 인코딩하는 방식이 있다. base64
인코딩은 바이너리 데이터를 안전하게 읽을 수 있는 아스키 코드 0 - 64까지의 범위의 문자열로 변환한 형태를 반환한다. 예를 들어 이미지 파일의 경우 base64
형태로 문자열로 변환 후에 데이터베이스에 저장할 수 있다.
중요한 것은 Blob
객체 역시 base64
의 형태로 변환이 가능하고, 변환된 문자열을 바로 source(src)
로써 사용할 수 있다는 점이다.
이러한 URL은 보통 data url
이라고 부르는데, base64
형태로 인코딩 된 data url
의 형태는 다음과 같다: data:[<mediatype>][;base64],<data>
. 이렇게 변환된 base64
형태의 URL은 위에서 URL.createObjectURL
메서드를 통해 변환한 형태와는 달리 어디에서나 사용이 가능하다.
// base64로 인코딩 된 data url 예시
<img src="data:image/png;base64,R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7">
브라우저는 이러한 data url
을 만나면 내부적으로 디코딩을 진행하고 그에 걸맞은 이미지와 같은 파일을 보여주게 된다.
Blob
객체를 base64
로 인코딩 하여 사용하는 방식은 주로 내장 객체 FileReader
를 사용할 때 활용한다. 해당 객체를 이용하면 Blob
객체로부터 더 다양한 포맷으로 필요한 정보를 가져올 수 있다. 이는 다음 File API
챕터에서 더 자세히 다루어보도록 하자. 다음 예시는 Blob
객체를 base64
형태로 인코딩 하여 FileReader
객체를 통해 다운로드하는 코드이다.
let link = document.createElement('a');
link.download = 'hello.txt';
let blob = new Blob(['Hello, world'], {type: 'text/plain'});
let reader = new FileReader();
reader.readAsDataURL(blob);
reader.onload = function() {
link.href = reader.reulst;
link.click();
};
Blob
객체를 URL로 변환하는 두 가지 방식 모두 활용도가 높다. 하지만 base64
로 인코딩 하는 경우엔 문자열의 길이가 매우 길어질 수 있다는 점과 URL.craeteObjectURL
메서드보다 더 느리다는 점이 있는 반면 변환된 data url
은 현재 탭의 문서뿐만 아니라 어디에서도 유효하다는 장점이 있다.
Blob
객체를 이미지, 이미지의 한 부분 또는 페이지 전체의 스크린샷 이미지로도 생성할 수 있다. 이러한 형태는 어딘가로 업로드 하기 굉장히 편리하다.
이와 같인 이미지 작업은 <canvas>
요소를 통해 수행된다.
1. 캔버스위에 이미지를 그린다 (canvas.drawImage
)
2. 캔버스 메서드 toBlob(callback, fromat, quality)
을 호출하여 Blob
객체를 생성하고 callback
을 실행
Blob
생성자를 이용해 거의 BufferSource
를 포함해 대부분의 것으로부터 Blob
객체를 생성할 수 있었다. 그러나 로우 레벨(low-level
)의 처리를 수행하기 위해서는, FileReader
를 이용해 가장 낮은 레벨의 ArrayBuffer
에 접근해야 할 수 있다.
let fileReader = new FileReader();
fileReader.readAsArrayBuffer(blob);
fileReader.onload = function(event) {
let arrayBuffer = fileReader.result;
};
File
객체는 Blob
객체를 확장한 객체로 주로 파일시스템과 관련된 기능을 담당한다. 파일시스템은 OS의 영역인데, 브라우저 상에서도 파일을 주고 받는 등의 기능이 가능하기 때문에 이를 지원하기 위한 규격으로 볼 수 있다.
브라우저에서 자바스크립트를 이용해 파일을 다루기 위한 방법으로는 크게 두 가지가 있다.
먼저 Blob
객체와 유사하게 File
객체를 생성자로 사용하는 방법이 있다.
new File(fileParts, fileName, [options]);
fileParts
: Blob
/ BufferSource
/ String
과 같은 배열 fileName
: 파일 이름 (문자열)options
: 옵셔널 값lastModified
: 마지막 수정이 일어난 때의 timestamp
두 번째 방법으로는 HTML 태그에서 <input type='file'>
등의 속성으로 파일을 간편하게 가져오는 방법이 있다. 그 외에 부가적으로 브라우저에서 드래그&드랍으로 파일을 가져오는 방법도 있을 수 있다.
File
객체는 Blob
의 확장 객체이기 때문에 Blob
객체와 동일한 프로퍼티를 가지며, 추가적으로 다음 두 개의 프로퍼티를 가지고 있다.
name
: 파일 이름lastModified
: 마지막 수정이 일어난 때의 timestamp
아래 예시는 <input type='file'>
을 이용하여 File
을 가져오는 방법에 대한 코드이다.
<input type="file" onchange="showFile(this)">
<script>
function showFile(input) {
let file = input.files[0];
alert(`File name: ${file.name}`);
alert(`Last modified: ${file.lastModified}`);
}
</script>
이때
input type='file'
의 경우 추가적으로multiple
옵션을 지정할 수 있는데 이 경우에는 다량의File
이 배열 형태로 전달되게 된다. 이때를 고려하여 단일 파일만 전달되는 경우에도File
은 항상 유사 배열로 전달되기 때문에 하나의 파일만 다루는 경우에도 인덱스로 접근하는 것에 주의하자
FileReader
는 Blob
또는 File
과 같은 객체로부터 데이터를 읽어 들이기위한 목적으로 사용되는 객체이다. 읽어들인 데이터는 주로 이벤트를 사용하여 필요한 타이밍에 데이터를 전달한다. 문법은 아래와 같다.
let readr = new FileReader();
생성된 FileReader
객체에서 사용할 수 있는 주요 메서드는 아래와 같다.
readAsArrayBuffer(blob)
: ArrauBuffer
형태로 데이터를 읽음readAsText(blob, [encoding])
: encoding
방식에 맞게 텍스트 형태로 데이터를 읽음 (기본 인코딩 방식 - utf-8
)readAsDataURL(blob)
: base64
형태의 data url
로 데이터를 읽음abort()
: 작업을 즉시 중단read*
로 시작하는 메서드는 우리가 어떤 포맷의 데이터를 더 선호하고, 추후에 어떤 형식으로 데이터를 다룰 것인지에 따라 다양하게 선택할 수 있다. 다음은 각 메서드의 간단한 가이드라인이다.
readAsArrayBuffer
의 경우 바이너리 파일 대상으로 로우 레벨의 바이너리 작업이 필요한 경우에 유용하다. 대부분 하이레벨에서 하는 작업의 경우엔 File
객체가 Blob
을 상속받고 있기 때문에 별도의 읽기 과정없이 즉각적으로 slice
등의 메서드 호출이 가능하다.
readAsText
의 경우 텍스트 형태의 문자열이 필요한 경우 유용하다.
readAsDataURL
의 경우 img
태그와 같이 src
속성에 리소스를 다루어야 하는 경우 유용하다. 또는 이전 챕터에서 다룬바와 같이 URL.createObjectURL
을 이용하는 방법도 있다.
FileReader
객체를 이용해 데이터를 읽는 과정에서 발생하는 이벤트 목록은 다으과 같다.
loadstart
: 로딩이 시작될 때progress
: 읽기를 수행하고 있는 중load
: 에러 없이 리딩이 완료된 때abort
: abort()
메서드가 호출된 때error
: 에러가 발생한 때loadend
: 성공/실패 여부 상관없이 리딩이 완료된 때만약 읽기가 모두 완료되었다면 그에 대한 결과는 다음과 같은 프로퍼티로 접근이 가능하다.
reader.result
: 성공 시 읽어들인 결과reader.error
: 실패 시 발생한 에러보통 범용적으로 사용되는 이벤트는 주로 error
와 load
이다. 다음 예시를 통해 그 쓰임을 간략하게 살펴보자.
<input type='file' onchange='readFile(this)' />
<script>
function readFile(input) {
let file = input.files[0];
let reader = new FileReader();
reader.readAsText(file);
reader.onload = function() {
console.log(reader.result);
};
reader.onerror = function() {
console.log(reader.error);
};
}
</script>
이전 챕터에서 언급했던 바와 같이 FileReader
는 File
뿐만 아니라 Blob
역시 읽어들일 수 있다. 따라서 FileReader
객체를 통해 Blob
객체를 다른 형태로 전환할 수 있다.
readAsArrayBuffer(blob)
: blob
→ ArrayBuffer
readAsText(blob, [encoding])
: blob
→ string
(또는 TextDecoder
사용)readAsDataURL(blob)
: blob
→ base64 data url
FileReaderSync
는 웹 워커(Web Worker
)에서 사용할 수 있다. 이는read*
메서드가 이벤트를 통해 결과에 접근하는 것이 아니라 일반 함수처럼 결과를 바로 반환하는 동기 방식으로 사용할 수 있음을 말한다. 웹 워커는 페이지에 영향을 미치지 않기 때문에 동기 방식에서 읽는 동안 발생하는 딜레이가 크게 중요하지 않다.
잘보고 갑니다~!😊