[JS] Buffer.from()메서드에 배열을 인자로 넘기면 일어나는 일

김현우·2022년 2월 22일
1

JavaScript

목록 보기
7/8

Buffer.from(Array)

node@17.5.0 기준이다. 버퍼 쪽은 버전에 크게 영향이 없겠지만 참고하길 바란다.

이 페이지에서는 Buffer.from()에 배열을 넘기면 내부적으로 어떻게 동작하는지 확인하기 위해 nodejs 소스코드를 까본다.

(링크) nodejs 소스코드 중 Buffer에 관련된 소스

Buffer.from() 메서드를 정의한 곳부터 쫓아가면 되겠다. 위의 몇 줄만 보면 된다.

// buffer.js: line 296

Buffer.from = function from(**value**, encodingOrOffset, length) {
  if (typeof value === 'string')
    return fromString(value, encodingOrOffset);

  if (**typeof value === 'object'** && value !== null) { // 이 분기문에 걸려서
    if (isAnyArrayBuffer(value))
      **return fromArrayBuffer(value, encodingOrOffset, length); // fromArrayBuffer를 타고 들어간다.**

    const valueOf = value.valueOf && value.valueOf();
    if (valueOf != null &&
        valueOf !== value &&
        (typeof valueOf === 'string' || typeof valueOf === 'object')) {
      return from(valueOf, encodingOrOffset, length);
    }

    const b = fromObject(value);
    if (b)
      return b;

    if (typeof value[SymbolToPrimitive] === 'function') {
      const primitive = value[SymbolToPrimitive]('string');
      if (typeof primitive === 'string') {
        return fromString(primitive, encodingOrOffset);
      }
    }
  }

  throw new ERR_INVALID_ARG_TYPE(
    'first argument',
    ['string', 'Buffer', 'ArrayBuffer', 'Array', 'Array-like Object'],
    value
  );
};

Buffer.from()에 넘긴 인자가 배열이면 fromArrayBuffer() 함수를 실행하여 그 반환값을 그대로 반환한다.

우리가 Buffer.from()에 넘긴 인자를 그대로 fromArryaBuffer()의 첫 번째 인자로 전달한다.

fromArrayBuffer 함수는 조금만 스크롤하면 바로 있다. 맨 아래만 보면 된다.

// buffer.js: line 449

function fromArrayBuffer(**obj**, byteOffset, length) {
  // Convert byteOffset to integer
  if (byteOffset === undefined) {
    byteOffset = 0;
  } else {
    byteOffset = +byteOffset;
    if (NumberIsNaN(byteOffset))
      byteOffset = 0;
  }

  const maxLength = obj.byteLength - byteOffset;

  if (maxLength < 0)
    throw new ERR_BUFFER_OUT_OF_BOUNDS('offset');

  if (length === undefined) {
    length = maxLength;
  } else {
    // Convert length to non-negative integer.
    length = +length;
    if (length > 0) {
      if (length > maxLength)
        throw new ERR_BUFFER_OUT_OF_BOUNDS('length');
    } else {
      length = 0;
    }
  }

  **return new FastBuffer(obj, byteOffset, length);**
}

return구문 전까지는 fromArrayBuffer()의 인자 중 byteOffset과 length가 안넘어왔을 때 스스로 그 값을 지정하는 부분이고, 마지막에 FastBuffer클래스의 인스턴스를 반환한다.

지금까지 쫓아온 경로를 되돌아보면

const buf1 = Buffer.from([1,2,3])

—> Buffer.from(...)은 fromArrayBuffer(...)을 return

—> fromArrayBuffer(...)는 FastBuffer 인스턴스를 return

이므로 Buffer.from()메서드는 결국 FastBuffer 인스턴스를 반환하고 그 인스턴스가 buf1에 할당된다.

FastBuffer 클래스는 아래 소스에 정의되어 있다.

(링크) FastBuffer 클래스가 정의된 소스

// internal/buffer.js: line 954

class FastBuffer extends **Uint8Array** {
  // Using an explicit constructor here is necessary to avoid relying on
  // `Array.prototype[Symbol.iterator]`, which can be mutated by users.
  // eslint-disable-next-line no-useless-constructor
  constructor(**bufferOrLength**, byteOffset, length) {
    super(**bufferOrLength**, byteOffset, length);
  }
}

위에서 new FastBuffer(obj, ...)코드의 obj가 처음 우리가 Buffer.from()메서드에 넘긴 배열이다.

FastBuffer클래스의 생성자에서는 bufferOrLength라고 부르고 있고, Uint8Array클래스의 생성자에 그대로 넘겨버린다.

정리하면

Buffer.from()에 넘긴 배열은 털끝하나 안건드리고 그대로 Uint8Array 클래스의 생성자에 전달된다.

Uint8Array객체의 요소는 8bit로 표현할 수 있는 정수 뿐이다. 따라서 0~255 사이의 숫자를 넣어야 한다.

Uint8Array 객체의 생성 과정

nodejs 공식문서에 따르면 Buffer.from()에 배열을 넘겼을 때

배열의 각 요소와 255를 앤드연산(&; 비트연산)한 결과를 버퍼에 집어넣는다고 한다.

우리는 Buffer.from()가 내부적으로 Uint8Array 객체를 생성한다고 확인했으니, 결국 Uint8Array 객체가 만들어질 때 앤드연산을 수행하는 것이다. Uint8Array 클래스가 정의된 곳은 찾지 못했지만, 얼추 아래와 같은 로직일 것이라고 추측할 수 있다.

create(array, ...) {
	let  uint8array= []
	for (let i = 0; i<array.length; i++) {
		uint8array[i] = array[i] & 255 
	}

	return uint8array
}

위 코드에서 uint8array는 Array이지만, 실제로 Uint8Array는 배열과 비슷하지만 진짜 배열은 아니다.

좀 더 정확하게 추론해보기 위해 Uint8Array에 여러가지 배열을 넘겨보면서 직접 실험해보았다.

new Uint8Array( [1, 10] ) // 1. Uint8Array(2) [ 1, 10 ]
new Uint8Array( ['A'] ) // 2. Uint8Array(1) [ 0 ]
new Uint8Array( ['10'] ) // 3. Uint8Array(1) [ 10 ] ?!?!?!?!
  1. 숫자들을 넣어주면 그대로 잘 들어간다.
  2. 문자열을 넣으면 0이 나온다.
  3. 문자열인데 숫자를 써놓은 문자열을 넣으면 숫자로 바뀌어서 들어간다?!

실험해보기 전까지는 문자를 넣으면 해당 문자에 대응하는 정수로 바꿔주지 않을까 했지만,

(2)번 실험을 통해 그건 아니라는 걸 알았다.

그럼 문자를 넣으면 그냥 다 0이 되나 싶었지만

(3)번 실험을 통해 그것도 아니라는 걸 알았다.

이 상황에서 추측할 수 있는 시나리오는 딱 하나다.

바로 배열의 원소(= item)를 Number()나 parseInt()를 이용해 정수로 바꾼 후에 255와 앤드연산을 수행한다는 것.

근데 Number()를 사용하는지 parseInt()를 사용하는지는 아직 모르니 하나 더 실험해 보았다.

숫자가 적힌 문자열을 number타입으로 바꿀 때

Number(): 숫자로 변환되지 않는 문자가 포함되어 있으면 NaN을 반환

parseInt(): 숫자로 바꿀 수 없는 문자가 포함되어 있어도 그 문자를 만나기 전까지는 숫자로 변환해서 반환

이 차이를 떠올리면 아래와 같이 실험해볼 수 있다.

new Uint8Array( ['1A'] ) // Uint8Array(1) [ 0 ]

parseInt()를 사용했다면 ‘1A’는 1이 되어 Uint8Array에도 1이 담겨야 하는데, 실제 담긴 값은 0이었다. 고로 Number()를 사용해서 number로 캐스팅한다는 결론이 나오고, 위에서 추론했던 코드를 이렇게 다시 작성할 수 있겠다.

create(array, ...) {
	let  uint8array= []
	for (let i = 0; i<array.length; i++) {
		uint8array[i] = **Number(array[i])** & 255 
	}

	return uint8array
}

255와 앤드연산을 한다는 것

Buffer(내부적으로는 Uint8Array)는 8bit(1Byte)씩 나누어 데이터를 담는다 했다. 한 자리당 1Byte로 표현되는 값만 담는다는 말이다. 1Byte는 8bit, 즉 ‘00000000’ ~ ‘11111111’의 범위를 가진다. ‘00000000’과 ‘11111111’을 정수로 바꾸면 0~255이다. 따라서, 가령 300(비트로 표현하면 100101100. 비트9개)처럼 비트 8개로 표현할 수 없는 값은 맨 뒤부터 8비트만 남기고 그 앞은 다 짤라버리고 담아야지~ 하는 것이다.

  100101100 => 300
&  11111111 => 255
-----------
   00101100 => 44

그러므로 Uint8Array에 300을 담으면 결국 거기에서 256을 뺀 44가 담긴다.

new Uint8Array([300]) // Uint8Array(1) [ 44 ]

몇 비트짜리 값을 가져와도 맨 뒤의 8비트만 취한다.

그렇다면 Uint16Array는? 16비트로 표한할 수 있는 정수를 요소로 가지기 때문에 16비트보다 긴 비트만 앞이 짤릴 것이다. 300은 9비트로 표현되기 때문에 300그대로 잘 들어간다.

0개의 댓글