3 분 소요

ESLint와 Airbnb JavaScript 스타일 가이드를 사용해 프로젝트를 진행하던 도중, for… of 문을 사용할 시 아래와 같이 경고가 발생하는 것을 확인할 수 있었다.

for (let winningNumber of winningNumberInput) {
  if (winningNumber !== something) {
	// ...
  }
}

iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations.

대체 for… of문에 어떤 문제가 있기에 이렇게 별도의 규칙까지 만들어 사용을 막는 것일까? 위 경고문을 분석하며 그 이유를 찾아 보았다.

iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them…

이 부분은 완벽하게 이해하지 못 했다. 추후 이해가 깊어지면 내용을 추가하겠다.

먼저 경고문은 iterator/generator가 ‘too heavywieght’ 하다고 말한다. 간단히 말해, for… of의 동작을 확실하게 이해하기가 어렵다는 뜻이다.

const makeEven = [];
for (const val of arr) {
  makeEven.push(val % 2 ? val : val + 1);
}

위와 같은 간단해 보이는 코드가, 브라우저 내부에서는 아래와 같이 처리된다.

var makeEven = [];
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
  for (var _iterator = arr[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
    var val = _step.value;
makeEven.push(val % 2 ? val : val + 1);
  }
} catch (err) {
  _didIteratorError = true;
  _iteratorError = err;
} finally {
  try {
    if (!_iteratorNormalCompletion && _iterator.return) {
      _iterator.return();
    }
  } finally {
    if (_didIteratorError) {
      throw _iteratorError;
    }
  }
}

실제로 pure for… of는 transpiled map보다 ~50% 빠르지만, transpiled for… of는 transpiled map보다 ~70% 느리다고 한다. 대부분의 프로젝트에서는 transpiled bundle을 사용하므로, map을 사용하는 편이 더 효율적이라고 할 수 있겠다.

또한 for… of는 Symbol을 지원하지 않는 브라우저에서는 polyfill하지 않는 이상 사용할 수 없다는 문제가 있다.

…Loops should be avoided in favor of array iterations.

다음으로 경고문은 loop 대신 map, reduce 를 비롯한 array iteration 함수들을 사용하라 권장한다. 왜일까?

Why? This enforces our immutable rule. Dealing with pure functions that return values is easier to reason about than side effects.

Airbnb 스타일 가이드는 array iteration 대신 loop을 사용하면 예상치 못한 결과, 즉 버그를 만들 가능성이 높아진다고 말한다. 예제를 통해서 그 이유를 살펴보자.

for (let i = 0; i < arr.length; i++) {
  if (arr[i] % 2) {
    arr[i] += 1;
  }
}

위와 동일한 기능을 하는 코드를 for… of문을 사용해 작성해 보자. 만약 for…of 문에 익숙하지 않거나, 피곤에 절어 있는 사람이라면 아래와 같이 잘못된 코드를 작성할 가능성이 꽤 크다.

for (const i of arr) {
  if (arr[i] % 2) {
	arr[i] += 1;
  }
}

arr이 [1, 2, 3] 일 때, 위 코드를 실행하면 어떤 결과가 나올까?

아마 for… of를 사용해 보지 않은 사람이라면 arr이 [2, 2, 4]로 바뀌리라 예측했을 것이다. for… of가 어떻게 작동하는 지 아는 사람이라 해도, 실제로 코드를 실행시켜 보기 전까지는 arr = [1, 2, 4]라는 결과가 나온다는 사실을 파악하기 쉽지 않다.

또한 위 코드는 arr을 직접적으로 변경하고 있는데, 이러한 행동은 예상치 못한 결과를 만들기 쉽다. 위 코드의 문제를 해결하고 의도한 대로 동작하도록 하려면 아래와 같이 바꾸어야 한다.

const makeEven = [];
for (const val of arr) {
  makeEven.push(val % 2 ? val : val + 1);
}

그런데 이 코드 역시 깔끔하지는 않다. 위 코드를 함수 형태로 사용한다고 가정해 보자.

function makeEvenArr(arr) {
  const makeEven = [];
  for (const val of arr) {
	makeEven.push(val % 2 ? val : val + 1);
  }
  return makeEven;
}

makeEven이란 추가적인 변수를 사용해야만 정상적으로 원하는 결과를 반환할 수 있다. map을 사용한다면 변수를 추가적으로 사용할 필요도 없고, 코드 길이도 줄어든다.

function makeEven(arr) {
  return arr.map((val) => val % 2 ? val : val + 1);
}

for 대신 array iteration 함수를 사용할 시 불필요한 오류를 예방할 수 있으며, 코드 역시 좀 더 깔끔해지는 경향이 있다고 정리할 수 있겠다.

속도는 어느 쪽이 빠를까?

참고:

  • https://medium.com/@paul.beynon/thanks-for-taking-the-time-to-write-the-article-i-enjoyed-it-db916026647

댓글남기기