심볼 & 이터레이션 프로토콜 프로토콜

Tori·2024년 12월 12일

JavaScript

목록 보기
7/7
post-thumbnail

poiemaweb - Symbol
mdn - Symbol
poiemaweb - 이터레이션과 for...of 문





심볼(Symbol)

심볼은 ES6에서 도입된 7번째 데이터 타입으로 변경 불가능한 원시 타입의 값이다. 심볼 값은 다른 값과 중복되지 않는 유일무이한 값으로 주로 이름의 충돌 위험이 없는 유일한 프로퍼티 키를 만들기 위해 사용한다.

심볼 값의 생성

Symbol 함수

다른 원시값(문자열, 숫자, 불리언, undefined, null) 타입의 값은 리터럴 표기법을 통해 값을 생성할 수 있지만, 심볼 값은 Symbol 함수를 호출하여 생성해야 한다.
이때 생성된 심볼 값은 외부로 노출되지 않아 확인할 수 없고, 다른 값과 절대 중복되지 않는 유일무이한 값이다.

Symbol() 함수를 호출하여 유일무이한 심볼 값을 생성하는 예제 코드

const symbol1 = Symbol();

console.log(typeof symbol1); // symbol

// 심볼 값은 외부로 노출되지 않아 확인할 수 없음
console.log(symbol1); // Symbol()

심볼 값은 유일무이한 값이므로 심볼 값으로 프로퍼티 키를 만들면 다른 프로퍼티 키와 절대 충돌하지 않는다.
기존 프로퍼티 키와 충돌하지 않는 것은 물론, 미래에 추가될 어떤 프로퍼티 키와도 충돌할 위험이 없다.


심볼 값은 변경 불가능한 원시 값이다.

Symbol 함수는 String, Number, Boolean 생성자 함수와 달리 new 연산자와 함께 호출하지 않는다.
new 연산자와 함께 생성자 함수 or 클래스를 호출하면 객체(인스턴스)가 생성되지만 심볼 값은 변경 불가능한 원시 값이다.

new Symbol() 할 때 발생하는 에러

const symbol2 = new Symbol(); // TypeError: Symbol is not a constructor

Symbol 함수에 문자열을 인수로 전달

이 문자열은 생성된 심볼 값에 대한 설명(description)으로 디버깅 용도로만 사용되고 심벌 값 생성엔 영향을 주지 않음

심볼 값에 대한 설명이 같더라도 생성된 심벌 값은 유일무이한 값이다.

const symbol1 = Symbol('symbol description');
const symbol2 = Symbol('symbol description');

console.log(symbol1 === symbol2); // false

Symbol 값도 문자, 숫자, 불리언과 같이 객체처럼 접근하면 암묵적으로 래퍼 객체를 생성한다.

인스턴스 속성 및 메서드

: 이 속성들은 Symbol.prototype에 정의되며 모든 Symbol 인스턴스가 공유한다.

description property, toString method는 Symbol.prototype의 프로퍼티다.

const symbol1 = Symbol('symbol description');

// 심벌도 래퍼 객체를 생성함
console.log(symbol1.description); // 'symbol description'
console.log(symbol1.toString()); // 'Symbol(symbol description)'

Symbol 값은 암묵적으로 문자열 or 숫자 타입으로 변환되지 않음

const symbol1 = Symbol('symbol description');

console.log(symbol1 + ''); // TypeError: Cannot convert a Symbol value to a string
console.log(+symbol1); // TypeError: Cannot convert a Symbol value to a number

불리언 타입으로는 암묵적 타입 변환이 된다.

if 문 등 에서 존재를 확인할 수 있다.

const symbol1 = Symbol('symbol description');

console.log(!!symbol1); // true

if(symbol1) console.log('symbol1 is not empty.');



Symbol.for / Symbol.keyFor 메서드

Symbol.for 메서드

Symbol.for 메서드는 인수로 전달받은 문자열을 키로 사용하여 키와 심볼 값의 쌍들이 저장되어 있는 전역 심벌 레지스트리(global symbol registry, "전역 심볼 레지스트리"는 설명을 위한 상상의 개념이며 JavaScript 엔진 내의 실제 데이터 구조와는 일치하지 않을 수 있다.)에서 해당 키와 일치하는 심볼 값을 검색한다.

  • 검색에 성공하면 새로운 심볼 값을 생성하지 않고 검색된 심볼 값을 반환한다.
  • 검색에 실패하면 새로운 심볼 값을 생성하여 Symbol.for 메서드의 인수로 전달된 키로 전역 심볼 레지스토리에 저장한 후, 생성된 심볼 값을 반환한다.
// 전역 심볼 레지스트리에 symbol이라는 키로 저장된 심볼 값이 없으면 새로운 심볼 값을 생성
const s1 = Symbol.for('symbol');

// 전역 심볼 레지스트리에 symbol이라는 키로 저장된 심볼 값이 있으면 해당 심볼 값을 반환
const s2 = Symbol.for('symbol');

console.log(s1 === s2); // true

Symbol 함수는 호출될 때 마다 유일무이한 심볼 값을 생성한다.

이때 자바스크립트 엔진이 관리하는 심벌 값 저장소인 전역 심볼 레지스트리에서 심볼 값을 검색할 수 있는 키를 지정할 수는 없어서 전역 심볼 레지스트리에 등록되어 관리되진 않는다.

하지만, Symbol.for 메서드를 사용하면 애플리케이션 전역에서 중복되지 않는 유일무이한 상수인 심볼 값을 단 하나만 생성하여 전역 심볼 레지스트리를 통해 공유할 수 있다.



Symbol.keyFor

Symbol.keyFor 메서드를 사용하면 전역 심볼 레지스트리에 저장된 심볼 값의 키를 추출할 수 있다.

// 전역 심볼 레지스트리에 symbol이라는 키로 저장된 심볼 값이 없으면 새로운 심볼 값을 생성
const s1 = Symbol.for('symbol');

// 전역 심볼 레지스트리에 저장된 심볼 값의 키를 추출
Symbol.keyFor(s1); // symbol

// Symbol 함수를 호출하여 생성한 심볼 값은 전역 심볼 레지스트리에 등록되어 관리되지 않음
const s3 = Symbol('new symbol3');

Symbol.keyFor(s3); // undefined



심볼과 상수

예를 들어서 권한을 나타내는 상수를 정의한다고 가정해보자.

// 값 1, 2, 3에는 특별한 의미가 없고, 상수 이름에 의미가 있다.
const PERMISSION = {
  ADMIN: 1,
  USER: 2,
  GUEST: 3
};

// 변수에 상수를 할당
const permissionResult = PERMISSION.USER;

if (permissionResult === PERMISSION.USER) {
  console.log('you permission is User.');
}

위 예제와 같이 값에는 특별한 의미가 없고 상수 이름 자체에 의미가 있는 경우가 있다.

이때 문제는 상수 값 1, 2, 3이 변경될 수 있으며 다른 변수 값과 중복될 수도 있다.


이런 경우에 변경/중복될 가능성이 있는 무의미한 상수 대신 중복될 가능성이 없는 유일무이한 심볼 값을 사용할 수 있다.

// 권한을 나타내는 상수를 정의한다.
// 중복될 가능성이 없는 심볼 값으로 상수 값을 생성한다.
const PERMISSION = {
  ADMIN: Symbol('admin'),
  USER: Symbol('user'),
  GUEST: Symbol('guest')
};

const permissionResult = PERMISSION.USER;

if (permissionResult === PERMISSION.USER) {
  console.log('you permission is User.');
}



심볼과 프로퍼티 은닉

심볼 값을 프로퍼티 키로 사용하여 생성한 프로퍼티는 for ... in문이나 Object.keys, Object.getOwnPropertyNames 메서드로 찾을 수 없다.

이처럼 심볼 값을 프로퍼티 키로 사용하여 프로퍼티를 생성하면 외부에 노출할 필요가 없는 프로퍼티를 은닉할 수 있다.

const obj = {
  // 심볼 값으로 프로퍼티 키를 생성
  [Symbol('symbol1')]: 1,
  str1: 'string1'
};

for (const key in obj) {
  console.log(key); // str1 // 심볼 값으로 만든 키는 출력되지 않는다.
}

console.log(Object.keys(obj)); // ['str1']
console.log(Object.getOwnPropertyNames(obj)); // ['str1']

하지만 프로퍼티를 완전하게 숨길 수 있는 것은 아니다.
ES6에서 도입된 Object.getOwnPropertySymbols 메서드를 사용하면 심볼 값을 프로퍼티 키로 사용하여 생성한 프로퍼티를 찾을 수 있다.

const obj = {
  // 심볼 값으로 프로퍼티 키를 생성
  [Symbol('symbol1')]: 1,
  str1: 'string1'
};

// getOwnPropertySymbols 메서드는 인수로 전달한 객체의 심볼 프로퍼티 키를 배열로 반환한다.
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(symbol1)]

// getOwnPropertySymbols 메서드로 심볼 값도 찾을 수 있다.

const symbolKey1 = Object.getOwnPropertySymbols(obj)[0];
console.log(obj[symbolKey1]); // 1



심볼과 표준 빌트인 객체 확장

일반적으로 표준 빌트인 객체에 사용자 정의 메서드를 직접 추가하여 확장하는 것은 권장하지 않는다.
표준 빌트인 객체는 읽기 전용으로 사용하는 것이 좋다.

// 표준 빌트인 객체를 확장하는 것은 권장하지 않음
Array.prototype.sum = function () {
  return this.reduce((acc, cur) => acc + cur, 0);
}

[1, 2].sum(); // 3

이유

개발자가 직접 추가한 메서드와 미래에 표준 사양으로 추가될 메서드의 이름이 중복될 수 있다. 또 다른 라이브러리와의 충돌이 일어날 수도 있다.

이름이 중복되어 표준 빌트인 메서드를 사용자 정의 메서드가 덮어씌울 수 있는데 그렇게 되면 문제가 된다.


중복될 가능성이 없는 심볼 값으로 프로퍼티 키를 생성하여 표준 빌트인 객체를 확장

중복될 가능성이 없는 심볼 값으로 프로퍼티 키를 생성하여 표준 빌트인 객체를 확장하면 표준 빌트인 객체의 기존 프로퍼티 키와 충돌하지 않고, 표준 사양의 버전이 올라감에 따라 추가될지 모르는 어떤 프로퍼티 키와도 충돌할 위험이 없이 안전하게 표준 빌트인 객체를 확장할 수 있다.

심볼 값으로 프로퍼티 키를 동적 생성하면 다른 프로퍼티 키와 절대 충돌하지 않아 안전함

Array.prototype[Symbol.for('sum')] = function () {
  return this.reduce((acc, cur) => acc + cur , 0);
}

[1, 2][Symbol.for('sum')](); // 3



Well-known Symbol

: 자바스크립트가 기본 제공하는 빌트인 심볼 값을 ECMAScript 사양에서 Well-konwn Symbol이라 부른다.

빌트인 심볼 값은 Symbol 함수의 프로퍼티에 할당되어 있다.

브라우저 콘솔에서 Symbol 함수를 참조

console.dir(Symbol);


Well-known Symbol은 자바스크립트 엔진의 내부 알고리즘에 사용된다.

예를 들어 Array, String, Map, Set, TypedArray, arguments, NodeList, HTMLCollection과 같이 for ... of 문으로 순회 가능한 빌트인 이터러블은 Well-known Symbol인 Symbol.iterator를 키로 갖는 메서드를 가지며, Symbol.iterator 메서드를 호출하면 이터레이터를 반환하도록 ECMAScript 사양에 규정되어있다.

빌트인 이터러블은 이터레이션 프로토콜을 준수한다.


빌트인 이터러블이 아닌 일반 객체를 이터러블처럼 동작하도록 구현하고 싶다면 이터레이션 프로토콜을 따르면 된다.

ECMAScript 사양에 규정된대로 Well-known Symbol인 Symbol.iterator를 키로 갖는 메서드를 객체에 추가하고, 이터레이터를 반환하도록 구현하면 그 객체는 이터러블이 된다.

// 1~5 범위의 정수로 이루어진 이터러블
const iterable = {
  // Symbol.iterator 메서드를 구현하여 이터러블 프로토콜을 준수
  [Symbol.iterator]() {
    let cur = 1;
    const max = 5;
    
    // Symbol.iterator 메서드는 next 메서드를 소유한 이터레이터를 반환
    return {
      next() {
        return { value: cur++, done: cur > max + 1 };
      }
    };
  }
};

for (const num of iterable) {
  console.log(num); // 1 2 3 4 5
}

이터레이션 프로토콜을 준수하기 위해 일반 객체에 추가해야 하는 메서드의 키 Symbol.iterator는 기존 프로퍼티 키 또는 미레에 추가될 프로퍼티 키와 절대로 중복되지 않는다.

심볼은 중복되지 않는 상수 값을 생성하는 것은 물론 기존에 작성된 코드에 영향을 주지 않고 새로운 프로퍼티를 추가하기 위해, 즉 하위 호환성을 보장하기 위해 도입되었다.












이터레이션 & 이터레이터 & 이터러블

소프트웨어는 데이터를 처리하기위해 만들어졌고, 데이터를 처리하는 데 사용하는 패턴은 코드 전체의 가독성에 큰 영향을 미친다.



이터레이터 패턴(iterator pattern)

: 이 패턴을 사용하면 데이터를 덩어리(chunk) 단위로 표준화된 방법을 사용해 각각 처리할 수 있다.
이터레이터 패턴의 접근 방식은 데이터를 일정 단위로 쪼개어 하나의 조각들을 차례대로 순회하며 점진적으로 처리하면 좀 더 범용적이고 유용할 거라는 아이디어에서 출발했다.

이터레이터 패턴의 핵심

: 반복 작업으로 데이터를 처리할 때 표준화된 방법을 제공한다는 점이다.
처리해야 할 데이터 컬렉션의 형태가 다르면 반복 작업에 사용되는 코드도 제각각이 될 수밖에 없는데 이터레이터 패턴을 사용하면 깔끔하고 이해하기 쉬운 코드로 반복 작업을 수행할 수 있다.





이터레이션 프로토콜(iteration protocol)

ES6에서 JS 내장 문법을 통해 이터레이터 패턴을 구현하는 구체적인 프로토콜(규약)이 추가되었다.
데이터 컬렉션을 순회하기 위한 프로토콜로 이터레이션 프로토콜을 준수한 객체는 for...of문으로 순회할 수 있고, Spread 문법의 피연산자가 될 수 있다.

이 프로토콜에서 next() 메서드는 이터레이터 리절트(iterator result)라 불리는 객체를 반환하고, 이터레이터 리절트 객체에는 value와 done이라는 프로퍼티가 있어야 한다고 규정한다.
또한 반복 작업이 끝나지 않은 경우, done에는 불리언 값 false가 저장되어야 한다고 정의한다.

{
  next() {
    return { 
      value: any, 
      done: boolean 
    };
  }
}

이터레이션 프로토콜에는

이터러블 프로토콜(iterable protocol)
이터레이터 프로토콜(iterator protocol)이 있다.





이터러블

이터러블 프로토콜을 준수한 객체를 이터러블이라 한다.

이터러블은 Symbol.iterator 메소드를 구현하거나 프로토타입 체인에 의해 상속한 객체를 말한다.

Symbol.iterator 메소드는 이터레이터를 반환한다.
이터러블은 for ... of 문에서 순회할 수 있으며 Spread 문법의 대상으로 사용할 수 있다.
디스트럭쳐링 할당, Map과 Set의 생성자에도 사용된다.

ES6에서는 문자열, 배열, 맵, 셋 같이 기본이 되는 자료구조나 컬렉션을 이터러블로 정의한다.


배열은 이터러블이다.

  • 배열은 Symbol.iterator 메소드를 소유한다. 따라서 배열은 이터러블 프로토콜을 준수한 이터러블이다.
let arr = [10, 20, 30];

console.log(Symbol.iterator in arr); // true

// 이터러블 프로토콜을 준수한 배열은 for ... of 문에서 순회 가능
for (const val of arr) {
  console.log(`배열의 값: ${val}`);
}
// 배열의 값: 10
// 배열의 값: 20
// 배열의 값: 30



일반 객체는 Symbol.iterator 메소드를 소유하지 않는다. 따라서 일반 객체는 이터러블 프로토콜을 준수한 이터러블이 아니다.

const obj = { a: 1, b: 2 };

console.log(Symbol.iterator in obj); // false

for (const prop of obj) {
  console.log(`객체의 값: ${prop}`); // TypeError: obj is not iterable
}

일반 객체는 이터레이션 프로토콜을 준수(Symbol.iterator 메소드를 소유)하지 않기 때문에 이터러블이 아니다.
따라서 일반 객체는 for ... of 문에서 순회할 수 없으며 Spread 문법의 대상으로 사용할 수도 없다.

하지만 일반 객체도 이터러블 프로토콜을 준수하도록 구현하면 이터러블이 된다.









이터레이터

ES6에서 정의한 이터레이션 프로토콜을 사용하면 데이터를 한 조각씩 순차적으로 처리한다.

이터레이터 프로토콜은 next 메서드를 소유하며, next 메서드를 호출하면 이터러블을 순회하며 value, done 프로퍼티를 갖는 이터레이터 리절트 객체를 반환하는 것이고, 이 규약을 준수한 객체가 이터레이터이다.

이터러블 프로토콜을 준수한 이터러블은 Symbol.iterator 메서드를 소유한다. 이 메서드를 호출하면 이터레이터를 반환한다. 이터레이터 프로토콜을 준수한 이터레이터는 next 메서드를 갖는다.

// 배열은 이터러블 프로토콜을 준수한 이터러블이다.
const arr = [1, 2, 3];

// Symbol.iterator 메서드는 이터레이터를 반환한다.
const iterator = arr[Symbol.iterator]();

// 이터레이터 프로토콜을 준수한 이터레이터는 next 메서드를 갖는다.
console.log('next' in iterator); // true



이터레이터의 next 메서드를 호출하면 value, done 프로퍼티를 갖는 이터레이터 리절트(iterator result) 객체를 반환한다.

// 배열은 이터러블 프로토콜을 준수한 이터러블이다.
const arr = [1, 2, 3];

// Symbol.iterator 메서드는 이터레이터를 반환한다.
const iterator = arr[Symbol.iterator]();

// 이터레이터 프로토콜을 준수한 이터레이터는 next 메서드를 갖는다.
console.log('next' in iterator); // true

let iteratorResult = iterator.next();
console.log(iteratorResult); // {value: 1, done: false}



이터레이터의 next 메서드는 이터러블의 각 요소를 순회하기 위한 포인터의 역할을 한다.
next 메서드를 호출하면 이터러블은 순차적으로 한 단계씩 순회하며 이터레이터 리절트 객체를 반환한다.

// 배열은 이터러블 프로토콜을 준수한 이터러블이다.
const arr = [1, 2, 3];

// Symbol.iterator 메서드는 이터레이터를 반환한다.
const iterator = arr[Symbol.iterator]();

// 이터레이터 프로토콜을 준수한 이터레이터는 next 메서드를 갖는다.
console.log('next' in iterator); // true

console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}
  • next() 호출 반환값인 이터레이터 리절트 객체의 done 프로퍼티가 true라면 반복 작업이 중단된다.
  • 이터레이터의 next 메서드가 반환하는 이터레이터 리절트 객체의 value 프로퍼티는 현재 순회중인 이터러블의 값을 반환하고, done 프로퍼티는 이터러블의 순회 완료 여부를 반환한다.





하지만 이 방식은 다소 시간이 들기 때문에 ES6에서는 for ... of 반복문 같은 새로운 문법과 api를 새롭게 정의해 표준화된 방법으로 이터레이터 리절트 객체를 소비할 수 있도록 제공하고 있다.

// 처리하려는 데이터의 이터레이터
const data = /* ...*/;

// 이터레이터 리절트 객체를 순회함
for (let val of data) {
  console.log(`이터레이터 값: ${val}`);
}

for ... of 반복문 말고도 표현식 ...을 사용하면 이터레이터를 소비할 수 있다.
...은 대칭 형태인 전개 구문(spread syntax)와 나머지 매개변수(rest parameter)를 이용해 사용한다.
이때 전개 구문이 바로 이터레이터를 소비하는 주체인 이터레이터 소비자(consumer)이다.

이터레이터를 펼치려면(spread) 펼칠 무언가가 있어야되는데 JS에서는 배열 or 함수 호출 시 넘기는 인수를 다룰 때 가능하다.





이터레이션 프로토콜의 필요성

데이터 소비자(Data consumer)인 for ... of 문, spread 문법 등은 아래와 같이 다양한 데이터 소스를 사용한다.

Array, String, Map, Set, TypedArray(Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array), DOM data structure(NodeList, HTMLCollection), Arguments

위의 데이터 소스는 모두 이터레이션 프로토콜을 준수하는 이터러블이다.
즉, 이터러블은 데이터 공급자(Data provider)의 역할을 한다.


다양한 데이터 소스가 각자의 순회 방식을 갖는다면 데이터 소비자는 소스의 순회 방식을 모두 지원해야해서 효율적이지 않다.
하지만 다양한 데이터 소스가 이터레이션 프로토콜을 준수하도록 규정하면 데이터 소비자는 이터레이션 프로토콜만 지원하도록 구현하면 된다.

즉, 이터레이션 프로토콜은 다양한 데이터 소스가 하나의 순회 방식을 갖도록 규정하여 데이터 소비자가 효율적으로 다양한 데이터 소스를 사용할 수 있도록 데이터 소비자와 데이터 소스를 연결하는 인터페이스의 역할을 한다.

이터러블을 지원하는 데이터 소비자는 내부에서 Symbol.iterator 메서드를 호출해 이터레이터를 생성하고 이터레이터의 next 메서드를 호출하여 이터러블을 순회한다.
그리고, next 메서드가 반환한 이터레이터 리절트 객체의 value 프로퍼티 값을 취득한다.








profile
🌿

0개의 댓글