ES6
부터 도입된 새로운 빌트인 생성자 함수인 Symbol
아무리 MDN
문서를 읽어보고 용법을 읽어봐도 도저히 왜 필요한건지에 대한 이해가 힘들었다.
그!래!서! 몇몇 유튜브와 블로그를 돌아가보며 이해하고 예시를 만들어봤다.
Symbol
이 왜 필요한데?// 다른 작업자가 만들어둔 클래스
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
introduce() {
Object.entries(this).forEach(([property, value]) => {
console.log(`${property} : ${value}`);
});
}
}
만약 이미 어떤 모듈의 작업자는 인스턴스에는 모두에게 공유 가능한 개인정보만 담을 것을 기대하고 만든
클래스가 존재한다고 해보자
그래서 해당 클래스에는 인스턴스의 모든 프로퍼티를 순회하며 값을 출력하는 introduce
라는 메소드가 존재한다.
이전 작업자 : ㅋㅋ 인스턴스에는 공개 가능하며 자기 소개 성격을 갖는 프로퍼티만 넣으세요
현재 작업자 : 싫어요 ㅋㅋ 내 맘대로 변형해서 쓸거셈
이 때 내가 모듈을 import
하여 사용 하는데, 나는 조금 변형하고 싶어서 클래스를 상속 받은 후 변경해서 사용했다고 해보자
외부에 공개하고 싶지 않은 인스턴스인 password
프로퍼티나
개인정보와는 무관한 메소드인 birthCalcuate
를 인스턴스에 추가해뒀다.
// 다른 작업자가 만들어둔 class 를 변경해서 쓰고 싶은 경우
class newPerson extends Person {
constructor(name, age, password) {
super(name, age);
this.password = password;
this.birthCalculator = function birthCalculator() {
const currentYear = new Date().getFullYear();
return currentYear - this.age;
};
}
}
const tom = new newPerson('tom', 16, '1234');
이 때 introduce
를 사용하면
tom.introduce();
name : tom
age : 16
password : 1234 // 외부에 공개하고 싶지 않은 프로퍼티
birthCalculate : function birthCalculate() {
const currentDate = new Date();
return currentDate.getFullYear() - this.age; // 모듈 생성자의 의도와 다른 프로퍼티
}
처럼 이전 작업자가 원하지 않는 결과를 초래하게 될 수도 있다.
이전 작업자는 인스턴스에 외부에 공개 가능하면서 자기 소개와 관련된 인스턴스만 들어올 것이라고 생각했기 때문이다.
그럼 이런 문제를 해결하기 위해서는 클래스를 변경할 때 작업자의 의도에 맞춰
// 다른 작업자가 만들어둔 class 를 변경해서 쓰고 싶은 경우
class newPerson extends Person {
constructor(name, age, password) {
super(name, age);
Object.defineProperty(this, 'password', {
value: password,
enumerable: false, // 나열 불가능하게 enumerable : false 설정
}),
Object.defineProperty(this, 'birthCalculator', {
value: () => {
const currentDate = new Date();
return currentDate.getFullYear() - this.age;
},
enumerable: false,
});
}
}
const tom = new newPerson('tom', 16, 1234);
tom.introduce();
name : tom
age : 16
처럼 프로퍼티의 내부 프로퍼티를 설정해주며 넣어줘야 한다.
Symbol
을 사용한다면 ?class newPerson extends Person {
constructor(name, age, password) {
super(name, age),
(this[Symbol('password')] = password),
(this[Symbol('birthCalcul')] = () => {
const currentDate = new Date();
return currentDate.getFullYear() - this.age;
});
}
}
const tom = new newPerson('tom', 16, 1234);
tom.introduce();
name : tom
age : 16
그냥 프로퍼티를 symbol
로 설정해주기만 해도 enumerable
하지 않게 설정해줄 수 있다.
메소드인 introduce
에서 뿐이 아니라
console.log(tom.password); // undefined
프로퍼티로 값을 확인하는 것 조차도 막아준다.
console.log(tom);
/**
newPerson {
name: 'tom',
age: 16,
[Symbol(password)]: 1234,
[Symbol(birthCalcul)]: [Function (anonymous)]
} */
하지만 Object.defineproperty
에서 enumerable : false
로 해준것과는 다르게 객체를 로그하면 뜨기는 한다.
만약 이렇게 뜨게도 하기 싫다면 다른 방법을 써줘야 할 것이다.
정리
Symbol
을 사용하면 외부에서 액세스 되지 않도록 프로퍼티를 설정 할 수 있다.
클래스를 상속하거나 클래스의 인스턴스를 동적으로 수정하는 경우,Symbol
을 활용하면 외부에 노출되지 않아야 하는 프로퍼티나 메소드를 쉽게 추가 할 수 있다.
이를 통해 기존 작업자가 의도한 것과 다른 의도치 않은 결과를 방지하고, 민감한 정보를 안전하게 다룰 수 있다.
이번에는 조금 다른 예시로 위 예시에선 프로퍼티와 프로퍼티의 값이 연관 있었지만
이번엔 프로퍼티 키만 의미있고 값 자체는 의미가 없는 경우를 예시로 들어보자
class Jet {
constructor() {
this.up = 1;
this.down = 2;
this.left = 3;
this.right = 4;
}
/**
이후 어떤 무수한 메소드들 ...
*/
}
처럼 비행기가 날아가는 게임을 개발했을 때 객체의 up , down , left , right
라는 프로퍼티를 지정해두고
해당 프로퍼티를 이용해서 메소드들을 구현해뒀다고 해보자
이 때 프로퍼티의 값들은 모두 고유하며 독립적이여야 한다.
프로퍼티의 키가 중복되지 않는 것도 중요하지만 프로퍼티의 값도 중복되지 않는 것이 중요한 상황이라고 가정해보자
그리고 프로퍼티의 값이 의미가 없기를 기대한다. 값 자체는 중요한게 아니니까
그러면 이럴 때 값이 절대 중복되지 않으며 의미가 적은 Symbol 을 이용하자
const Jet = class jet {
constructor() {
this.up = Symbol('up');
this.down = Symbol('down');
this.left = Symbol('left');
this.right = Symbol('right');
}
};
const plane = new Jet();
정리
이처럼
Symbol
은 중복되지 않는 고유한 값을 생성하는 일종의 상수 같은 역할을 한다.
만약 빌트인 생성자 함수인 Array
에 뭔가 있으면 좋을 것 같은 메소드가 생각나
내 파일에서 메소드를 추가 하려고 한다고 해보자
배열을 상속 받아 메소드가 담긴 새로운 클래스를 생성하는 것이 너무 귀찮았다고 해보자
이 때 내가 추가한 메소드는 빌트인 생성자 함수의 프로토타입 메소드 명과 절!대! 중복되면 안된다.
만약 중복되게 되면 메소드가 덮어 씌워져 기존 프로토타입 메소드를 사용하지 못하게 되기 때문이다.
Array.prototype.includes = function includes(target) {
for (let i = 0; i < this.length; i += 1) {
if (this[i] === target) {
console.log('있어유');
return i;
}
}
console.log('없어유');
return -1;
};
const arr = [1, 2, 3, 4, 5];
arr.includes(2); // 있어유
더 이상 나는 Array.prototype.includes
메소드를 사용하지 못하게 됐다.
프로퍼티를 새로 추가하고 싶을 때 기존 프로퍼티 키들과 충돌 되지 않게 하고 싶으면 Symbol
을 사용하자
Array.prototype[Symbol.for('includes')] = function (target) {
for (let i = 0; i < this.length; i += 1) {
if (this[i] === target) {
console.log('있어유');
return i;
}
}
console.log('없어유');
return -1;
};
const arr = [1, 2, 3, 4, 5];
console.log(arr.includes(2)); // true
console.log(arr[Symbol.for('includes')](2));
// 있어유
// true
정리
빌트인 객체에 프로퍼티를 추가 할 때
Symbol
을 사용하자이처럼
Symbol
은 고유하게 존재하는 상수 역할을 하기 때문에 기존 빌트인 생성자 함수들도Symbol
의 프로토타입들을 메소드로 가지고 있는 경우가 있다.
Symbol
의 사용 예시Symbol
은 생성자 함수 없이 사용한다.
const symbol = Symbol('mysymbol');
const symbo2 = new Symbol('mysymbol'); // TypeError: Symbol is not a constructor
Symbol
들은 절대 중복되지 않는다.const symbol1 = Symbol('mysymbol');
const symbol2 = Symbol('mysymbol');
console.log(symbol1 === symbol2); // false
console.log(symbol1 == symbol2); // false
뭔가 안에 인수를 넣은 값들이 동일함에도 불구하고 값이 같느냐는 비교 연산자에서 false
가 나온다.
인수를 아무것도 넣지 않아도 동일하다.
그 이유는 안에 들어가는 인수는 주석 역할을 할 뿐, 다른 의미를 가지지 않는다.
파라미터에도 Description
이라고 한다.
이러한 설정을 통해 프로퍼티에 Symbol
을 이용하게 되면 Symbol
값을 재할당하여 프로퍼티가 덮이는 일을 방지 할 수 있다.
const obj = {
[Symbol('symbol')]: 1,
};
console.log(obj[Symbol('symbol')]); // undefined
obj[Symbol('symbol')] = 2;
console.log(obj); // { [Symbol(symbol)]: 1, [Symbol(symbol)]: 2 }
외부 참조를 못하게 한다고 하였기 때문에 값을 조회하려고 하니 undefined
라고 나온다.
Symbol
은 호출 할 때 마다 독립적인가요 ?아니용
다른 방법들도 있다.
const mysymbol1 = Symbol.for('mysymbol');
const mysymbol2 = Symbol.for('mysymbol');
console.log(mysymbol1 === mysymbol2); // true
Symbol.for
을 사용하는 것인데, 이는 이미 해당 description
이 동일한 Symbol
이 있을 경우엔 해당 값을 그대로 참조한다.
이 때 Symbol
은 고유한 상수 역할이라고 하였기 때문에 이전에 생성된 원시값인 Symbol
자체를 그대로 참조한다.
만약 동일한 description
이 없다면 새로 생성한다.
description
은 어떻게 접근하나요 ?Symbol.keyFor()
을 통해 참조할 수 있다.
const mysymbol1 = Symbol.for('mysymbol');
console.log(Symbol.keyFor(mysymbol1)); // mysymbol
Symbol
은 enum
을 흉내 낼 수 있어요enum
이 뭘까 ?
C,자바,파이썬 등 여러 프로그래밍 언어에서 지원하는 자료구조 타입이다. 특정 값들의 집합을 나타내는 자료구조로서 , 고유의 프로퍼티 값들은 모두 외부에서 수정이나 삭제가 불가능해야 한다.
그럼 고유의 프로퍼티 값은 어떻게 생성할까 ? ==> Symbol
외부에서 수정이나 삭제가 불가능하게 어떻게 하는데 ? ==> Object.freez
const obj = Object.freeze({
up: Symbol('up'),
down: Symbol('down'),
left: Symbol('left'),
right: Symbol('right'),
});
console.log(obj);
/**
{
up: Symbol(up),
down: Symbol(down),
left: Symbol(left),
right: Symbol(right)
}
*/
아 굿이예용 ~~
이후 Symbol
의 활용은 위 예시들에서 충분히 설명한 것 같다.
그러니 패스하고
Well-known Symbol
배열과 객체 모두 공통된 프로토타입 메소드를 가지고 있는 것들을 종종 볼 수 있다.
예를 들어 toString
같은 경우는
const arr = [1, 2, 3];
const obj = { a: 1 };
console.log(arr.toString()); // '1,2,3'
console.log(obj.toString()); // [object Object]
같이 공통적으로 사용 가능하다.
그럼 Array
와 Object
안에 모두 toString
이 중복돼서 적혀있는걸까 ?
아닙니다
전역 객체 내에 Symbol
로 정의 되어 있는 고유한 메소드들이 존재한다.
빌트인 메소드들은 저 Symbol
로 정의되어 있는 고유한 메소드들을 참조한다.
또한 빌트인 메소드들이 참조하고 있는 Symbol
로 정의된 메소드들을 Weel-known Symbol
이라 한다.
가장 직관적으로 이해 할 수 있는 방법은 iterable
한 자료구조들을 이용해 for of
를 이용하는 경우이다.
const arr = [1, 2, 3, 4, 5];
for (const num of arr) {
console.log(num); // 1 2 3 4 5
}
어떻게 iterable
한 자료구조들은 for
문을 만나면 순회 할 수 있는 걸까 ?
그 이유는 모두 프로퍼티로 [Symbol.iterator]
를 가지고 있기 때문이다.
Symbol.iterator
는 특정 객체를 반환하는 메소드이다.
이 객체는 value , done
이라는 프로퍼티를 가지는 객체를 return
하는 next()
메소드가 담긴 객체이다.
잘 이해가 안돼요
저도 그렇습니다
for of
코드는 next()
라는 객체가 담긴 메소드를 받아 next()
메소드를 꾸준히 호출하며, value
프로퍼티에 적힌 값을 반환하고 done
이란 프로퍼티 값을 조건부로 받아 순회를 마친다.
const arr = [1, 2, 3, 4, 5];
for (const num of arr) {
console.log(num); // 1 2 3 4 5
}
내가 여기서 for of
를 쓰는 순간 어떤 일이 벌어지는지 확인해보자
const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator](); // Symbol.iterator 메소드 호출
let result = iterator.next(); // 첫 번째 이터레이션
console.log(result.value); // 1
console.log(result.done); // false
result = iterator.next(); // 두 번째 이터레이션
console.log(result.value); // 2
console.log(result.done); // false
result = iterator.next(); // 세 번째 이터레이션
console.log(result.value); // 3
console.log(result.done); // false
result = iterator.next(); // 더 이상 반환할 요소가 없는 경우
console.log(result.done); // true
for of
를 쓰는 순간 [Symbol.iterator]
에 담긴 함수를 조회한 후 ()
로 호출하여 iterator
라는 객체를 생성한다.
생성한 iterator
의 next()
메소드를 호출하면 result
라는 객체가 호출되며 result
의 value
에는 array
의 값이, done
에는 조건문이 있는걸 발견 할 수 있다.
그럼 iterator
객체는 어떻게 생겼을까 ?
const simpleIterator = {
currentIndex: 0,
array: [1, 2, 3],
next: function () {
return this.currentIndex < this.array.length
? { value: this.array[this.currentIndex++], done: false }
: { value: undefined, done: true };
}
};
매우 복잡하게 생겼겠지만 간소하게 구현한 값은 다음과 같다.
시작의 인덱스인 currentIndex
가 존재하고 array
, next
메소드는 조건에 따라 {value , done}
이 담긴 객체를 반환한다.
만약 현재 인덱스가 길이보다 작으면 어레이의 해당 인덱스를 value
에 담고 인덱스를 1 증가시킨다.
만약 인덱스 길이보다 길면 값을 리턴하지 않고 done
을 false
로 한 객체를 반환한다.
그러니 결국 for of
로 iterable
한 자료구조를 순회하는 것은
Symbol.iterator
를 프로퍼티로 갖는 메소드를 호출시켜 Iterator
객체를 생성하고
해당 객체에서 next()
를 모두 순회 할 때 까지 호출하는거였다.
와우
자세한 내용은 다음 강의에서 자세히 배워보자 다음 강의가 이터레이터다
정리
빌트인 생성자 함수들은 전역에서
Symbol
로 생성된 고유한 메소드들을 참조하고 있다.
빌트인 생성자들이 참조학고 있는 고유한 메소드를Well-Known-Symbol
이라고 한다.
프로퍼티가Symbol
로 참조하는 이유는 중복된 프로퍼티 명을 피하기 위함이다.