Symbol 은 어떤 친구일까?

차차·2024년 1월 31일
1

Symbol 이란?

  • ES6 에서 추가된 7번째 데이터 타입 (primitive type)
  • 다른 코드와 충돌되지 않는 고유한 값이 필요할 때 사용
  • 객체 프로퍼티의 키로 허용
  • ID, 토큰과 같은 식별자 역할

Symbol 의 도입

정보 은닉을 위해, 객체 내에서 private 한 속성 값을 지원하기 위해 도입되었다.
따라서 초기 사양에서는 공개 심볼, 비공개 심볼이 존재했다. 비공개 심볼은 get 과 set 연산을 통해서만 관리되는 숨겨진 데이터이다.

하지만 프록시와의 조합에서 프록시가 모든 메타오브젝트 연산(get, set, has)을 가로채면서 비공개 심볼의 의도와 어울리지 않는 문제가 발생했다. 따라서 비공개 심볼은 사양에서 제거되며 공개 심볼만 특정 용도로 쓰이고 있다.

  • Proxy는 이런 친구이다!
    // 타깃 객체
    const targetObject = {
      name: 'John',
      age: 30
    };
    
    // 프록시 생성
    const proxy = new Proxy(targetObject, {
      get: function(target, property, receiver) {
        console.log(`Getting property: ${property}`);
        return target[property];
      },
      set: function(target, property, value, receiver) {
        console.log(`Setting property: ${property} to ${value}`);
        target[property] = value;
        return true;
      }
    });
    
    // 프록시를 통한 객체 접근
    console.log(proxy.name); // Getting property: name, John
    proxy.age = 31; // Setting property: age to 31

Symbol 만들기

Symbol() 팩토리 함수를 사용하여 심볼을 생성할 수 있으며, 선택적으로 값을 전달하여 생성된 심볼에 설명을 부여할 수 있다.

생성 함수 Symbol

Symbol: SymbolConstructor(description?: string | number | undefined) => symbol

생성 예시

const symbol1 = Symbol();
const symbol2 = Symbol('symbol');
const symbol3 = Symbol(0);

// new 연산자는 지원하지 않는다
const symbol4 = new Symbol();
/* 
	'new' expression, whose target lacks a construct signature,
	implicitly has an 'any' type.
*/

Description

const mySymbol = Symbol('This is a description');
console.log(mySymbol.description); // 'This is a description'

고유한 값 symbol

console.log(Symbol('bar') === Symbol('bar'));
// false

Symbol 사용하기 #1

Symbol 은 어떻게 써야 할까?

Symbol.for()

주어진 키를 사용하여 심볼을 찾고, 존재할 경우 해당 심볼을 반환한다. 존재하지 않는다면 해당 키로 새로운 심볼을 생성하여 반환한다.

💡 어디서 찾는걸까 !

심볼은 어플리케이션 내에서 전역 심볼 레지스트리에 존재한다. 즉, 브라우저 환경 내에서 실행 중인 모든 코드는 하나의 심볼 저장소를 보고 있다는 것이다. 따라서 모든 실행 컨텍스트(iframe, 웹 워커 등등)에서 심볼을 공유할 수 있다.

console.log(Symbol.for('bar') === Symbol.for('bar'));
// true

Symbol.keyFor()

주어진 심볼의 키(description)를 반환한다.

const mySymbol = Symbol.for('hello');

console.log(Symbol.keyFor(mySymbol));
// hello

Symbol.prototype.toString()

주어진 심볼 값을 나타내는 문자열을 반환한다. ”Symbol(description)”

심볼을 문자열화하려면 해당 심볼의 toString() 메서드를 호출하거나 String() 을 호출해야 한다.

const mySymbol = Symbol.for('hello');

console.log(mySymbol.toString());
// Symbol(hello)

console.log(String(mySymbol));
// Symbol(hello)

console.log(`${mySymbol}`);
// error

Symbol 사용하기 #2

그렇다면 Symbol 은 언제 쓰면 좋을까?

상수 역할

상수가 필요한 상황에서, 문자열을 할당하여 사용할 수 있다.

// 방향값을 상수로
const DIREC_UP = 'up';
const DIREC_DOWN = 'down';

하지만 이러한 값들이 완전 고유하다고 할 수는 없다. 어플리케이션이 완전 커진 상태라고 가정하면, 먼 곳에서 아래와 같은 상수가 생길 수도 있는 것이다.

// 증감여부를 상수로
const COUNT_UP = 'up';
const COUNT_DOWN = 'down'

결국 DIREC_UP === COUNT_UP 이기 때문에, 의미는 달라도 값이 같은 이상한 상수가 탄생한다.

💡 뭐가 문제인가요?

direction 만을 받아서 이동하는 함수 move 가 있을 때, move(COUNT_UP)move(DIREC_UP) 과 동일하게 동작하게 된다.
= 의도와 다른 상황 발생

symbol 을 활용하면 이러한 문제를 해결할 수 있다.

const DIREC_UP = Symbol('up');
const DIREC_DOWN = Symbol('down');
type Direction = typeof DIREC_UP | typeof DIREC_DOWN

const COUNT_UP = Symbol('up');
const COUNT_DOWN = Symbol('down');

const move = (
	direction: Direction
) => {
	console.log(direction.description);
}

move(DIREC_DOWN)// down
move(COUNT_UP)  // TypeError

의도에 맞게, move 함수에 방향과 관련 없는 식별자가 전달되었을 경우를 걸러줄 수 있다.

enum 역할

위의 사례를 조금 더 발전시킨 것으로, 값 자체에는 의미가 없으나 상수 이름에 의미가 있는 열거형 자료형의 경우 symbol 을 통해 구현할 수 있다.

타입스크립트에서는 enum 자료형을 제공한다.
순수 자바스크립트에서 enum 을 구현하고자 할 때 Symbol 을 활용하면 된다는 것!

const Direction = Object.freeze({ // 객체 동결
	UP: Symbol('up'),
	DOWN: Symbol('down'),
	LEFT: Symbol('left'),
	RIGHT: Symbol('right'),
}); 

let direction = Direction.UP;

switch (direction) {
  case Direction.UP:
    console.log('Go up');
    break;
  case Direction.DOWN:
    console.log('Go down');
    break;
  case Direction.LEFT:
    console.log('Go left');
    break;
  case Direction.RIGHT:
    console.log('Go right');
    break;
  default:
    break;
}

실제 사용 예시로, React/hydration.js 에서 hydration 을 위한 메타데이터가 아래와 같이 작성되어있다.

export const meta = {
  inspectable: Symbol('inspectable'),
  inspected: Symbol('inspected'),
  name: Symbol('name'),
  preview_long: Symbol('preview_long'),
  preview_short: Symbol('preview_short'),
  readonly: Symbol('readonly'),
  size: Symbol('size'),
  type: Symbol('type'),
  unserializable: Symbol('unserializable'),
};

고유한 객체 프로퍼티 키

객체의 키값으로 symbol 타입이 허용되며, 타입스크립트의 인덱스 시그니쳐에서도 사용될 수 있다.
symbol 은 고유한 값이기 때문에, 객체의 프로퍼티 키로 사용될 때 다른 어떤 키와도 충돌하지 않는다.
아래와 같이 전역 스토어를 구현할 때 활용해 보았다. 고유성을 보장하고, 실수로 같은 키값을 넣었을 때의 에러를 방지할 수 있기 때문이다.

class Store {
	constructor(){
		this.data = {}; 
		// { [key: symbol]: any }
	}
	addData(key, value){
		const storeKey = Symbol(key);
		this.data[storeKey] = value;
		return storeKey;
	}
	getData(storeKey){
		return this.data[storeKey]
	}
	// 생략
}
// store 초기화 파일
export const messageKey = store.addData('message', 'hello');

// 사용 컴포넌트
import {messageKey} from '...'
const message = store.getData(messageKey); // hello

객체 프로퍼티 숨기기

symbol 값을 키로 사용하여 생성한 속성은 for … in 과 같은 반복문이나 Object.keys, Object.getOwnPropertyNames 와 같은 객체 메서드로 찾을 수 없다.

이런 특징을 활용하여, 외부에 노출하면 안되는 (또는 굳이 노출 안해도 되는) 속성을 숨길 수 있다.

const obj = {
	[Symbol("hiddenMessage")]: "hello",
	name: "chacha",
	age: 25
}

for (const [key,value] of obj){
	console.log(key, ':', value);
}
// name : chacha
// age : 25

console.log(Object.keys(obj)); // ['name', 'age']
console.log(Object.getOwnPropertyNames(obj)); // ['name', 'age']

하지만, Object.getOwnPropertySymbols 메서드를 사용하면 symbol 값을 키로 사용한 프로퍼티를 찾을 수 있다.

const hiddenMessageKey = Object.getOwnPropertySymbols(obj)[0];
console.log(obj[hiddenMessageKey]); // hello

XSS 공격 방지

추가로, symbol 을 사용한 속성이라면 JSON.stringify 에서 반환된 결과에 포함되지 않는다.
React 에서는 이를 활용하여 XSS(크로스 사이트 스크립팅) 공격을 방지하고 있다.

React.createElement 로 생성되는 React Element 는 $$typeof 라는 특별한 속성을 지니고 있다. 이 프로퍼티는 React 가 해당 객체가 유효한 React Element 인지 판별하기 위해 사용된다.
$$typeof 의 유효한 값은 Symbol(react.element) 이다.

// 올바르게 생성된 React element 객체
{
  type: 'div',
  props: {
	prop1: 'this is prop',
    children: 'this is children'
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element'),
}

아래 예시로 보면, 서버에서 사용자 프로필 정보를 가져와 화면에 렌더링을 하고자 한다.
하지만, 서버에서는 악의적인 스크립트가 담긴 사용자 프로필을 보냈다..!

const UserProfile = (props) => {
	// 가정: 서버에서 사용자의 프로필 정보를 가져옴
  const {username, description} = getUserProfileFromServer();

  return (
    <div>
      <h2>{username}'s Profile</h2>
      {description}
    </div>
  );
}

function getUserProfileFromServer() {
  return {
    username: 'chacha',
    description: {
	    type: 'div',
	    props: {
	      dangerouslySetInnerHTML: {
	        __html: <script>alert('XSS attack!')</script>,
	      },// 사용자의 프로필 소개에 악의적인 스크립트를 담음
	    },
	  },
  };
}

export default UserProfile;

이 코드를 완전 그래도 실행하게 되면, 악의적인 스크립트가 브라우저에서 실행되어 사용자의 개인 정보를 뺏어가거나 보안 문제를 일으킬 수 있다.

리액트에서는 이러한 값이 서버에서 내려왔을 때의 위험을 방지하기 위해 symbol 값을 가지는 $$typeof 필드를 넣었다.
서버에서 악의적인 스크립트가 포함된 JSON 을 내려준다면, 해당 JSON 에는 symbol 값을 가지는 $$typeof 친구가 없을 것이다.
따라서 이는 리액트 내에서 생성된 정상 element 가 아니라 판단하여 실행하지 않는다.

정리하자면,

리액트는 $$typeof 에 올바른 symbol 값이 있을 때만 해당 element 를 렌더링한다.
그리고 서버에서 JSON 으로 element 를 내려준다면 symbol 값이 들어있지 않으므로 렌더링하지 않는다.

이로 인해 XSS 공격을 막을 수 있다.

0개의 댓글