자바스크립트를 배우면서 for
문을 배우고 자연스레 for ...in
과 for ...of
도 배웠다.
당시 for ...in
과 for ...of
의 차이는 단순히 객체 순환과 배열 순환 할 때의 차이라고 알고 있었다.
for ...of
의 동작 원리가 ES6에서 새롭게 추가된 데이터 타입인 Symbol
과 밀접한 관계가 있다는 것을 알게 되면서 Symbol
에 대해 더 자세히 알아 보고 싶었다.
Symbol
은 ES6에서 새롭게 추가된 원시 타입의 값이다.
원시 타입의 값들로는 Number
, String
, undefined
등이 있다.
각각 이름만 보고서도 어떤 유형인지 가늠이 가지만, Symbol
은 잘 가늠이 가지 않는다.
Symbol
의 가장 큰 특징은 다른 값과 절대 중복되지 않는 유일무이한 값이다.
따라서 이러한 특성을 통해 이름의 충돌 위험이 없는 유일한 프로퍼티 키의 용도로 사용된다.
다음과 같이 Symbol
값을 생성해보자.
const Symbol1 = Symbol.for('name')
Symbol
값은 다른 값과 절대 중복되지 않는 유일무이한 값이다.
따라서 Symbol
값을 생성하기에 앞서 기존에 같은 이름으로 생성되어 있는지 확인하고 생성할 필요가 있다.
이를 위한 메서드가 Symbol.for
메서드이다.
위의 코드의 예시를 보자.
name
이라는 키로 저장된 Symbol
값이 있는지 탐색하고 없다면 생성, 있다면 해당 값을 반환한다.
const Symbol1 = Symbol.for('name')
const Symbol2 = Symbol.for('name')
console.log(Symbol1 === Symbol2) // true
기존에 필자는 객체의 프로퍼티 키를 동적으로 생성하고 싶었을 때 다음과 같이 코드를 작성한 적이 있다.
let tasks = {};
const ID = Date.now().toString();
const newTaskObject = {
[ID]: {id: ID, task: '' ,complete: false}
};
tasks = {...tasks, ...newTaskObject};
Date
함수를 통해 매번 새로운 id
을 생성해 키로 사용한 적이 있다.
Symbol
값은 절대 중복되지 않는 값을 보장해 주니 다음과 같이 Symbol
값을 통해 더욱 견고한 객체의 프로퍼티 키을 생성할 수 있다.
let tasks = {};
const ID = Date.now().toString();
const newTaskObject = {
[Symbol.for(ID)]: {id: ID, task: '' ,complete: false}
};
tasks = {...tasks, ...newTaskObject};
객체는 일반적으로 for ...in
문을 통해 순환이 가능하다.
하지만 Symbol
값을 통해 생성한 객체 프로퍼티 키는 for ...in
문으로 순환이 불가능하다.
정확히 말하면 순환이 불가능한 것이 아니라, Symbol
값을 찾지 못한다.
다음 예제는 위에서 생성한 예제 코드에서 출력해보았다.
for (const key in tasks) {
console.log(key) // undefined
}
따라서 Symbol
값으로 생성한 객체 프로퍼티 키는 외부로부터 은닉이 가능하다.
이터러블은 이터레이터를 리턴하는 [Symbol.iterator]()
를 가진 값이다.
이터레이터는 { value, done }
을 리턴하는 next()
를 가진 값이다.
이터러블/이터레이터 프로토콜은 이터러블을 for ...of
, 전개 연산자 등과 함께 동작하도록 한 규약을 뜻한다.
사실 정의는 위와 같지만,
이터러블 프로토콜을 따르는 for ...of
문의 동작 원리를 바탕으로 이해하면 더욱 쉽다.
ES5 까지 배열을 순회 할 때 다음과 같이 for
문을 통해 순회가 가능했다.
const arr = [1, 2, 3];
for (var i = 0; i < arr.length; i++) {
console.log(arr[i]); // 1 2 3
}
그리고 ES6 에서 도입된 for ...of
문을 통해서도 순회가 가능하다.
const arr = [1, 2, 3];
for (const a of arr) {
console.log(a); // 1 2 3
}
먼저 for ...of
문은 배열 뿐만이 아니라 set
과 map
자료구조에서도 순회가 가능하다.
const set = new Set([1, 2, 3]);
for (const a of set) {
console.log(a); // 1 2 3
}
하지만 set
과 map
자료구조에서는 배열과는 다르게 기존 for
문과 같이 키로 접근이 불가능하다.
console.log(set[0]) // undefined
console.log(set[1]) // undefined
그렇다는 말은 for ...of
문의 동작은 각 자료구조의 키를 통한 것이 아니라는 것을 알 수 있다.
위의 Symbol
에서 보았듯이 Symbol
은 어떤 객체의 키로 사용될 수 있다.
그리고 다음과 같이 배열에 Symbol.iterator
라는 키로 접근해 보면 어떤 함수가 존재한다는 걸 알 수 있고,
Symbol.iterator
라는 키를 null
값으로 할당하면 순회가 되지 않는다는 것도 알 수 있다.
console.log(arr[Symbol.iterator]) // ƒ values() { [native code] }
arr[Symbol.iterator] = null;
for (const a of arr) {
console.log(a)
}
위에서 언급했듯이 이터러블과 이터레이터의 정의는 다음과 같다.
이터러블은 Symbol.iterator
를 실행했을 때 이터레이터를 리턴하게 되고,
그 이터레이터는 next()
메서드를 통해서 { value, done }
객체를 리턴한다.
코드로 이해해보면 다음과 같다.
const arr = [1, 2, 3]
let iterator = arr[Symbol.iterator]();
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: undefined, done: true }
for (const a of arr) {
console.log(a); // 1 2 3
}
즉, for ...of
문의 동작 원리는 객체의 프로퍼티 키를 통한 것이 아닌,
Symbol.iterator
를 실행해 리턴한 이터레이터의 done
값이 true
가 되기 전까지 value
값을 변수 a
에 담아 출력하게 된다.
따라서 배열과 set
, map
자료구조는 이터러블이라고 할 수 있고,
이터러블/이터레이터 프로토콜을 따른다고 할 수 있다.