본문 바로가기
프론트엔드

[React] React-dom에서의 innerHTML

by ISA(류) 2022. 5. 31.

리액트의 경우 리액트(jsx,정의 등)와 렌더러(웹의 경우 보통 react-dom)를 사용해서 어플리케이션의 UI를 렌더링한다.
렌더러의 경우 그 내용이나 구조등이 복잡한 편이라 정리해서 쪼개서 글을 쓰자니 너무 양이 많아지는 문제점이 있다.
그래서 소스코드 일부를 작게작게 쪼개서 정리하는 포스팅을 추가적으로 하기로 했다.

리액트의 경우 v-dom을 만들어서 DOM 자체를 리액트에서 관리하다보니 XSS 공격등 상대적으로 프론트엔드 영역에서의 보안이 안정적이다. 내부 dom을 직접 모두 관리하고 innerHTML등의 기존 js API를 최대한 사용하지 않아서 그런데 문제는 개발을 하다보면 innerHTML등의 API가 필요한 상황이 존재한다는 것이다.

HTML 태그가 포함된 문자열을 DOM으로 추가하는 경우 등이 그렇다. 이럴 경우 리액트에서 권장하진 않지만dangerouslySetInnerHTML라는 것을 사용해서 특정 위치에 HTML 문자열을 innerHTML 할 수 있다.
물론 그냥 innerHTML로도 가능하지만 v-dom. 즉 리액트에서 해당 동작을 제어 할 수 없다는 점으로 인한 성능상 이슈 때문에 해당 API를 사용하지 않을 이유는 없다. (직접 dom에 접근하는 것으로 일종의 비제어 동작에 해당한다.)해당 API의 경우 결국 근본적으로 innerHTML 프로퍼티를 사용하는데 아래와 같다.

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import {SVG_NAMESPACE} from '../shared/DOMNamespaces';
import createMicrosoftUnsafeLocalFunction from './createMicrosoftUnsafeLocalFunction';
import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags';

// SVG temp container for IE lacking innerHTML
let reusableSVGContainer;

/**
 * Set the innerHTML property of a node
 *
 * @param {DOMElement} node
 * @param {string} html
 * @internal
 */
const setInnerHTML = createMicrosoftUnsafeLocalFunction(function(
  node: Element,
  html: {valueOf(): {toString(): string, ...}, ...},
): void {
  if (node.namespaceURI === SVG_NAMESPACE) {
    if (__DEV__) {
      if (enableTrustedTypesIntegration) {
        // TODO: reconsider the text of this warning and when it should show
        // before enabling the feature flag.
        if (typeof trustedTypes !== 'undefined') {
          console.error(
            "Using 'dangerouslySetInnerHTML' in an svg element with " +
              'Trusted Types enabled in an Internet Explorer will cause ' +
              'the trusted value to be converted to string. Assigning string ' +
              "to 'innerHTML' will throw an error if Trusted Types are enforced. " +
              "You can try to wrap your svg element inside a div and use 'dangerouslySetInnerHTML' " +
              'on the enclosing div instead.',
          );
        }
      }
    }
    if (!('innerHTML' in node)) {
      // IE does not have innerHTML for SVG nodes, so instead we inject the
      // new markup in a temp node and then move the child nodes across into
      // the target node
      reusableSVGContainer =
        reusableSVGContainer || document.createElement('div');
      reusableSVGContainer.innerHTML =
        '<svg>' + html.valueOf().toString() + '</svg>';
      const svgNode = reusableSVGContainer.firstChild;
      while (node.firstChild) {
        node.removeChild(node.firstChild);
      }
      while (svgNode.firstChild) {
        node.appendChild(svgNode.firstChild);
      }
      return;
    }
  }
  node.innerHTML = (html: any);
});

export default setInnerHTML;

해당 코드에서 몇가지 특이 사항을 알 수 있는데 리액트 dom에서 익스플로러를 어느 정도 지원을 고려하고 있다는 것과 개발 모드일시 svg와 익스 브라우저에 대한 경고를 하고 있다는 것과 브라우저에서 svg 노드 내부 프로퍼티로 innerHTML API를 제공하지 않을 시 임의로 새로운 노드를 만들어서 추가하는 방식으로 대응 한다는 점 정도 알 수 있다.

추가로 마이크로소프트 관련된 unsafe 로직이 있는데 그냥 간단히 MSApp이라는 전역 객체가 있을시 해당 객체를 불러다가 필요한 조치를 취해주는게 전부다. 해당 부분은 내가 관련 개발을 해본 경험이 없으므로 그저 추측할 뿐인데 특정 플랫폼에서 웹뷰로 사용할시 대응해주기 위한 용도가 아닌가 생각이 든다. 아래 주석에는 윈도우 8 앱을 지원하기 위해서라고 나와있다.

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

/* globals MSApp */

/**
 * Create a function which has 'unsafe' privileges (required by windows8 apps)
 */
const createMicrosoftUnsafeLocalFunction = function(func) {
  if (typeof MSApp !== 'undefined' && MSApp.execUnsafeLocalFunction) {
    return function(arg0, arg1, arg2, arg3) {
      MSApp.execUnsafeLocalFunction(function() {
        return func(arg0, arg1, arg2, arg3);
      });
    };
  } else {
    return func;
  }
};

export default createMicrosoftUnsafeLocalFunction;

위의 코드들을 통해서 알 수 있는 점은 리액트의 dangerouslySetInnerHTML은 결국 개발모드에서의 에러 알림과 svg tag에 대한 호환성 지원 이나 MSApp지원 정도 기능이 추가 된 innerHTML이다.
그리고 리액트에서 제공하는 일종의 API이다 보니 해당 프로퍼티를 통해서 innerHTML을 할 경우 컴포넌트 초기화와 업데이트시 업데이트 되는 라이프 사이클을 따른다.

function setInitialDOMProperties(
  tag: string,
  domElement: Element,
  rootContainerElement: Element | Document | DocumentFragment,
  nextProps: Object,
  isCustomComponentTag: boolean,
): void {
  for (const propKey in nextProps) {
    if (!nextProps.hasOwnProperty(propKey)) {
      continue;
    }
    const nextProp = nextProps[propKey];
    if (propKey === STYLE) {
      if (__DEV__) {
        if (nextProp) {
          // Freeze the next style object so that we can assume it won't be
          // mutated. We have already warned for this in the past.
          Object.freeze(nextProp);
        }
      }
      // Relies on `updateStylesByID` not mutating `styleUpdates`.
      setValueForStyles(domElement, nextProp);
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      const nextHtml = nextProp ? nextProp[HTML] : undefined;
      if (nextHtml != null) {
        setInnerHTML(domElement, nextHtml);
      }
	}
    ...
}

DOM 프로퍼티를 초기화하는 단계에서 innerHTML을 하고(setInnerHTML 함수 호출) DOM 프로퍼티를 업데이트 하면서

function updateDOMProperties(
  domElement: Element,
  updatePayload: Array<any>,
  wasCustomComponentTag: boolean,
  isCustomComponentTag: boolean,
): void {
  // TODO: Handle wasCustomComponentTag
  for (let i = 0; i < updatePayload.length; i += 2) {
    const propKey = updatePayload[i];
    const propValue = updatePayload[i + 1];
    if (propKey === STYLE) {
      setValueForStyles(domElement, propValue);
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      setInnerHTML(domElement, propValue);
    } else if (propKey === CHILDREN) {
      setTextContent(domElement, propValue);
    } else {
      setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
    }
  }
}

다시 innerHTML한다. innerHTML한 DOM부분을 감시하는 방법은 없지만 __html으로 넘기는 값을 컴포넌트에서 감시 할 수 있고 호환성이나 간단한 경고등을 지원해주기에 리액트에서는 innerHTML을 직접하지 않는게 좋다. 물론 해당 방식을 통한 접근 역시 XSS등 보안 이슈와 리액트의 매커니즘에 적합하지 않기에 권장되진 않는다.

이렇게 리액트 dangerouslySetInnerHTML가 그냥 innerHTML과 무슨 차이가 있는지 간단히 알아보았다.

반응형

'프론트엔드' 카테고리의 다른 글

프론트엔드 설계 고민 -5-  (0) 2022.07.10
[JS]스와이프 기능을 만들어보며.  (0) 2022.07.02
프론트엔드 설계 고민 -4-  (0) 2022.05.21
프론트엔드 설계 고민 -3-  (0) 2022.04.26
[React]React와 JSX.  (0) 2022.04.14