Javascript 에서는 원시 이진 데이터를 컨트롤 할 수 있다. 이를 Typed Array 라 한다. 이를 사용하면 텍스트나 파일을 바이트 형태로 전송할 수 있다.

WebRTC나 WebSocket 의 경우 파일을 Base64 형태의 문자열로 변환하여 보낼 수도 있으나 이럴 경우 대용량 파일이 문제가 된다. 어마어마한 양의 Base64 문자열을 안정적으로 핸들링하기가 여간 어려운것이 아니다. 이와 같은 경우 파일을 바이트로 변환하여 송수신할 수 있다.

TypedArray 를 사용하기 위해서는 두가지만 기억하면 된다. 바로 버퍼와 뷰이다. 버퍼는 길이가 정해진 이진 데이터 버퍼이다. 다시 말해 데이터가 담길 그릇의 크기를 정하는 것이다. 뷰는 버퍼에 데이터를 읽거나 쓸 수 있도록 로우 레벨 인터페이스를 제공한다.

여기서는 버퍼와 뷰의 사용법 외에 한글을 인코딩하는 방법에 대해서도 설명한다.

자료형

이름 범위 설명 Type
Int8Array -128 ~ 127 부호있는 8비트 정수 char
Uint8Array 0 ~ 255 부호없는 8비트 정수 unsigned char
Int16Array -32,768 ~ 32,767 부호있는 16비트 정수 short
Uint16Array 0 ~ 65,535 부호없는 16비트 정수 unsigned short
Int32Array -2,147,483,648 ~ 2,147,483,647 부호있는 32비트 정수 int
Uint32Array 0 ~ 4,294,967,295 부호없는 32비트 정수 unsigned int
Float32Array -3.4 x 10의 38승 ~ 3.4 x 10의 38승 32-bit IEEE floating point number float
Float64Array -1.79 x 10의 308승 ~ 1.79 x 10의 308승 64-bit IEEE floating point number double

Buffer

일단 8 바이트 크기의 버퍼를 생성하는 방법은 아래와 같다.

var buf = new ArrayBuffer(8);

View

뷰는 다음과 같이 생성한다.

var view = new DataView(buf);

DataView 는 다양한 형태의 데이터를 읽고 쓸 수 있다. 뷰를 생성했다면, 뷰의 메소드들을 사용할 수 있다. 뷰의 get/set 메소드들은 아래와 같은 것들이 있다.

set

이름 설명
setInt8 1 바이트 크기의 value 를 설정한다
setUint8 1 바이트 크기의 value 를 설정한다
setInt16 2 바이트 크기의 value 를 설정한다
setUint16 2 바이트 크기의 value 를 설정한다
setInt32 4 바이트 크기의 value 를 설정한다
setUint32 4 바이트 크기의 value 를 설정한다
setFloat32 4 바이트 크기의 value 를 설정한다
setFloat64 8 바이트 크기의 value 를 설정한다

get

이름 설명
getInt8 1 바이트 크기의 value 를 반환한다
getUint8 1 바이트 크기의 value 를 반환한다
getInt16 2 바이트 크기의 value 를 반환한다
getUint16 2 바이트 크기의 value 를 반환한다
getInt32 4 바이트 크기의 value 를 반환한다
getUint32 4 바이트 크기의 value 를 반환한다
getFloat32 4 바이트 크기의 value 를 반환한다
getFloat64 8 바이트 크기의 value 를 반환한다

데이터 읽고 쓰기

생성한 뷰에 데이터를 써보겠다.

view.setInt8(0, 1);
view.setInt8(1, 2);
view.setInt8(2, 3);
view.setInt8(3, 4);
view.setInt8(4, 5);
view.setInt8(5, 6);
view.setInt8(6, 7);
view.setInt8(7, 8);

첫번째 인자는 값을 설정하는 버퍼의 위치이다. 그리고 두번째 인자는 해당 위치에 설정되는 값이다.

view.getInt8(0); //0번째 위치의 값을 반환. 1
view.getInt8(1); //1번째 위치의 값을 반환. 2
view.getInt8(2); //2번째 위치의 값을 반환. 3
view.getInt8(3); //3번째 위치의 값을 반환. 4
view.getInt8(4); //4번째 위치의 값을 반환. 5
view.getInt8(5); //5번째 위치의 값을 반환. 6
view.getInt8(6); //6번째 위치의 값을 반환. 7
view.getInt8(7); //7번째 위치의 값을 반환. 8

첫번째 인자로 위치를 지정하면 해당 위치의 값을 반환한다.

2 바이트를 차지하도록 값을 설정해 보도록 하겠다.

view.setInt16(0, 32001);
view.setInt16(2, 32002);
view.setInt16(4, 32003);
view.setInt16(6, 32004);
view.getInt16(0); //32001
view.getInt16(2); //32002
view.getInt16(4); //32003
view.getInt16(6); //32004

0 번째부터 2바이트를 읽으므로 위치를 0, 2, 4, 6 으로 지정하였다.

특정 형태의 데이터 뷰 생성하기

var view = new Uint8Array(buf);

해당 view 는 unsigned char 형태의 데이터만 읽거나 쓸 수 있다. 고로 해당 뷰는 0 ~ 255 까지의 데이터만을 설정할 수 있는 것이다.

해당 뷰에 데이터를 쓰기 위해서는 아래와 같이 한다.

view[0] = 1;
view[1] = 2;
view[2] = 3;
view[3] = 4;
view[4] = 5;
view[5] = 6;
view[6] = 7;
view[7] = 8;

뷰에서 데이터를 읽기 위해서는 아래와 같이 한다.

view[0]; //1
view[1]; //2
view[2]; //3
view[3]; //4
view[4]; //5
view[5]; //6
view[6]; //7
view[7]; //8

한글 인코딩하기

Uint8Array 로 생성한 뷰에 한글을 데이터로 설정하려면 어떻게 해야하는 것일까? 일단 charCodeAt 메소드를 이용하여 문자의 유니코드 값을 반환받는다. 그런데 영문이 아닌 경우 유니코드 값은 255 가 넘어 갈 것이다. 자료형이 표현할 수 있는 범위를 넘어서는 것이다. 이 경우 Uint8Array 의 뷰에 2 바이트를 차지하도록 데이터를 설정해야 한다.

var view = new Uint8Array(buf);
view[0] = xxx;
view[1] = xxx;
//위 두바이트가 한 글자이다.

위와 같이 설정하려면 어떻게 해야할까? 이는 비트연산과 논리곱을 통해 수행할 수 있다.

var text = "가나다라마바사";
var buf = new ArrayBuffer(text.length * 2);
var view = new Uint8Array(buf);
var unicode = 0;
var index = 0;

for(var i=0; i<text.length; i++){
  unicode = text.charCodeAt(i);
  view[index] = unicode >>> 8;
  view[index + 1] = unicode & 0xFF;
  index = index + 2;
}

우선 각 문자에 대한 유니코드 값을 반환받는다. “가”의 경우 10진수 유니코드 값은 44032 이다. 이를 오른쪽으로 1바이트만큼 비트연산을 수행하면 172 가 된다. 이를 2 바이트의 첫번째 바이트에 저장한다. 그리고 유니코드 값에 255를 논리곱을 수행하여 두번째 바이트에 저장한다. 이렇게 하면 첫번째 바이트에는 172, 두번째 바이트에는 0 이 저장되었을 것이다.

이를 다시 원래 문자로 읽기 위해서는 다음과 같이 한다.

var text2 = "";
for(var i=0; i<view.length; i=i+2){
  unicode = (view[i] * 255) + view[i] + view[i + 1];
  text2 = text2 + String.fromCharCode(unicode);
}

공식은 다음과 같다.

(비트연산의 값 * 255) + 비트연산의 값 + 논리곱의 값

데이터뷰를 사용하여 읽을 경우, 2바이트를 한꺼번에 읽을 수 있다. 위 공식을 적용한 것과 같은 결과를 반환한다.

var text2 = "";
var dataView = new DataView(buf);
for(var i=0; i<dataView.byteLength; i=i+2){
  unicode = dataView.getUint16(i);
  text2 = text2 + String.fromCharCode(unicode);
}

TypedArray 를 이용하여 파일 전송하기

위에서 언급한 것처럼 WebRTC 나 WebSocket 을 이용하여 대용량 파일을 전송할 경우 TypedArray 를 이용하여 바이트로 전송할 수 있다. 대용량의 경우, 한번에 전송할 수 없으면 적절한 청크 사이즈만큼 잘라서 전송해야한다. 이 경우 송신측과 수신측이 바이트배열의 포맷을 미리 약속해 두어야하는데 예를 들어 다음과 같다

0 ~ 7 처음 8 바이트까지는 보내는 파일에 대한 유니크한 아이디값
8 ~ 15 그다음 8바이트는 파일의 용량
16 ~ 270 그다음 255 바이트는 파일의 mimeType
271 ~ 525 그다음 255 바이트 파일명
526 ~ 529 그다음 4바이트는 청크사이즈만큼 자른 후의 보낼 횟수. 곧 페이지의 전체 크기

이제 실제 바이트배열로 변환해 보겠다.

var headerBuf = new ArrayBuffer(530);
var headerDv = new DataView(headerBuf);
headerDv.setFloat64(0, "유니크ID");
headerDv.setFloat64(8, totalSize);

//mimeType 은 2바이트 처리한다.
for (var i = 16; i<271; i=i+2) {
  tmp = mimeType.charCodeAt(j);
  if(tmp){
    headerDv.setUint8(i, tmp >>> 8);
    headerDv.setUint8(i+1, tmp & 0xFF);
    //headerDv.setUint16(tmp); //혹은 tmp 를 2바이트를 차지하도록 바로 저장할 수 있다.
  }
}

//file name 은 2바이트 처리한다.
for (var i = 271; i<526; i=i+2) {
  tmp = fileName.charCodeAt(j);
  if(tmp){
    headerDv.setUint8(i, tmp >>> 8);
    headerDv.setUint8(i+1, tmp & 0xFF);
    //headerDv.setUint16(tmp); //혹은 tmp 를 2바이트를 차지하도록 바로 저장할 수 있다.
  }
}

headerDv.setInt32(526, Math.ceil(fileSize / chunkSize));


//websocket send
websocket.send(headerBuf);

위와 같이 파일에 헤더값을 바이트배열로 변환하여 수신측에 전송하여 파일 전송을 알린다. 그리고 그 다음부터 본격적으로 파일을 청크 사이즈 만큼 전송한다. 이때도 역시 포맷을 미리 약속해야 한다.

0 ~ 7 처음 8 바이트까지는 보내는 파일에 대한 유니크한 아이디값
8 ~ 11 그다음 4바이트는 현재 보내고 있는 페이지 인덱스. 1씩 증가하면서 페이지의 전체 크기만큼 증가할 것이다.
15 ~ 청크사이즈만큼 그다음 크기는 청크사이즈만큼 자른 파일을 전송한다.

파일을 실제 보내보겠다.

function concatBuffer(buf1, buf2){
  var tmp = new Uint8Array(buf1.byteLength + buf2.byteLength);
  tmp.set(new Uint8Array(buf1), 0);
  tmp.set(new Uint8Array(buf2), buf1.byteLength);
  return tmp.buffer;
}

var bodyBuf = new ArrayBuffer(12);
var bodyDv = new DataView(bodyBuf);
bodyDv.setFloat64(0, "유니크ID");

var reader = new FileReader();

reader.onload = function(e){
  bodyDv.setUint16(8, 1); //1 페이지 전송
  var buf = concatBuffer(bodyBuf, e.target.result);

  //websocket send
  websocket.send(buf);
};

var offset = 0;
var size = offset + chunkSize;  //chunkSize를 지정해야한다.
var slice = file.slice(offset, size); //파일을 청크사이즈만큼 자른다.
reader.readAsArrayBuffer(slice);

코드에서는 첫번째 청크사이즈만큼만 파일을 보냈지만, 이를 변경하여 파일이 전부 보내질때까지 반복하여 보낼 수 있다.