들어가기 전에

자바스크립트 개발자라면 알아야 할 33가지 개념 #12 자바스크립트 비트연산 실제로 활용하기!

자바스크립트에서 비트연산자는 우리에게 이상한 야생의 세계를 소개합니다. (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"같은 거 있잖아요? 비록 파일 시스템 권한 관리는 더욱 복잡하지만, 아마 조금 스케일을 낮추면 우리 문제를 해결하는데 사용될 수 있을 것이라고 생각했습니다. 제 머릿속에서, 어떻게 각 속성에 번호를 부여해야 할지, 그리고 각 상태에 대해 유니크한 번호를 붙여줄지 머리털 한 올까지 집중력을 끌어올리기 시작했습니다. 그리고 ...

다른 동료 한명이 제 자신으로부터 저를 구해줬습니다. "파일 시스템 권한 관리를 재발명하는것 보다는, 그냥 비트 연산자를 쓰는 게 어때?" 그가 말했습니다. "그래 좋아". 저는 흐릿한 아이디어가 있었습니다. 저는 ~ 연산자를 지난 몇년간 단 몇번밖에 써보지 않았습니다. 그래서 비트 연산자에 대한 완벽한 이해가 없었습니다. 운좋게도, 이 동료 개발자는 지난 프로젝트에서 비트 연산자를 써봤고 우리의 두뇌를 깨울 수 있었습니다.

파일 시스템 권한 관리지만 제가 상상했던 것보다 훨씬 우아했습니다. 정수단위에서 계산하는 것 대신, 수동으로 작성한 덧셈, 뺄셈, 비트 연산자는 각 정수를 표현한 비트 위에서 작동했습니다. 우리에게 숫자를 직접 다루고 비교할 수 있게 만들어주었습니다. 각각 true/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진법의 규칙에 맞게 증가시킵니다. 다음 코드를 보세요.

// `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 & 1001을 하게 되면 1000이 리턴됩니다. 왜냐하면 왼쪽 끝에 있는 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링크가 있습니다.

bitwise-cheat-sheet.png


// (역자 주 : 위의 소스에서 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을 다룰 수 있게 됐다는 것입니다. 만일 여러분이 적당한 커피를 마시고 눈을 감는다면, 몇번 잠깐동안 1950년대의 기계어 코더들을 흉내낼 수 있습니다.