ES6 In Depth: 이터레이터(iterator)와 for-of 루프 구문

ES6 In Depth는 ECMAScript 표준 6번째 에디션에 의해 JavaScript 언어에 추가된 새로운 요소들을 살펴보는 시리즈입니다.

배열을 루프로 순회하기 위해 어떤 방법을 사용하나요? 20년쯤 전, JavaScript가 처음 소개됐을 당시에는 아마 다음과 같은 방법을 사용했을 것입니다.

for (var index = 0; index < myArray.length; index++) {
  console.log(myArray[index]);
}

ES5 발표 이후에는 forEach 메소드를 쓸 수 있게 되었습니다.

myArray.forEach(function (value) {
  console.log(value);
});

좀 더 간단한 방법이기는 하지만, 여기에는 사소한 단점이 있습니다. break 구문을 이용해서 루프를 중단하거나 return 구문을 이용해서 함수를 벗어날 수 없다는 점입니다.

배열을 순회하는 방법이 오직 for 루프 뿐이라면 오히려 괜찮을 것 같습니다.

그런데 forin 루프는 어떤가요?

for (var index in myArray) {    // 실제 상황에서는 사용하지 마세요
  console.log(myArray[index]);
}

forin 루프는 몇가지 이유로 나쁜 방법입니다.

  • 이 코드에서 index에 할당되는 값들은 "0", "1", "2"과 같은 문자열입니다. 숫자가 아닙니다. 분명 당신이 바라는 것은 문자열 연산 ("2" + 1 == "21")이 아닐 것이기 때문에, 이것은 아무리 봐도 불편한 방법입니다.
  • 루프 구문이 배열 요소들만을 순회하지 않습니다. 대신 누군가에 의해 추가된 확장속성(expando)들도 순회합니다. 예를 들어, 당신이 다루는 배열이 myArray.name이라는 속성을 가지고 있다면, 이 루프는 배열 요소들 말고도 index == "name" 속성을 대상으로 한번 더 실행될 것입니다. 뿐만 아니라 배열의 프로토타입 체인(prototype chain)도 순회할 것입니다.
  • 가장 당혹스러운 것은, 어떤 환경에서는 이 루프의 순회 순서가 무작위라는 점입니다.

간단히 말해, forin 구문은 일반 Object의 문자열 키(key)를 순회하기 위해 만들어진 문법입니다. Array를 다루는데는 그다지 유용하지 않습니다.

강력한 for-of 루프

기억하시나요? 지난주에 ES6로 인해 이미 작성된 기존 JS 코드가 실행되지 않는 일은 없을 거라고 말씀 드렸습니다. 그런데 현존하는 수백만개의 웹 사이트들이 forin 구문을 사용하고 있습니다. 이들은 forin 구문이 배열을 다루는 방식에도 의존적 입니다. 그래서 forin 구문이 배열을 다루는 방식을 좀 더 우아한 형태로 “고치자”는 요구는 전혀 없었습니다. ES6가 문제를 개선하기 위해 선택할 수 있었던 유일한 방법은 새로운 종류의 루프 문법을 추가하는 것 뿐이었습니다.

이것이 그 노력의 결과입니다.

for (var value of myArray) {
  console.log(value);
}

흠. 만들어진 모습이 그다지 인상적이지 않군요. 어떤가요? 우리는 forof 구문이 멋진 것을 감추고 있지 않나 살펴 볼 것입니다. 일단 지금은 다음 사실들을 알아둡시다.

  • 이 구문은 지금까지 있었던 배열 순회 방법들 중에서 문법적으로 가장 간결하고, 직접적입니다.
  • 이 구문은 forin 구문의 모든 단점들을 배제합니다.
  • forEach() 구문과 달리, break, continue, 그리고 return 구문과 함께 사용할 수 있습니다.

forin 루프 구문은 객체의 속성들을 순회하기 위한 구문입니다.

forof 루프 구문은 배열의 요소들, 즉 data를 순회하기 위한 구문입니다.

그런데 그게 전부가 아닙니다.

for-of 구문을 지원하는 또다른 컬렉션(collection)들

forof 구문은 배열만을 위한 것이 아닙니다. forof 구문은 배열과 비슷한 대부분의 객체들을 대상으로 사용할 수 있습니다. DOM NodeList 같은 객체들 말이죠.

forof 구문은 문자열도 다룰 수 있습니다. 문자열을 유니코드 문자들로 이루어진 배열로 취급합니다.

for (var chr of "") {
  alert(chr);
}

forof 구문은 MapSet 객체도 대상으로 다룰 수 있습니다.

아 미안합니다. MapSet 객체가 낯선가요? 이들은 ES6에 새로 추가된 객체들입니다. 언젠가 이들에 대해서도 따로 글을 올릴 것입니다. 만약 다른 랭귀지에서 map과 set을 사용해 본 적이 있다면 그리 어렵지 않게 이해할 수 있을 것입니다.

예를 들어 Set 객체는 중복을 제거하는데 좋습니다.

// 단어들의 배열을 이용해서 set 객체를 생성합니다
var uniqueWords = new Set(words);

일단 Set 객체를 갖게 되면, 당신은 Set 객체의 내용을 순회하고 싶을 것입니다. 간단합니다.

for (var word of uniqueWords) {
  console.log(word);
}

Map 객체는 약간 다릅니다. Map 객체 안의 데이터는 key-value 쌍으로 이루어집니다. 아마도 당신은 key와 value를 별도의 변수로 분해(destructuring) 하고 싶을 것입니다.

for (var [key, value] of phoneBookMap) {
  console.log(key + "'s phone number is: " + value);
}

분해(destructuring) 역시 ES6에 새로 도입된 멋진 기능입니다. 이에 대해서도 따로 글을 올리겠습니다.

지금까지의 내용을 정리해봅시다. JS는 이미 몇가지 컬렉션 클래스들을 지원했습니다. 그런데 더 많은 종류의 컬렉션들을 지원할 예정입니다. forof 구문은 그런 모든 종류의 컬렉션들을 대상으로 사용할 수 있습니다.

forof 구문은 일반 Object를 대상으로는 동작하지 않습니다. 만약 어떤 객체의 속성을 순회하고 싶다면 forin 구문을 이용하거나 (forin 구문이 존재하는 이유입니다), 내장된(built-in) Object.keys() 메소드를 사용합니다.

// 객체의 모든 속성을 콘솔에 출력합니다
for (var key of Object.keys(someObject)) {
  console.log(key + ": " + someObject[key]);
}

수면 아래서 일어나는 일

“훌륭한 예술가는 모방합니다. 위대한 예술가는 아예 훔치죠.” —파블로 피카소

ES6의 철학은 이유가 없다면 새로운 요소를 추가하지 않는 것입니다. ES6에 새로 추가된 대부분의 요소들은 다른 랭귀지에서 유용성이 검증된 것들입니다.

예를 들어 forof 구문은 C++, Java, C#, 그리고 Python의 루프 구문과 유사합니다. 다른 랭귀지들의 루프 구문처럼 forof 구문도 랭귀지와 표준 라이브러리가 제공하는 다양한 데이터 구조체들을 다룰 수 있습니다. 그런데 forof 구문은 랭귀지를 확장하는 수단이기도 합니다.

다른 랭귀지들의 for/foreach 구문처럼 forof 구문도 어떤 메소드를 반복 호출하는 방식으로 동작합니다. Array, Map, Set, 그리고 우리가 언급했던 모든 객체들은 공통적으로 이터레이터(iterator) 메소드를 제공합니다.

그리고 이터레이터 메소드를 제공할 수 있는 객체가 더 있습니다. 바로 당신이 원하는 모든 객체입니다.

당신이 어떤 객체에든 myObject.toString() 메소드를 추가하면 JS가 이를 통해 해당 객체를 문자열로 변환하는 방법을 알아내는 것과 마찬가지로, 어떤 객체에든 myObject[Symbol.iterator]() 메소드를 추가하면 JS는 해당 객체를 어떻게 순회(loop)해야 하는지 알아냅니다.

예를 들어, 당신이 jQuery를 사용하고 있다고 가정해봅시다. 비록 .each() 메소드를 써도 좋겠지만 forof 구문을 써서 jQuery 객체를 다뤄봅시다. 방법은 다음과 같습니다.

// jQuery 객체는 배열(array)과 유사하므로,
// Array와 똑같은 이터레이터 메소드를 제공합니다
jQuery.prototype[Symbol.iterator] =
  Array.prototype[Symbol.iterator];

좋습니다. 당신이 무슨 생각을 하는지 알 것 같습니다. [Symbol.iterator] 구문의 문법이 이상하다고 생각되지 않나요? 이 구문은 어떤 뜻일까요? 이 구문에서 중요한 것은 메소드의 이름입니다. 표준화 위원회가 이 메소드를 .iterator() 메소드라고 명명해도 좋았을 것입니다. 하지만 기존에 작성된 코드의 어떤 객체가 이미 .iterator()라는 메소드를 갖고 있다면 문제가 무척 복잡해집니다. 그래서 표준화 위원회는 평범한 문자열로 메소드 이름을 정하는 대신 symbol을 사용하기로 결정했습니다.

Symbol도 ES6에 새로 도입된 개념입니다. Symbol에 대해서도 (이미 예상했겠지만) 따로 글을 올리겠습니다. 지금 당장 알아야 할 것은 ES 표준이 Symbol.iterator 같은 방식으로 기존의 어떤 코드와도 충돌하지 않는 새로운 심볼을 정의할 수 있다는 사실입니다. 그 대가(trade-off)는 문법이 다소 이상해보이는 것입니다. 하지만 이토록 유용한 기능을 완벽한 하위호환성과 함께 제공하는 장점에 비하면 아주 사소한 대가입니다.

[Symbol.iterator]() 메소드를 제공하는 객체를 이터러블 객체 (iterable object)라고 부릅니다. 다음 주에 우리는 이터러블 객체가 랭귀지 전반에 걸쳐 사용된다는 사실을 살펴볼 것입니다. 이터러블 객체는 forof 구문 뿐 아니라 MapSet의 생성자, 분해 할당 (destructuring assignment), 그리고 새로운 spread 연산자에서도 사용됩니다.

이터레이터 객체 (iterator object)

아마, 이터레이터 객체를 바닥부터 직접 구현해야 하는 경우는 없을 것입니다. 그 이유는 다음주에 살펴볼 예정입니다. 지금은 이터레이터 객체가 어떻게 생겼는지 살펴보는 것으로 충분합니다. (이 부분을 건너뛰어도 좋지만 그러면 재미있는 기술적 세부 사항을 놓치게 됩니다.)

forof 루프는 컬렉션(collection)에 있는 [Symbol.iterator]() 메소드 호출로 시작합니다. 이 메소드는 새로운 이터레이터 객체를 리턴합니다. .next() 메소드를 제공하는 객체는 모두 이터레이터 객체입니다. forof 루프는 .next() 메소드를 반복적으로 호출하면서 컬렉션에 포함된 객체들을 순회합니다. 여기 제가 생각할 수 있는 가장 간단한 이터레이터 객체가 있습니다.

var zeroesForeverIterator = {
  [Symbol.iterator]: function () {
    return this;
  },
  next: function () {
    return {done: false, value: 0};
  }
};

여기 정의된 .next() 메소드는 호출될 때마다 같은 결과를 리턴합니다. 이 리턴값은 forof 루프에게 (a) 순회(iterating)가 아직 끝나지 않았으며, (b) 다음 값(next value)은 0 (제로)라고 알려줍니다. 이는 for (value of zeroesForeverIterator) {} 가 무한 루프임을 의미합니다. 물론 실제 이터레이터는 이렇게 허술하지 않겠지요.

.done 그리고 .value 속성을 갖는 이런 형태의 이터레이터가 표면적으로는 다른 랭귀지의 이터레이터와 달라 보일 것입니다. Java의 이터레이터는 분리된 .hasNext() 메소드와 .next() 메소드를 제공합니다. Python의 이터레이터는 하나의 .next() 메소드를 제공하는 대신 더이상 순회할 값이 없을 때 StopIteration 예외를 던집니다. 하지만 본질적으로는 이 세가지 형태의 이터레이터가 모두 같은 정보를 리턴하는 것입니다.

이터레이터 객체는 경우에 따라 .return() 메소드와 .throw(exc) 메소드를 제공할 수도 있습니다. forof 루프는 예외(exception)나, break 구문, return 구문으로 인해 조건보다 일찍 루프를 벗어날 때 .return() 메소드를 호출합니다. 이터레이터가 사용하던 자원을 해지해야 할 필요가 있다면 .return() 메소드에서 정리합니다. 대부분의 이터레이터 객체는 .return() 메소드를 구현할 필요가 없을 것입니다. .throw(exc) 메소드는 이보다 더 특별한 케이스입니다. forof 구문은 이 메소드를 전혀 호출하지 않습니다. 다음주에 이 메소드에 대해 설명하겠습니다.

이제 모든 세부 사항들을 알게 됐습니다. 이제 우리는 간단한 forof 구문을 보고 이를 루프 실행시 수면 아래에서 일어나는 메소드 호출 형태로 재작성할 수 있습니다.

우선 여기 forof 루프 구문이 있습니다.

for (VAR of ITERABLE) {
  STATEMENTS
}

그리고 여기 수면 아래에서 일어나는 메소드 호출과 임시 변수를 사용해서 재작성한 대략 동등한 코드가 있습니다.

var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
  VAR = $result.value;
  STATEMENTS
  $result = $iterator.next();
}

이 코드는 .return() 구문의 동작 양식을 보여주지 않습니다. .return() 구문의 동작 양식도 추가할 수 있었지만 코드만 복잡해질 것 같아 제외시켰습니다. forof 구문은 사용하기 쉽습니다. 하지만 그 이면에 아주 많은 것들이 담겨있습니다.

언제부터 쓸 수 있나요?

현재 출시된 Firefox의 모든 버전이 forof 루프 구문을 지원합니다. Chrome에서도 chrome://flags 페이지에서 “실험용 자바스크립트 사용 (Experimental JavaScript)” 옵션을 켜면 사용할 수 있습니다. 마이크로소프트의 Spartan 브라우저에서도 사용할 수 있습니다. 하지만 IE에서는 사용할 수 없습니다. 새로운 문법을 웹에서 사용하고 싶지만 IE와 Safari를 지원해야 하는 상황이라면, Babel 또는 구글 Traceur 같은 컴파일러를 써서 ES6 코드를 웹-친화적인 ES5 코드로 변환시켜 사용하면 됩니다.

서버에서는 컴파일러를 사용할 필요가 없습니다. forof 구문은 io.js (그리고 --harmony 옵션을 켠 Node)에서 지금 당장 사용할 수 있습니다.

(UPDATE: 글을 쓸 당시 Chrome이 디폴트 상태에서 forof 구문을 지원하지 않는다는 사실을 잊고 있었습니다. Oleg이 커멘트를 통해 해당 사실을 지적해준 덕분에 내용을 정정할 수 있었습니다.)

{done: true}

휴!

예, 오늘은 여기까지 입니다. 하지만 아직 forof 루프 구문에 대해 못다한 얘기가 남아 있습니다.

ES6에는 forof 구문과 어울려 멋지게 동작하는 새로운 종류의 객체가 하나 더 있습니다. 오늘 이 객체를 언급하지 않은 것은 다음주에 다룰 주제이기 때문입니다. 개인적으로 이 객체가 ES6의 가장 신비한 요소라고 생각합니다. 만약 당신이 Python 이나 C# 같은 언어에서 이 객체의 개념을 접해보지 못했다면, 처음엔 무척 이상해 보일 것입니다. 하지만 이 개념은 이터레이터를 작성하는 가장 쉬운 방법입니다. 그리고 유용한 리팩토링 수단입니다. 또 브라우저에서든 서버에서든 비동기적 코드를 작성하는 방식 자체를 바꿀 개념입니다. 그러니 다음 주에 저와 함께 ES6 제너레이터(generator)를 살펴봅시다.

이 글은 이 쓴 ES6 In Depth: Iterators and the for-of loop의 한국어 번역본입니다.

작성자: ingeeKim

"누구에게나 평등하고 자유로운 웹"에 공감하는 직장인.

ingeeKim가 작성한 문서들…


11 댓글

  1. 전현제

    번역 감사드립니다.

    8월 23rd, 2015 at 3:32 오후
  2. ingeeKim

    응원 감사합니다. ^^

    8월 23rd, 2015 at 6:56 오후
  3. HJ.Park

    잘 보고 갑니다. =)

    9월 2nd, 2015 at 2:33 오후
  4. ingeeKim

    @HJ.Park 댓글 감사합니다. ^^

    9월 2nd, 2015 at 6:17 오후
  5. cheolho

    감사합니다!

    2월 24th, 2016 at 6:05 오후
  6. HyunSeob

    이렇게 좋은 게 있는지 너무 늦게 알았네요. 좋은 번역 감사드립니다.

    5월 1st, 2016 at 10:28 오후
  7. ingeeKim

    @cheolho @HyunSeob 감사합니다. 정성을 다했습니다. ^^;;;

    5월 2nd, 2016 at 7:29 오후
  8. 김현석

    좋은 글 번역해주셔서 정말 감사드려요. ‘ㅅ’)b

    6월 29th, 2016 at 4:07 오후
  9. ingeeKim

    모질라 핵스에 자주 들려주세요. 열심히 노력하고 있습니다. ^^

    7월 1st, 2016 at 2:10 오후
  10. jejette

    잘보았습니다.
    감사합니다 🙂 공부하는데 많은 도움이 될것 같아요 <3

    12월 19th, 2016 at 5:55 오후
  11. ingeeKim

    Mozilla 프로젝트에 많은 관심과 참여 부탁 드립니다. ^^

    12월 26th, 2016 at 7:37 오후

댓글 쓰기