

스토리북이나 DOM 요소를 콘솔에 출력해보면, 다음과 같은 코드들을 한 번쯤은 보신 적 있을 겁니다. 그중에서도 저는 Symbol이라는 표현에 특히 눈길이 갔는데요.
자바스크립트의 타입 중 하나라는 건 알고 있었지만, 실제로 활용해본 적도 없었고, 코드에서 직접 마주친 것도 처음이었습니다. 그래서 궁금증이 생겨 조금 더 자세히 알아보게 되었습니다.
(이 글에는 개인적인 추측이 일부 포함되어 있어, 모든 내용이 100% 정확하다고 보장할 수는 없습니다!)
생성자가 Symbol 원시 값을 반환하는 내장 객체입니다. 고유한 키를 부여하여 다른 코드와 충돌하지 않도록 할 때 많이 쓰이며, 이렇게 추가한 속성은 일반적인 방법으로는 접근할 수 없기에 약한 형태의 캡슐화, 정보 은닉을 제공합니다.
외부로 유출되지 않아 확인할 수 없습니다.
const secretSymbol = Symbol('secret');
// 일반적인 방법으로는 접근할 수 없음
Object.keys({ [secretSymbol]: 'value' }); // []
Object.getOwnPropertyNames({ [secretSymbol]: 'value' }); // []
다른 값과 절대 중복되지 않는 유일무이한 값입니다.
new 연산자와 함께 쓰이지 않습니다. 보통 new 연산자와 함께 사용되면 객체(인스턴스)가 생성되지만 Symbol값은 변경 불가능한 원시 값이기에 객체를 생성하지 않습니다.
심볼 값 자체는 문자열/숫자 타입으로 반환하지 않습니다. 다만, 불리언 타입으로는 암묵적으로 타입 변환이 됩니다. 따라서 조건문을 통해 존재 확인이 가능합니다.
const mySymbol1 = Symbol('sebin')
const mySymbol2 = Symbol('sebin')
console.log(mySymbol1 === mySymbol2) // false

리액트 코드를 보다보면 다음과 같은 검증 로직을 자주 볼 수 있었습니다. 이 한 줄의 코드가 왜 이렇게 정교하게 설계되었을까요? 단순한 타입 체크라면 typeof만으로도 충분할 텐데요.
가추를 해봅시다.
앞서 Symbol을 사용하면 정보 은닉이 가능하고 다른 값과 절대 중복되지 않는 유일무이한 값이라고 했습니다. 이점을 미루어 생각해보면, 보안/안전성과 연관이 있지 않을까요?
if(elementType ==='object' && type !== undefined && type.$$typeof === REACT_ELEMENT_TYPE)
이제 하나씩 파헤쳐 보겠습니다. 이 코드 한 줄만 해석해봅시다.
typeof element === 'object' - 객체인지 확인element !== null - null이 아닌지 확인element.$$typeof === REACT_ELEMENT_TYPE - Symbol 검증단순한 타입 체크처럼은 안보이지 않나요? 대체 어떤 상황들을 막고 있는 걸까요? 하나씩 파헤쳐 보겠습니다.
리액트의 $$typeof 속성은 Symbol(react.element) 같은 고유한 심볼 값을 가지는데, 이 심볼은 JSON으로 직렬화되지 않기 때문에 서버로부터 주입된 문자열 데이터가 React Element처럼 위장하는 것을 방지할 수 있습니다.
예를 들어, 누군가가 서버 응답을 조작해 아래와 같은 악성 데이터를 클라이언트로 보내려 한다고 가정해봅시다.
const maliciousServerResponse = `{
"$$typeof": "Symbol(react.element)",
"type": "div",
"props": {
"dangerouslySetInnerHTML": {
"__html": "<script>alert('XSS Attack!')</script>"
}
}
}`;
해당 데이터를 파싱하면 단순한 객체가 생성될 뿐, 실제 리액트가 인식하는 React Element가 아닙니다.
const parsedData = JSON.parse(maliciousServerResponse);
console.log(parsedData.$$typeof); // "Symbol(react.element)" - 문자열
console.log(REACT_ELEMENT_TYPE); // Symbol(react.element) - 실제 Symbol
console.log(parsedData.$$typeof === REACT_ELEMENT_TYPE); // false!
리액트는 $$typeof가 실제 심볼인지 확인하는 로직을 통해, 이 객체를 유효하지 않은 요소로 판단하고 렌더링을 거부합니다. 이로 인해 악성 스크립트가 화면에 나타나는 것을 차단할 수 있습니다
개발자가 실수로 만든 유사 객체와 진짜 React Element를 확실히 구분할 수 있습니다.
const fakeElement = {
type: 'div',
props: { children: 'Hello' } // $$typeof가 없음
};
const realElement = React.createElement('div', null, 'Hello');
겉보기에는 비슷하지만, React.createElement 를 통해 생성된 진짜 Element는 $$typeof 속성에 심볼 값을 가지고 있어 리액트가 이를 식별할 수 있습니다.
console.log('가짜 요소:', isValidElement(fakeElement)); // false
console.log('진짜 요소:', isValidElement(realElement)); // true
try {
ReactDOM.render(fakeElement, container); // 에러 발생
} catch (error) {
console.log('타입 안전성 보장');
}
리액트는 내부적으로 $$typeof가 실제 Symbol(react.element)인지 확인하여, 가짜 요소에 대해선 렌더링을 거부하고 에러를 발생시킵니다. 이처럼 심볼은 타입 안전성을 확보하는 데 큰 역할을 합니다.
리액트의 내부 구조는 외부 코드에 의해 변조되지 않아야 안전하게 동작할 수 있습니다. 하지만 일반 문자열이나 상수를 사용한다면, 누군가 전역 객체를 덮어써서 구조를 오염시킬 수 있습니다.
// 다른 라이브러리나 악의적 코드의 오염 시도
window.REACT_ELEMENT_TYPE = 'hacked'; // 불가능
Symbol.for('react.element') = 'fake'; // 불가능
하지만 심볼은 이런 시도를 원천 차단합니다.
Symbol.for()는 전역 심볼 레지스트리를 통해 고유한 심볼을 관리합니다.// 전역 Symbol 레지스트리 조작 시도
try {
delete Symbol.for; // 불가능
Symbol.for = () => 'fake'; // 불가능
} catch (e) {
console.log('Symbol 레지스트리는 보호됨');
}
// React의 내부 Symbol은 안전하게 보호
const safeSymbol = Symbol.for('react.element');
console.log(typeof safeSymbol); // "symbol"
이처럼 심볼을 사용함으로써 외부 코드나 다른 라이브러리가 리액트의 내부 구조를 변조하거나 덮어쓰는 것을 막아 안정성을 확보할 수 있습니다.
심볼이 지닌 보안적 특성을 이해하게 되니, 서버사이드 렌더링처럼 민감한 상황에서 데이터 검증이 왜 중요한지, 또 타입 안전성을 고려한 컴포넌트 설계가 왜 필요한지 자연스럽게 체감하게 되었습니다.
리액트의 심볼은 단순한 기술적 선택이 아니라, 페이스북이 제품을 만들며 마주했던 현실적인 보안 위협에 대한 깊은 고민의 결과물이라고 느껴졌습니다. 그리고 이 낯선 JavaScript 타입 하나가, 우리 애플리케이션의 보안을 지키는 데 얼마나 중요한 역할을 하고 있었는지 새삼 깨닫게 되었습니다.
우테코 레벨 1 당시 실제 DOM을 직접 다루는 미션이 있었는데, 그때 “혹시 XSS 공격이 일어나면 어떡하지?” 하고 걱정했던 기억이 납니다. 지금 돌아보면, 그 고민은 리액트 팀이 이미 수년 전에 깊이 고민했던 문제였고, 그 해답이 바로 Symbol이라는 형태로 구현되어 있었던 것이 아닐까 싶습니다. 해결책은 생각보다 가까운 곳에 있었던 것이죠.
끗.