forEach, for in, for of 특징 및 성능 비교

한 번은 정리하고 싶던 글이였습니다. 배열, 객체, map, set에 대해 위에 쓴 forEach, for in, for of가 어떻게 동작하는지 살펴보겠습니다.

map, set?

자바스크립트의 배열과 객체는 이미 알고 있을거라 생각합니다. map과 set은 ES 6에서 추가 된 자료구조입니다. map은 key-value 형태로 key는 중복되지 않는 특징을 가집니다.

let map = new Map();
map.set('name', 'seo');
map.set('age', 28);
map.set('age', 29);
map.set('age', 30);
console.log(map) // Map { 'name' => 'seo', 'age' => 30 }

결과를 보면 age 값을 3번 set 했는데 가장 마지막에 set한 값만 유지하는 걸 알 수 있습니다.

set은 값의 집합인데, 배열과 다른 점은 중복된 값을 가지지 않습니다.

let setA = new Set();
setA.add(1)
setA.add(2)
setA.add(3)
setA.add(1)
setA.add(1)
console.log(setA) // Set { 1, 2, 3 }

예제로 사용할 데이터들을 아래에 정리하겠습니다. for 관련 loop들을 테스트해 볼 배열, 객체, map, set들의 데이터입니다.

let arr = [1,2,3,4,5]; // 배열
let obj = { “k1” : 1, “k2” : 2, “k3”:3 } // 객체
let map = new Map(); // map
map.set(‘name’, ‘seo’);
map.set(‘age’, 29);
let setA = new Set(); // set
setA.add(1)
setA.add(2)
setA.add(3)
forEach 결과

우선 실행한 코드와 결과를 보겠습니다.

/////// Array forEach //////////
arr.forEach( v => { console.log( v )}) // 1,2,3,4,5
//obj.forEach( v=> { console.log(v )})  // 에러
/////// Map forEach //////////
mapA.forEach( v => { console.log(v)}) // seo, 29
/////// Set forEach //////////
setA.forEach( v => { console.log(v)}) // 1,2,3

위의 결과를 보면 Object를 제외한 Array, Map, Set 에서 forEach 메소드가 구현된 것을 확인할 수 있습니다. 출력결과를 보면 모두 값이 나온 것을 확인할 수 있습니다. forEach의 경우 callback의 인자에 따라 index가 추가 될 수 있습니다. 변경 해보겠습니다.

function keyValueElement(value, key) {
console.log(`${key} = ${value}`);
}
console.log(‘=====Array=====’)
arr.forEach( keyValueElement )
console.log(‘=====Map=====’)
mapA.forEach( keyValueElement )
console.log(‘=====Set=====’)
setA.forEach( keyValueElement )

함수 keyValueElement는 forEach의 인자로 전달되는 callback 함수로, value와 key를 받게 됩니다. 결과는 아래와 같습니다.

특이한 점은, Set의 경우 value와 key 구분 없이 모두 value 값으로 들어오는 것을 확인할 수 있습니다.

for in 결과

우선 실행한 코드와 결과를 보겠습니다. 현재 typescript에서 실행중이라 for 문안에 const 키워드가 있는데, typescript에 익숙하지 않으신 분들은 그냥 없다고 생각하셔도 됩니다.

console.log(‘=====Array=====’)
for (const i in arr) { console.log(i) }
console.log(‘=====Object=====’)
for (const i in obj) { console.log(i) }
console.log(‘=====Map=====’)
for (const i in mapA) { console.log(i) }
console.log(‘=====Set=====’)
for (const i in setA) { console.log(i) }

위 코드에 대한 결과입니다.

위의 결과를 보면 Array, Object의 key는 정상적으로 나왔습니다. 헌데 Map, Set은 아무 결과도 나오지 않았습니다. 이는 for in 구문이 내부적으로 객체의Enumerable(열거가능한) 속성이 true로 설정된 속성들만 접근가능하기 때문입니다. 크롬의 개발자 도구에서 확인해보겠습니다.

위에 배열 arr를 정의하고 Object의 propertyIsEnumerable을 호출하여 해당 키의 Enumerable 속성이 무엇인지 확인할 수 있습니다. key 0,1,2에 대해서는 true지만 3번째 인덱스 부터는 false가 나오는 것을 확인할 수 있습니다. arr 배열에 3개의 데이터만 들어가 있으므로 인덱스가 2까지 존재하기 때문입니다.

그렇다면 Map과 Set은 어떤 결과가 나오는지 보겠습니다.

크롬에서 Map과 Set을 정의하고 데이터를 추가한 뒤 동일하게 함수를 호출해보면 존재하는 key에 대한 결과가 false인 것을 알 수 있습니다. 정리해보면 자바스크립트의 for in 구문은 객체 속성이 가진 Enumerable 속성이 true 인 것을 보여주지만 Map과 Set은 false이기 때문에 나오지 않습니다.

for of 결과

우선 코드와 결과를 보겠습니다.

console.log(‘=====Array=====’)
for (const i of arr) { console.log(i) }
console.log(‘=====Object=====’)
//for (const i of obj) { console.log(i) } // 에러 발생
console.log(‘=====Map=====’)
for (const i of mapA) { console.log(i) }
console.log(‘=====Set=====’)
for (const i of setA) { console.log(i) }

위의 for in에서 in 구문을 of 로 바꾼것 말곤 차이가 없습니다. 결과는

위와 같습니다. 살펴보면 Array와 Set은 값을 출력했고 Map의 경우는 [key, value]를 출력했습니다. Object의 경우는 에러가 나서 주석처리를 했습니다.

for in 을 다시 되돌아보면 객체의 Enumerable 속성을 따진다고 했습니다. for of 문의 경우는 컬렉션 전용입니다. 객체 기준이라기 보다는 [Symbol.iterator] 속성이 있는 모든 컬렉션 요소에 대해 반복합니다.

Chrome에서 배열을 출력해서 속성을 보면 아래와 같이 Symbol.iterator를 확인할 수 있습니다.

node 소스에서 아래와 같이 코드를 추가했습니다.

console.log(arr[Symbol.iterator])
console.log(obj[Symbol.iterator])
console.log(mapA[Symbol.iterator])
console.log(setA[Symbol.iterator])

결과를 보면 아래와 같습니다.

[Function: values] // 배열
undefined // 객체
[Function: entries]// Map
[Function: values] // Set

아까 for of로 출력된 결과를 보면 배열은 값이 나왔고 객체는 에러가 발생해서 주석 처리를 하였고, Map은 Key가 나왔고 Set은 값이 나왔습니다. 위에 결과랑 연관이 있어보입니다. 객체는 undefined라 에러가 발생하였고 Map에서 entries는 key, value 한 쌍에 접근하는 것 같습니다.

Chrome에서 각 Symbol.iterator가 뭘 가리키는지 찍어봅시다.

배열, Map, Set의 Symbol.iterator가 가리키는 함수들을 실행해보니 각각의 iterator를 반환합니다. 이제 내부에서 iterator가 요소들에 하나씩 접근하여 출력할 겁니다.

이제 저한테 남은 마지막 궁금점이 있습니다. for in 구문은 내부의 Enumerable 속성에 따라 값을 출력한다고 했습니다. for of 의 경우는 내부에 Symbol.iterator가 존재할 때, iterator가 무엇을 기준으로 루프를 도느냐 입니다. 가령 모든 속성에 접근하나?

이건 Symbol.iterator가 가리키는 함수에 따라 다른 것 같습니다. 가령 배열의 Symbol.iterator가 가리키는 Array.prototype.values()의 경우 배열의 각 인덱스에 대한 값을 가지는 새로운 Array iteraotr 객체를 반환합니다. 따라서 아래 for in의 출력결과랑은 차이가 있는 것 같습니다.

let arr = [1,2,3,4,5];
arr[“test”] = 1;
for (const i in arr) { console.log(i)} // 0,1,2,3,4,test
console.log(‘ — — — — — — — — — — — — ‘)
for (const i of arr) { console.log(i)} // 1,2,3,4,5

이제 궁금한 것, 성능이 어찌될까요. 음 이건 개인적으로 테스트하기 보단, 이미 저보다 똑똑한 사람들이 테스트한 결과를 믿는게 나을 것 같습니다.

아래 결과는 여기(http://jsben.ch/BQhED)에서 참고했습니다. 이 하나뿐만 아니라 여러 글들을 찾아보면 for in, for of, map, reduce 같은 방법보다 클래식한 for loop가 가장 빠른 성능을 보여주고 있습니다… 음…. 어… 앞으로 일반 for문 써야 할까봐요…