자바스크립트 비트연산 실제로 활용하기!

EJ·2020년 12월 18일
0

JavaScript 개념

목록 보기
12/13

자바스크립트에서 비트연산자는 (12 & 3) = 0이 되고 (12 & 4) = 4가 되게 한다. 때때로 비트연산자는 우리가 어떻게 해결해야 할지 확신이 없는 문제들을 해결해주는 해결사가 될 수 있다.

오브젝트 안에 있는 4개의 독립적인 true/false 변수들의 존재를 체크하고 저장하는 가장 좋은 방법이 무엇일까?
이 속성들을 foo1부터 foo4라고 불러보자. JavaScript(ES6)에서의 표현법은 다음과 같을 것이다.

const myObject = {
  foo1: false,
  foo2: true,
  foo3: false,
  foo4: true
};

상당히 직관적이다. 하지만 어플리케이션은 이 속성들의 아주 많은 조합을 체크해야할 필요가 있다. 어렵기도 하고, 언젠가는 하나의 추가적인 속성을 더 추가해야 할 수도 있다. 이 문제를 해결하기 위해서 두 가지 가장 확실한 옵션이 있다.

1) 모든 가능한 모델 오브젝트 만들기, 그리고 필요할 때마다 코드를 비교하기

const hasFoo2andFoo4 = {
  foo1: false,
  foo2: true,
  foo3: false,
  foo4: true
}

const hasFoo3andFoo4 = {
  foo1: false,
  foo2: false,
  foo3: true,
  foo4: true
}

// ... 나머지 경우의 수 ...

// 그 후에
if (isEqual(myObject, hasFoo2andFoo4)) {
  // 오브젝트가 Foo2와 Foo4만 가지고 있는지 알 수 있습니다.
}

위 코드는 좋지 않은 방법이다. 비교하기 위해서 16개의 모델 오브젝트를 생성해야 할 것이다. 이러한 작업은 작은 정보를 얻기 위한 overhead가 너무나 크다. 게다가, 나중에 또다른 속성을 추가할 경우 모델 오브젝트를 두 배로 늘려야 할 것이다. 이런 방식은 피해야 한다.

2) 조건 블록 내에서 각각 개별 프로퍼티 체크하기

if (myObject[2] && myObject[4] && !(myObject[1] || myObject[3])) {
  // 우린 오브젝트가 Foo2와 Foo4만 갖고 있다는 것을 알 수 있습니다.
}

위 코드는 생생한 악몽과 같다. 클라이언트 사이드 코드에 약 백만개의 문장을 추가해야 할 것이다. 이 방법은 처음부터 오류가 발생하기 쉬운 방법이다. 그리고 후에 어떤 속성이 바뀌거나 새로운 속성이 추가됐을 때, 엄청난 작업이 필요할 것이다. 그렇다면 어떻게 해야 할까?

이 글의 원작자는 우리가 하는 일이 기본적으로 유닉스 파일 시스템에 쓰이는 권한 관리 비트마스킹 함수를 따라하고 있다는 것을 깨달았다. 이를테면, "755"는 읽기-쓰기-실행이고, "rwxr-xr-x"같은 것 말이다. 비록 파일 시스템 권한 관리는 더욱 복잡하지만, 아마 조금 스케일을 낮추면 문제를 해결하는데 사용될 수 있을 것이라고 생각했다. 그리고 원작자는 어떻게 각 속성에 번호를 부여할지, 각 상태에 대해 유니크한 번호를 붙여줄지를 고민하기 시작했다.

...

동료 한 명이 원작자를 구해줬다. "파일 시스템 권한 관리를 재발명하는 것보다는, 그냥 비트 연산자를 쓰는게 어때?"라고 동료가 제안한 것이다.

파일 시스템 권한 관리지만 훨씬 우아했다. 정수 단위에서 계산하는 것 대신, 수동으로 작성한 덧셈, 뺄셈, 비트 연산자는 각 정수를 표현한 비트 위에서 작동했다. 숫자를 직접 다루고 비교할 수 있게 만들어준 것이다.
각각 ture/false를 나타내는 속성들인 숫자의 0과 1에 따라서 4비트(혹은 3비트나 12비트 어떤 비트든)의 숫자들을 다루기 위해 그것들을 사용할 수 있는 것이다.

자바스크립트 내부의 모든 정수들(64bit/9,007,199,254,740,991까지의 수)은 2진법으로 표기될 수 있다. toString(2)를 호출함으로써 그들이 어떻게 변하는지 보자.

(1).toStirng(2);
// 1

(2).toString(2);
// 10

(3).toString(2);
// 11

(4).toString(2);
// 100

// ...

(3877494).toString(2);
// 1110110010101001110110

이 방법에 숨어있는 진짜 트릭은 비트연산은 이 바이너리 문자열들을 직접 다루고 비교할 수 있게 해준다는 것이다. 🔜바이너리 문자열 오른쪽에 0을 넣어주는 << 비트연산은 10진법 정수를 2진법의 규칙에 맞게 증가시킨다. 아래 코드를 살펴보자.

🔜 바이너리(Binaries)
http://elixir-ko.github.io/getting_started/6.html

// `fooBar`를 숫자 2로 셋팅해보자.

fooBar.toString(2);
// 10 <- 2의 2진법 표기다.

// fooBar의 바이너리 값의 끝에 0을 삽입할 것이다.
// 표기법은 다음과 같다.
foobar = fooBar << 1;

fooBar.toString(2);
// 100

// ... 이제 4가 됐다.
console.log(fooBar);
// 4

비트연산 전반을 고려해서, 이제 이진법에서 더하거나 빼거나 비교할 수 있다. 위 상세한 예제에서, 단일 4비트 숫자 내부에 4개의 가능한 속성을 저장할 수 있다. 0000-1111사이에 말이다. 각각의 비트는 true일 때 (1)로, false일 때 (0)으로 표기될 수 있다.

이러한 규칙을 이용해서 1111은 4개의 모든 속성의 true인 것이라는 것을 쉽게 상상할 수 있다. 1000은 오직 네 번째 값만 true인 것을 의미한다.
(바이너리 카운트는 오른쪽에서 왼쪽으로 간다는 것을 명심하자. 첫 번째 속성이 true인 경우 1이나 0001이 된다. 네 번째 속성이 1인 경우는 1000이다.)

비트연산 비교에서 가장 중요한 것은 "&"와 "|"이다. "&&"와 "||" 형태가 매우 닮은 것은 의도적인 것이다. "&"은 비교하는 두 개의 숫자가 교집합임을 표기하는 것이다. "|"은 합집합을 의미한다.
1010 & 10011000이 리턴된다. 왼쪽 끝에 있는 1이 유일하게 공통된 1이기 때문이다.
1010 | 10011011을 반환할 것이다. OR연산은 둘 중 하나만 있어도 값이 성립하기 때문이다.

예제를 살펴보자.

// 체크할 오브젝트를 정의해보자. API 결과나, 유저와 상호작용할 때나, form 형식에서 불러질 수 있다. 
// 아마 사전엔 알 수 없는 값일 것이다.
const myObject = {
  foo1: false,
  foo2: true,
  foo3: false,
  foo4: true
}

// 코드를 더욱 이해하기 쉽게 만들 수 있는 상수를 정의하자.
// 이 상수들은 많은 방법으로 작성될 수 있다.
// 하지만 이 방법이 직관적으로 이해하기 가장 좋은 방법이라고 원작자는 생각했다.
const HAS_FOO1 = 1;       // 0001
const HAS_FOO2 = 1 << 1;  // 0010
const HAS_FOO3 = 1 << 2;  // 0100
const HAS_FOO4 = 1 << 3;  // 1000

// 비트연산 숫자를 만들어라. 아마 use-case에 따라 다르게 만들어질 것이다.
// 하지만 해야 할 일은 같다. 
// 오브젝트 키를 수동으로 체크하고 if문을 사용하여 한 번에 하나씩 속성을 추가하는 것이다.
let myBitNumber = 0;

if (myObject['foo1'] === true)
  myBitNumber = myBitNumber | HAS_FOO1;
  // 합집합의 형태를 띄기 위해 bit연산자인 "|"를 사용한다.

if (myObject['foo2'] === true)
  myBitNumber = myBitNumber | HAS_FOO2;

if (myObject['foo3'] === true)
  myBitNumber = myBitNumber | HAS_FOO3;

if (myObject['foo4'] === true)
  myBitNumber = myBitNumber | HAS_FOO4;

console.log(myBitNumber.toString(2));
// 1010

/*
 * 비트연산 숫자는 이제 "1010"이라는 값을 가진다. 
 * 왜냐하면 두번째 값과 네번째 값이 true이기 때문이다.
 * 이렇게 표현할 수도 있다:
 *
 * | fourth | third | second | first | <= Attribute
 * |    1   |   0   |   1    |   0   | <= True/false
 *
 */

이제 테스트 해보자. 속성에 대해 비트연산 숫자를 체크하고 있다면, 체크할 수 있는 상태가 4가지가 있다. 숫자가 하나의 명확한 속성을 가지고 있든 아니든, 어떤 주어진 속성의 배열을 가지고 있든 아니든, 명시된 속성들만 가지고 있든 아니든 혹은 속성의 배열을 전부 가지고 있든 아니든말이다.

아래는 큰 도움이 되는 bitwise cheat-sheet이다.

// (역자 주 : 위의 소스에서 myBitNumber는 1010이 나오니 참고하자.)

// 비트 숫자가 하나의 소속성만 가지고 있는지 테스트해보자. 
// &는 두 숫자 사이의 교집합을 보증한다.
if (myBitNumber & HAS_FOO1) {
  // False, 이 예제에서는 False이다.
}
if (myBitNumber & HAS_FOO2) {
  // True!
}

// Test whether your bit number has ANY of the specified attributes
if (myBitNumber & (HAS_FOO1 | HAS_FOO2)) {
  // True! (역자 주: 1010 & 0011 의 결과로 0010이 나와서 true이다.)
}
if (myBitNumber & (HAS_FOO1 | HAS_FOO3)) {
  // False
}

// 오직 명시된 속성만을 가지고 있는지 테스트해보자.
if (myBitNumber == (HAS_FOO2 | HAS_FOO4)) {
  // True
}
if (myBitNumber == (HAS_FOO2 | HAS_FOO3 | HAS_FOO4)) {
  // False
}

// 모든 주어진 속성을 포함하는지 확인해보자.
// 살짝 헷갈릴 수 있다. 속성의 합집합은 혼자서 `myBitNumber`를 대체할 수 없다. 
// 다만, `myBitNumber` 가 가지지 못한 비트를 가지고 있다.
if (myBitNumber == (myBitNumber | (HAS_FOO2 | HAS_FOO4))) {
  // True
}
if (myBitNumber == (myBitNumber | (HAS_FOO2 | HAS_FOO3 | HAS_FOO4))) {
  // False
}

앞의 소스는 여러 true/false 속성을 효율적으로 저장하고 비교하기 위해 비트연산자들을 활용한 함수의 예제이다. 꽤 읽기 쉽고, 이해하기 쉬우며 업데이트하고 유지보수하기 간단하다. 그리고 하나의 문장을 수정할 필요가 있을 때 또는 또다른 속성을 추가할 필요가 있을 때 이전에 소개했던 방법들처럼 엄청나게 어려워지지 않을 것이다.

최고인 부분은 0과 1을 다룰 수 있게 된다는 것이다.


profile
주니어 프론트엔드 개발자 👼🏻

0개의 댓글