JavaScript · 4장 객체
심볼: 다른 키와 절대 겹치지 않는 키
객체의 키는 대부분 문자열이에요. 그런데 키로 쓸 수 있는 자료형이 하나 더 있어요. 만들 때마다 세상에 하나뿐인 값이 되는 Symbol이에요. 직접 만들 일은 많지 않지만, for...of가 배열을 도는 원리처럼 매일 쓰는 기능 밑바닥에 심볼이 있어요.
01. 만들 때마다 하나뿐인 값
Symbol()을 부르면 새 심볼이 생겨요. 괄호 안의 문자열은 설명일 뿐이라서, 설명이 같아도 서로 다른 값이에요. 설명은 디버깅할 때 어떤 심볼인지 알아보는 이름표 역할을 해요.
const a = Symbol('id');
const b = Symbol('id');
console.log(a === b); // false — 설명이 같아도 다른 값
console.log(typeof a); // symbol
console.log(a.description); // id
console.log(String(a)); // Symbol(id)
console.log(`키: ${a}`); // 문자열에 끼워 넣으면?
마지막 줄은 TypeError가 나요. 심볼은 문자열로 저절로 바뀌지 않아요. 실수로 화면에 찍히지 않게 막아 둔 거예요. 글자로 보고 싶으면 String(a)나 a.description처럼 직접 바꿔요.
02. 남의 객체에 겹치지 않는 키 붙이기
심볼이 쓸모 있는 곳은 내가 만들지 않은 객체에 정보를 붙일 때예요. 라이브러리가 넘겨준 객체에 id라는 키를 붙이면, 그 라이브러리가 이미 id를 쓰고 있을 때 값을 덮어써요. 심볼 키는 어떤 문자열 키와도 겹치지 않아요.
const user = { name: '하늘' }; // 다른 코드가 만든 객체라고 해요
const trackId = Symbol('trackId'); // 내 코드만 아는 키
user[trackId] = 'a1b2'; // 심볼 키는 대괄호로 써요
console.log(user[trackId]); // a1b2
console.log(Object.keys(user)); // ['name'] — 심볼 키는 빠져요
console.log(JSON.stringify(user)); // {"name":"하늘"} — 여기서도 빠져요
for (const key in user) console.log(key); // name
console.log(Reflect.ownKeys(user)); // 하지만 이렇게 하면 다 보여요
심볼 키는 Object.keys, for...in, JSON.stringify에서 빠져요. 그래서 원래 코드가 객체를 돌거나 서버로 보낼 때 방해가 되지 않아요. 다만 비밀은 아니에요. Reflect.ownKeys나 Object.getOwnPropertySymbols로 누구나 꺼낼 수 있어요. 정말 밖에서 못 보게 하려면 클래스의 #비공개 필드(9장)를 써요.
심볼을 파일이나 모듈 사이에서 같이 써야 하면 Symbol.for('이름')을 써요. 같은 이름으로 부르면 늘 같은 심볼을 돌려줘요. 대신 이름이 같으면 겹치니까, 'myapp.cache'처럼 앞에 내 앱 이름을 붙여요.
console.log(Symbol.for('myapp.cache') === Symbol.for('myapp.cache')); // true
console.log(Symbol('myapp.cache') === Symbol('myapp.cache')); // false
03. Symbol.iterator: for...of가 도는 원리
자바스크립트에는 미리 만들어 둔 심볼이 몇 개 있어요. 가장 자주 만나는 게 Symbol.iterator예요. for...of와 펼치기([...x])는 대상의 [Symbol.iterator] 메서드를 불러서 값을 하나씩 받아요. 배열 · 문자열 · Map · Set에는 이 메서드가 있고, 보통 객체에는 없어요. 그래서 객체를 for...of로 돌리면 is not iterable 오류가 나요.
console.log(typeof [][Symbol.iterator]); // function — 배열은 돌 수 있어요
console.log(typeof 'abc'[Symbol.iterator]); // function — 문자열도
console.log(typeof ({})[Symbol.iterator]); // undefined — 보통 객체는 없어요
const it = [10, 20][Symbol.iterator](); // for...of가 속에서 하는 일을 손으로
console.log(it.next()); // { value: 10, done: false }
console.log(it.next()); // { value: 20, done: false }
console.log(it.next()); // { value: undefined, done: true } — 끝
[Symbol.iterator]()가 돌려주는 객체를 반복자라고 해요. 반복자는 next() 메서드를 갖고, 부를 때마다 { value, done }을 돌려줘요. for...of는 done: true가 나올 때까지 next()를 불러요. 그러니 내 객체에 이 메서드를 달면 내 객체도 for...of로 돌 수 있어요.
const range = {
from: 1,
to: 3,
[Symbol.iterator]() { // 키 자리에 심볼을 써요
let current = this.from;
const last = this.to;
return { // 반복자
next() {
return current <= last
? { value: current++, done: false }
: { value: undefined, done: true };
},
};
},
};
for (const n of range) console.log(n); // 1, 2, 3
console.log([...range]); // [1, 2, 3]
반복자를 이렇게 손으로 만드는 일은 실무에서 드물어요. 보통은 배열로 바꿔서 돌거나 제너레이터(12장)로 짧게 써요. 돌 수 있는 객체의 조건은 이터러블과 유사 배열에서 더 다뤄요. 여기서는 "for...of는 Symbol.iterator를 부른다"는 것만 알아 두면, is not iterable 오류를 만났을 때 원인을 바로 알 수 있어요.
재생 목록 객체를 for...of로 돌리려니 playlist is not iterable 오류가 나요. playlist에 메서드 하나를 더해서 곡 이름 세 개가 차례로 나오게 해 보세요. 반복자를 직접 만들지 않아도 돼요.
const playlist = {
title: '산책할 때',
songs: ['봄', '여름', '가을'],
};
for (const song of playlist) console.log(song);
해답 보기
for...of는 playlist[Symbol.iterator]를 찾아요. 곡은 이미 배열 songs에 있고, 배열에는 반복자를 만드는 메서드가 있어요. 그래서 배열의 반복자를 그대로 돌려주면 돼요.
const playlist = {
title: '산책할 때',
songs: ['봄', '여름', '가을'],
[Symbol.iterator]() {
return this.songs[Symbol.iterator]();
},
};
for (const song of playlist) console.log(song); // 봄, 여름, 가을
04. 실전 포인트
- 언제 쓰나요? 남이 만든 객체에 내 정보를 붙일 때, 라이브러리를 만들며 사용자 키와 겹치지 않는 키가 필요할 때, 내 자료구조를
for...of로 돌게 할 때(Symbol.iterator) 써요. - 대신 쓰는 것: 숨기는 게 목적이라면 클래스의
#비공개필드를 써요. 객체를 바꾸지 않고 정보를 붙이고 싶다면 객체를 키로 쓰는WeakMap도 있어요. 정해진 값 몇 개 중 하나(상태 이름 같은 것)는 심볼보다 문자열 상수가 읽고 저장하기 편해요. - 흔한 실수: 심볼 키를
user.trackId처럼 점으로 읽는 것(대괄호user[trackId]로 읽어요), 심볼 키를 넣은 객체를JSON.stringify했는데 값이 사라졌다고 놀라는 것, 템플릿 문자열에 심볼을 그대로 넣는 것이에요.
정리
Symbol('설명')은 만들 때마다 하나뿐인 값이에요. 설명이 같아도 서로 달라요.- 심볼 키는 다른 키와 겹치지 않고,
Object.keys·for...in· JSON에서 빠져요. 하지만 비밀은 아니에요. Symbol.for('이름')은 같은 이름이면 같은 심볼을 돌려줘요.for...of는[Symbol.iterator]()로 반복자를 받아next()를done이 될 때까지 불러요.
Symbol('id') === Symbol('id')의 결과와 그 이유는?
false예요. Symbol()은 부를 때마다 새로운 값을 만들고, 괄호 안 문자열은 이름표 같은 설명일 뿐이라 비교에 쓰이지 않아요.
심볼 키에 넣은 값은 JSON.stringify 결과에 들어갈까요? 그럼 남이 절대 못 보는 값일까요?
JSON에는 들어가지 않아요. 하지만 Reflect.ownKeys나 Object.getOwnPropertySymbols로 누구나 꺼낼 수 있어서 비밀은 아니에요.
보통 객체를 for...of로 돌리면 왜 is not iterable 오류가 날까요?
for...of는 대상의 [Symbol.iterator] 메서드로 반복자를 받는데, 보통 객체에는 이 메서드가 없어서예요. 키나 값을 돌고 싶으면 Object.keys(obj)처럼 배열로 바꿔서 돌아요.