스크립트 태그가 포함된 HTML 삽입(SSG/SSR 지원)

29698 단어 Reacttech
JAMstack을 사용하는 웹 매체 등이라는 것은 콘텐츠 구성이 보도에 따라 조금씩 다르지만 표기상 표현력이 제한돼 있어 본문 등 내용의 일부를 HTML로 기술하려는 경우도 상응한다.
이 경우 외부 서비스에서 제공하는 스크립트 탭을 포함하는 내용을 표시하려는 요구에 대응합니다.

dangerouslySetInnerHTML의 제한


dangerouslySetInnerHTMLReact를 사용하여 HTML을 임의의 DOM 요소의 하위 요소로 삽입할 수 있습니다.
그러나 이 기능은 내부에서 사용innerHTML.innerHTML XSS 공격에 대한 대책으로 표시<script>를 실행하지 않습니다.(단, 입소 처리 프로그램으로 스크립트를 실행할 수 있음)
따라서 스크립트 태그가 포함된 HTML은 dangerouslySetInnerHTML에서 설정할 수 있지만 이 스크립트는 실행되지 않습니다.

appeendChild와createContextualFragment를 이용하여


WebAPIappendChild 방법은 특정 노드에 하위 노드를 추가하는 방법입니다.
이 방법으로 추가된 노드는 일반 노드와 같이 실행되기 때문에 추가된 스크립트 탭도 실행됩니다.
문자열로 전송되는 HTML에서 요소를 생성할 때 createContextualFragment 방법을 사용합니다.HTML 태그가 포함된 문자열에서 지정된 범위의 시작점을 시작점으로 문서 세그먼트를 생성하는 방법입니다.
React의 위조 코드는 다음과 같습니다.
const HTMLComponent = ({ htmlString }) => {
  const divRef = useRef();
  
  useLayoutEffect(() => {
    if (!divRef.current) {
      return;
    }
    
    const fragment = document
      .createRange()
      .createContextualFragment(htmlString);
    
    divRef.current.appendChild(fragment);
  }, [htmlString]);
  
  return <div ref={divRef} />;
};
그러나 상술한 코드는 대응할 수 없는 몇 가지 모델이 있다.

HubSpot의 삽입 코드와 같은 외부 파일의 읽기 및 관련 내연 스크립트가 있다면


<script charset="utf-8" type="text/javascript" src="//js.hsforms.net/forms/shell.js"></script>
<script>
  hbspt.forms.create({
    portalId: "xxxxxx",
    formId: "xxxxxx"
  });
</script>
appendChild가 추가되면 두 개의 스크립트 탭이 동시에 실행되기 때문에 두 번째 내연 스크립트hbspt는 전역 변수가 아닌 상태에서 실행됩니다.
이 문제는 외부 파일을 읽을 스크립트 탭을 추가하거나 실행한 다음, 완성된 후에 다른 요소를 추가해서 해결할 수 있습니다.
const HTMLComponent = ({ htmlString }) => {
  const divRef = useRef();
  
  useLayoutEffect(() => {
    if (!divRef.current) {
      return;
    }
    
    (async () => {
      const scriptStrings = htmlString.match(
        /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi
      );
      
      let updatedHtmlString = htmlString;
      await scriptStrings.reduce(async (acc, current) => {
        await acc;

        const scriptFragment = document
          .createRange()
          .createContextualFragment(current);
        const scriptElement = scriptFragment.querySelector('script');

        if (scriptElement.src === '') {
          return Promise.resolve();
        }
  
        updatedHtmlString = updatedHtmlString.replace(current, '');
  
        if (
          Array.from(document.querySelectorAll('script')).some(
            se => se.src === scriptElement.src
          )
        ) {
          return Promise.resolve();
        }
	
	      return new Promise(resolve => {
          scriptElement.addEventListener('load', () => {
            resolve();
          });

          document.head.appendChild(scriptElement);
        });
      }, Promise.resolve());
      
      const fragment = document
        .createRange()
        .createContextualFragment(updatedHtmlString);
      
      divRef.current.appendChild(fragment);
    })();
  }, [htmlString]);
  
  return <div ref={divRef} />;
};
외부 파일의 스크립트 태그를 읽으려면 머리글에 추가하고 원래 HTML 태그에서 제거합니다.
같은 원본을 읽을 스크립트 탭이 이미 존재하면 다중 읽기를 방지하기 위해 처리를 건너뜁니다.
스크립트 탭을 불러온 후 남은 HTML을 삽입하면 외부 파일에 의존하는 내연 스크립트를 문제없이 실행할 수 있습니다.

SSG/SSR에서 컨텐츠 재현 안 함

useLayoutEffect는 서버에서 실행되지 않으므로 SSP/SSG를 실행할 때 재현되지 않습니다.
이 문제에 대해서는 삽입할 요소에 대한 초기 값을 설정해서 대응합니다.
설정
const HTMLComponent = ({ htmlString }) => {
  const divRef = useRef();
  
  const initialHTMLString = useMemo(() => {
    const scriptStrings = htmlString.match(
      /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi
    );
    
    return scriptStrings.reduce((acc, current) => {
      return acc.replace(current, '');
    }, htmlString);
  }, [htmlString]);
  
  useLayoutEffect(() => {
    if (!divRef.current) {
      return;
    }
    
    (async () => {
      const scriptStrings = htmlString.match(
        /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi
      );
      
      let updatedHtmlString = htmlString;
      await scriptStrings.reduce(async (acc, current) => {
        await acc;
  
        const scriptFragment = document
          .createRange()
          .createContextualFragment(current);
        const scriptElement = scriptFragment.querySelector('script');
  
        if (scriptElement.src === '') {
          return Promise.resolve();
        }

        updatedHtmlString = updatedHtmlString.replace(current, '');
        
        if (
          Array.from(document.querySelectorAll('script')).some(
            se => se.src === scriptElement.src
          )
        ) {
          return Promise.resolve();
        }

        return new Promise(resolve => {
          scriptElement.addEventListener('load', () => {
            resolve();
          });

          document.head.appendChild(scriptElement);
        });
      }, Promise.resolve());
      
      const fragment = document
        .createRange()
        .createContextualFragment(updatedHtmlString);
      
      divRef.current.innerHTML = '';
      divRef.current.appendChild(fragment);
    })();
  }, [htmlString]);
  
  return (
    <div
      ref={divRef}
      dangerouslySetInnerHTML={{
        __html: initialHTMLString,
      }}
    />
  );
};
htmlString에서 스크립트 탭initialHTMLString을 제거하면 미리 렌더링된 HTML을 불러올 때 인라인 스크립트가 실행되지 않습니다.
예를 들어 HubSpot의 내장 스크립트를 포함하지 않으면 미리 렌더링된 HTML이 읽을 때 스크립트를 실행하고 폼을 추가합니다. 응용 프로그램의 시작과 높은 모의 처리 useLayoutEffect 를 통해 폼이 사라진 후에 다시 추가됩니다.

총결산

  • dangerouslySetInnerHTML에 스크립트 태그가 포함된 HTML을 삽입할 수 있지만 innerHTML의 사양에 따라 스크립트 태그를 실행하지 않습니다.
  • appendChildcreateContextualFragment를 사용하여 스크립트 태그가 포함된 HTML을 삽입할 수 있습니다.
  • 외부 파일을 읽는 데 사용되는 스크립트 태그와 인라인 스크립트가 있으면 외부 파일을 읽은 후에 HTML을 삽입해야 합니다.
  • SSG/SSR을 사용할 때 스크립트 태그를 제외한 HTML을 초기값으로 설정합니다.
  • 이 실현에서는 임의의 스크립트를 실행할 수 있기 때문에 기본적으로 신뢰할 수 있는 입력에만 적용됩니다.
    이번 데이터의 입력은 특정한 내용 관리자만 로그인할 수 있기 때문에 이 실시 방식을 채택하였다.
    불특정 다수의 사용자가 입력, 열람을 필요로 하는 상황에서 적당한 위성을 확보한다.

    좋은 웹페이지 즐겨찾기