Never give up

React - code editor 2 본문

WEB

React - code editor 2

대기만성 개발자 2023. 12. 15. 15:44
반응형

이번에는 iframe에서 어떻게 구현하는지 알아보도록 하겠습니다

 

(iframe 태그에 대한 설명들은 타 블로그에 자주 잘 정리되어있어서 굳이 정리는하지 않겠습니다)

 

src/hooks/useEditor.ts

const useEditor = () => {
  const resultRef = useRef<HTMLIFrameElement>(null);

  const [text, setText] = useState<string>(testString);
  const [printText, setPrintText] = useState<string>();

  const debounce = useRef<number>();
  const isLoaded = useRef<boolean>(false);
  const currentScript = useRef<HTMLScriptElement>();

  useEffect(() => {
    initIframe();
    initConsole();

    return () => {
      window.removeEventListener("message", onMessage);
    };
  }, []);

  useEffect(() => {
    onChangeText();
  }, [text]);

  /** - append react, react dom, body entry tag */
  const initIframe = () => {
    const scripts = [
      {
        isLoaded: false,
        src: "https://unpkg.com/react@18/umd/react.production.min.js",
      },
      {
        isLoaded: false,
        src: "https://unpkg.com/react-dom@18/umd/react-dom.production.min.js",
      },
    ];

    for (let i = 0; i < scripts.length; i++) {
      const script = document.createElement("script");
      script.src = scripts[i].src;
      script.defer = true;

      script.onload = () => {
        scripts[i].isLoaded = true;

        if (scripts.every((el) => el.isLoaded)) {
          isLoaded.current = true;
          buildScript();
        }
      };

      resultRef.current!.contentDocument!.head.appendChild(script);
    }

    const app = document.createElement("div");

    app.id = "app";

    resultRef.current!.contentDocument!.body.appendChild(app);
  };

  /** - post message from iframe and listen in parent */
  const initConsole = () => {
    const script = document.createElement("script");

    script.text = `
const originalLog = console.log;

console.log = (...args) => {
  parent.window.postMessage({ type: 'log', args: args }, '*')
  originalLog(...args)
}`.trim();

    resultRef.current!.contentDocument!.head.appendChild(script);

    window.addEventListener("message", onMessage);
  };

  /** - display console from iframe */
  const onMessage = (e: MessageEvent) => {
    const data = e.data;

    if (data.type === "log") {
      let args: string;

      if (typeof data.args === "object") {
        const value = JSON.stringify(data.args);

        // get rid of brace
        args = value.substring(1, value.length - 1);
      } else {
        args = data.args;
      }

      setPrintText(`${args}`);
    }
  };

  /** - debounce and build script */
  const onChangeText = () => {
    if (!isLoaded.current) {
      return;
    }

    clearTimeout(debounce.current);

    debounce.current = setTimeout(() => {
      buildScript();
    }, 2000);
  };

  /** -transpile and append script */
  const buildScript = async () => {
    try {
      const convertedCode = Babel.transform(`${text}`, {
        presets: ["react"],
      }).code;

      const previousScript = currentScript.current;

      currentScript.current = document.createElement("script");

      currentScript.current.text = `${convertedCode}`;

      if (previousScript !== undefined) {
        previousScript.replaceWith(currentScript.current);
      } else {
        resultRef.current!.contentDocument!.head.appendChild(
          currentScript.current
        );
      }

      setPrintText(undefined);
    } catch (e) {
      if (currentScript.current !== undefined) {
        removeBody();
      }

      setPrintText(`Error : ${e}`);
    }
  };

  /** - remove previous element */
  const removeBody = () => {
    resultRef.current!.contentDocument!.body.children[0].innerHTML = "";
  };

  return { text, setText, printText, resultRef };
};

export default useEditor;

createRoot.render를 이용하던 useTest와 달리 상대적으로 코드가 길은데

 

하나하나 보면 iframe에 script 추가, 제거 하는 코드들입니다

 

1. 첫 useEffect를 보면 iframe 초기화와 console 초기화 부분이 있는데

 

initIframe부분은 react와 react-dom cdn을 추가하는 작업 그리고 load가 완료됐을 때 buildScript를 콜하는 부분

 

createRoot에 사용할 element 추가로 필요한 기초작업(?)을 제외하고는 없습니다

 

그리고 initConsple부분은 iframe에서 console.log를 실행할 때

 

postMessage로 iframe에서 메시지를 보내고 받는 message를 view에 보여주기위해 사용됩니다

 

window.addEventListener("message",listener) 형태로 message가 발생했을 때 해당 부분에서 처리를 해줍니다

 

2. 다음 useEffect를 보면 이전과 동일하게 debounce를 걸어놓고 buildScript 내부에서

 

iframe에 변경을 주는 작업인데 하나씩 보면

 

Babel standalone으로 변환은 동일합니다만 이후에 처리하는 방식이 조금 다른데

 

script tag에 변환된 JS코드를 넣고 head에 append 해주거나 replace(스크립트 내용 변경)으로

 

첫 useEffect에서 id가 app div를 append한 부분에서 createRoot(element).render(component) 형태로 사용이 됩니다

 

직접적으로 접근할때와 달리 iframe을 처리하는 코드가 많아서 조금 길어졌고

 

cdn 로딩이 추가되다보니 조금더 불편하고 로딩이 길어질수 있는 단점은 있지만 환경을 분리할 수 있다는 장점이 있습니다

 

혹시나해서 js fixxle을 한번 보니

< js fixxle의 iframe 부분 >

 

내부 처리로직은 다르겠지만 마찬가지로 script 태그에 react, react-dom을 추가한것을 확인할 수 있겠습니다

 

다음 포스트에서는 prism을 이용해서 highlighting 처리하는것과

 

textarea를 어떤 꼼수(?)로 highlighting을 적용하는지 알아보도록 하겠습니다

 

component로 구현 : https://devmemory.tistory.com/121

highlight 처리 : https://devmemory.tistory.com/123

반응형
Comments