본문으로 건너뛰기
기술스택 · stack/javascript/심볼: 다른 키와 절대 겹치지 않는 키

JavaScript · 4장 객체

심볼: 다른 키와 절대 겹치지 않는 키

객체의 키는 대부분 문자열이에요. 그런데 키로 쓸 수 있는 자료형이 하나 더 있어요. 만들 때마다 세상에 하나뿐인 값이 되는 Symbol이에요. 직접 만들 일은 많지 않지만, for...of가 배열을 도는 원리처럼 매일 쓰는 기능 밑바닥에 심볼이 있어요.

#4.7

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]
①for...of는 먼저 range[Symbol.iterator]()를 불러 반복자를 받아요. 그다음 next()를 부를 때마다 값을 하나씩 받아 n에 넣고, done이 true인 결과가 오면 반복을 끝내요.

반복자를 이렇게 손으로 만드는 일은 실무에서 드물어요. 보통은 배열로 바꿔서 돌거나 제너레이터(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)처럼 배열로 바꿔서 돌아요.

SIMPLE · 기술스택 / JavaScript — 4장 객체