최근 개인 블로그 Overreacted를 통해 양질의 포스트를 쏟아내고 있는 Dan AbramovWhy Do React Elements Have a $$typeof Property?를 번역한 글입니다. 지나친 의역 및 오역 지적해주시면 감사하겠습니다.

당신은 (컴포넌트를 쓸 때) JSX를 쓰고 있다고 생각하겠지만

<marquee bgcolor="#ffa7c4">hi</marquee>

사실, 함수를 호출하고 있다.

React.createElement(
  /* type */ 'marquee',
  /* props */ { bgcolor: '#ffa7c4' },
  /* children */ 'hi'
)

그리고 이 함수는 객체를 반환한다. 우리는 이 객체를 React element 라고 부른다. 이 객체는 React에게 무엇을 렌더할지를 알려준다. 즉 당신의 컴포넌트는 객체 트리를 반환하는 셈이다.

{
  type: 'marquee',
  props: {
    bgcolor: '#ffa7c4',
    children: 'hi',
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element'), // 🧐 응? 이건 뭐지?
}

평소 React를 사용한다면 type, props, key, ref 등의 필드에 익숙할 것이다. 그런데 $$typeof는 대체 뭘까? 그리고 왜 Symbol()을 값으로 가지고 있는 걸까?

본론에 앞서, 굳이 React를 쓸 줄 몰라도 질문에 대한 답은 얻을 수 있지만, 그래도 React에 대한 기본 지식이 있다면 이 글을 더 쉽게 이해할 수 있을 것이다. 또한, 이 글에는 보안에 관련된 소소한 팁도 들어있다. 아마 언젠간 당신도 당신만의 UI 라이브러리를 만드는 날이 올텐데, 그 땐 (본론에서 말할) 이 모든 문제가 가볍게 해결되어 있지 않을까 하는 바람으로 글을 시작한다.


클라이언트 사이드의 UI 라이브러리들이 보편화되고 기본적인 보안 장치들이 존재하기 이전엔, HTML을 생성하고 DOM을 주입하기 위해 주로 아래와 같은 방법이 사용되곤 하였다.

const messageEl = document.getElementById('message');
messageEl.innerHTML = '<p>' + message.text + '</p>';

위 코드는 잘 돌아가긴 한다. message.text'<img src onerror="stealYourPassword()">'와 같은 값이 들어가지만 않는다면 말이다. 남이 작성한 코드가 당신의 앱의 렌더된 HTML에 떡하니 주입(인젝션)되는 걸 당신은 원치 않을 것이다.

(재밌는 사실: 만약 당신의 앱이 클라이언트 사이드 렌더링만 한다면, <script> 태그가 주입된다 하여도 그 안의 자바스크립트는 실행되지 않을 것이다. 그렇다고 해서 보안을 소홀히 하는 건 금물.)

위와 같은 공격을 막기 위해, 오로지 텍스트만 다루는 document.createTextNode()textContent와 같은 안전한 API들을 사용할 수 있다. 또한 예방 차원에서, 잠재적인 위험성을 가지고 있는 <, >등의 문자들을 입력값에서 미리 이스케이프(escape)할 수도 있다.

그러나 매번 유저가 작성한 입력값에 대해 이와 같은 보간 작업을 거쳐야하는 건 여간 귀찮은 일이 아니다. 실수를 했을 시 발생하는 비용 또한 크다. 이 때문에 React 같은 모던 라이브러리에선 문자열 텍스트에 대한 이스케이핑이 기본으로 지원되고 있다:

<p>
  {message.text}
</p>

만약 message.text<img>나 여타 다른 수상한 태그 문자열이 들어오면, React는 이를 실제 <img> 태그로 변환하지 않는다. React는 먼저 입력값을 이스케이프한 뒤 DOM에 주입시킨다. 결과적으로 <img> 태그가 나오는 대신 단순한 마크업 코드만 표시된다.

임의의 HTML을 React element 안에 넣어야하는 상황이라면, dangerouslySetInnerHTML={{ __html: message.text }} 를 사용하면 된다. 코드가 다소 투박하게 보일 수 있는데, 이는 사실 의도된 설계이다. 코드가 눈에 띄기 때문에, 코드 리뷰나 코드베이스 audit시 어느 부분에 보안 취약점이 있는지 쉽게 캐치할 수 있다.


그럼 React는 주입 공격으로부터 완전히 안전한 것일까? 그렇지 않다. HTML과 DOM은 리액트나 여타 UI 라이브러리들이 방어하기 어려운 다양한 공격들에 노출되어 있다. 대부분의 공격은 속성(attributes)을 통해 이루어진다. 예를 들어, <a href={user.website}>를 렌더링하는 경우, 어떤 user의 website 값에는 'javascript: stealYourPassword()'같은 값이 들어가 있을 수 있다. user의 입력값을 <div {...userData}> 와 같이 스프레딩(Spread)하여 넣는 것도 (드물긴 하지만) 위험한 경우이다.

물론 React가 더 많은 보안 대책을 제공할 수는 있지만, 이와 같은 문제는 보통 서버 이슈로 인해 생기는 경우가 많아서 결국엔 서버 쪽에서 이를 고쳐야만 한다.

그럼에도 여전히 텍스트 입력값을 이스케이핑하는 것은 잠재적인 공격을 합리적으로 방어할 수 있는 좋은 첫 걸음이라고 생각한다. 이런 식으로 코드를 짜는 거만으로 안심하고 쓸 수 있으니 얼마나 좋은가?

// 자동으로 이스케이핑됨
<p>
  {message.text}
</p>

그러나, 이 역시 항상 옳은 방법은 아니었다. 이 시점에서 바로 $$typeof 가 등장한다.


React element 는 객체로 설계되어있다:

{
  type: 'marquee',
  props: {
    bgcolor: '#ffa7c4',
    children: 'hi',
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element'),
}

일반적으로 React.createElement() 로 React element를 생성하지만, 꼭 그럴 필요는 없다. 방금 위에 적은 코드처럼 일반 객체를 사용해 element를 생성하는 사례들도 많다. 물론 이런 식으로 코드를 쓰고 싶진 않겠지만, 컴파일러 최적화나 worker 간 UI 엘리먼트 전달, React 패키지로부터 JSX를 분리시키는 등의 작업에선 이 방법이 더 효율적일 수 있다.

그런데, 만약 당신의 서버에 구멍이 생겨, (원래는 문자열로 입력을 받아야 하는데) 유저가 임의의 JSON 객체를 서버에 저장할 수 있는 문제가 발생했다고 하자. 클라이언트 쪽 코드에선 당연히 해당 정보를 문자열로 받게끔 설계되어 있을테니 문제가 발생하게 된다:

// 서버에 구멍이 생겨 JSON이 저장되었다고 가정하자.
let expectedTextButGotJSON = {
  type: 'div',
  props: {
    dangerouslySetInnerHTML: {
      __html: '/* put your exploit here */'
    },
  },
  // ...
};
let message = { text: expectedTextButGotJSON };

// React 0.13에서 이는 위험할 수 있다.
<p>
  {message.text}
</p>

이런 경우, React 0.13 버전은 XSS 공격에 취약할 수 있다. 다시 강조하지만 이는 서버의 결함으로 인해 발생하는 문제이다. 그러나 이런 공격들로부터 사용자를 지키기 위해 React는 더욱 개선되어야 했다. 그렇게 React 0.14 버전이 나왔고, 해당 문제는 해결되었다.

해결 방법은 바로, '모든 React element에 Symbol 태그를 달기'이다:

{
  type: 'marquee',
  props: {
    bgcolor: '#ffa7c4',
    children: 'hi',
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element'),
}

JSON에는 Symbol를 넣을 수 없다. 즉, 설사 서버에 보안 구멍이 생겨 텍스트 대신 JSON을 반환한다 하더라도, 그 JSON에는 Symbol.for('react.element') 코드를 포함시킬 수 없다. React는 element.$$typeof 를 체크하여, 해당 키가 없거나 무효하면 React element 생성을 거부한다.

Symbol.for() 가 특별히 좋았던 점은, iframes이나 workers와 같은 여러 환경에서 전역적으로 사용할 수 있다는 것이다. 그러므로 신뢰할 수 있는 React element라면 앱 속 각기 다른 환경들에서 서로 element를 전달하며 사용하는 것이 가능해진다. 마찬가지로, 여러 카피의 React가 페이지에 존재한다고 해도, $$typeof 의 값이 유효하면 상관없이 element를 사용할 수 있다.


그럼 Symbol을 지원하지 않는 브라우저들은 어떨까?

아쉽게도 이들은 방금 위에서 언급한 혜택을 받을 수 없다. 일관성을 위해 element에는 언제나 $$typeof 필드가 포함되어 있으나, Symbol를 지원하지 않는 환경에선 $$typeof 값에 Symbol 대신 number가 들어가게 된다 - 0xeac7.

왜 하필 이 숫자냐고? 0xeac7를 잘 보면 “React”처럼 보이니까.