Never give up

React - code editor 1(feat. babel standalone) 본문

WEB

React - code editor 1(feat. babel standalone)

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

바빴던 일들이 어느정도 마무리 돼가고 있어서 여유가 조금 생겼습니다

 

고로 이전 포스트에서 언급한 코드 에디터를 공유해보고자 합니다

 

포스트 3개로 나눠서 포스트 할 예정인데

 

1. babel standalone으로 string jsx를 js로 변환 후 createRoot의 render를 이용해서 구현

2. iframe 내부에 head의 script 부분을 변경시켜가면서 화면에 보여주는 부분

3. prism을 이용해서 textarea를 그럴싸하게(?) 보여주는 작업 및 간단한 키보드 처리 및 결과

 

정도로 진행해볼까 합니다

 

먼저 babel standalone을 살펴보면

https://babeljs.io/docs/babel-standalone

 

< babel 홈페이지에 나와있는 설명 >

 

node 환경이 아닌 곳에서 사용할 수 있는 standalone 형태의 바벨이라는건데

(엘x스x롤 같은거 해보신분들은 잘 아시는 그거 맞습니다)

 

별도의 환경에 구애받지않는 형태의 라이브러리라고 보시면 될거 같습니다

 

그래서 언제 사용하냐가 중요한데, 대충 해석해보면 다음과 같습니다

1. 간단한 HTML에 script tag로 사용할 때

2. 유저가 작성한 JS를 real time compile을 해줘야되는 사이트들(이번 예제와 같은 코드 에디터가 가장 좋은 예가 되겠습니다)

3. V8엔진과 같은 embedded JS를 사용하는 환경에서 Babel을 사용하고 싶을 때

4. 앱을 확장하는데 모던 ES와 같은 기능이 포함된 스크립트 언어로 JS를 사용하고 싶을 때 

5. node 환경이 아닌 환경들

 

해당 예제는 1, 2, 4(?)가 되지않을까 합니다

 

가장 중요한 사용 이유는 브라우저가 JSX를 읽지 못하기 때문에 JS형태로 변환을 해주는 작업이 필요하고

 

우리가 사용하는 Babel, esbuild이 JSX를 JS형태로 변환을 해주기 때문에 사용합니다

 

서론이 길었는데 바로 시작하도록 하겠습니다

 

src/data/testString.ts

const testString = `
function Component() {
  const [counter, setCounter] = React.useState(1);
  const [title, setTitle] = React.useState()
  const debounce = React.useRef()

  React.useEffect(() => {
    clearTimeout(debounce.current)

    debounce.current = setTimeout(() => {
      getTitle();
    }, 1000);
  },[counter])

  const getTitle = async () => {
    // const res = await fetch("https://jsonplaceholder.typicode.com/todos/" + counter)

    // const data = await res.json()

    // setTitle(data.title)
  }

  const increase = () => {
    setCounter(counter + 1);
  };

  const decrease = () => {
    setCounter(counter - 1);
  };

  return (
    <div style={{display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", height: "100%"}}>
      <div style={{textAlign: "center"}}>
        {title} <br/>
        {counter}
      </div>
      <div style={{display: "flex"}}>
        <button onClick={increase}>+</button>
        <div style={{width: "20px"}}/>
        <button onClick={decrease}>-</button>
      </div>
    </div>
  );
};

ReactDOM.createRoot(document.getElementById("app")).render(<Component />)
`.trim();

export default testString;

사용한 예제 텍스트 파일로, 간단한 더하기 빼기, 변경된 숫자에 따른 api 콜 정도로

 

간단한 예제 텍스트를 준비해봤습니다

 

iframe에 보여주는 부분은 createRoot가 필수이고

 

직접 React component에 render시킬 때는 불필요합니다

 

src/hooks/useTest.tsx

const useTest = () => {
  const resultRef = useRef<HTMLDivElement>(null);

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

  const root = useRef<Root>();
  const debounce = useRef<number>();

  useEffect(() => {
    root.current = createRoot(resultRef.current!);

    const originalConsoleLog = initConsole();

    return () => {
      console.log = originalConsoleLog;
    };
  }, []);

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

  /** - debounce and build script */
  const onChangeText = () => {
    clearTimeout(debounce.current);

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

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

      convertedCode = convertedCode?.replace('"use strict";', "").trim();
      const func = new Function("React", `return ${convertedCode}`);
      const App = func(React);

      root.current!.render(<App />);
      setPrintText(undefined);
    } catch (e) {
      setPrintText(`Error : ${e}`);
    }
  };

  /** - initial setting to show console to jsx */
  const initConsole = () => {
    const originalConsoleLog = console.log;

    console.log = (...args) => {
      originalConsoleLog(...args);

      const log = JSON.stringify(args);

      setPrintText(log.substring(1, log.length - 1));
    };

    return originalConsoleLog;
  };

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

export default useTest;

(이름이 useTest인건 그냥 테스트 삼아 만들어본거여서 별도의 의미는 없습니다)

 

1. 첫 useEffect부분을 보면 root에 createRoot를 할당해주고 나중에 render를 할 때 사용합니다

 

그리고 콘솔을 화면에 직접 보여주는 용도로 initConsole부분에서

 

console.log 함수 내부에 별도의 처리를 해줍니다

 

2. 다음 useEffect를 보면 text가 변화될 때, debounce를 처리한 buildScript를 콜하는 부분이 있는데

 

매번 타이핑할때마다 변경해주면 성능도 성능이고 syntax error을 매초마다 보게 될겁니다..

 

3. 이제 핵심 부분인 build script를 보면

 

Babel standalone의 transform 함수에서 text를

 

특정 프리셋에 맞게 transpile을 할 수 있는데

 

위 예제를 transpile해보면 다음과 같이 나옵니다

function Component() {
  const [counter, setCounter] = React.useState(1);
  const [title, setTitle] = React.useState();
  const debounce = React.useRef();
  React.useEffect(() => {
    clearTimeout(debounce.current);
    debounce.current = setTimeout(() => {
      getTitle();
    }, 1000);
  }, [counter]);
  const getTitle = async () => {
    // const res = await fetch("https://jsonplaceholder.typicode.com/todos/" + counter)

    // const data = await res.json()

    // setTitle(data.title)
  };
  const increase = () => {
    setCounter(counter + 1);
  };
  const decrease = () => {
    setCounter(counter - 1);
  };
  return /*#__PURE__*/React.createElement("div", {
    style: {
      display: "flex",
      flexDirection: "column",
      justifyContent: "center",
      alignItems: "center",
      height: "100%"
    }
  }, /*#__PURE__*/React.createElement("div", {
    style: {
      textAlign: "center"
    }
  }, title, " ", /*#__PURE__*/React.createElement("br", null), counter), /*#__PURE__*/React.createElement("div", {
    style: {
      display: "flex"
    }
  }, /*#__PURE__*/React.createElement("button", {
    onClick: increase
  }, "+"), /*#__PURE__*/React.createElement("div", {
    style: {
      width: "20px"
    }
  }), /*#__PURE__*/React.createElement("button", {
    onClick: decrease
  }, "-")));
}
;

결과를 보면 JSX의 return 부분을 제외하면 동일하게 나타나고

 

해당 부분은 createElement("tag명", {...attributes}, [...children]) 이 되는것을 확인할 수 있는데

 

우리가 작성하는 React가 렌더링을 할 때 createElement를 사용해서 DOM에 보여주는것을 알 수 있겠습니다

 

그리고 이렇게 변환된 코드를 이전에 초기화 했던 createRoot의 render 함수를 이용해서

 

원하는 Element에 보여줄 수 있는 형태가 됩니다

 

iframe으로 환경을 분리하지 않았을 때 발생할 수 있는 문제점들이

 

대략 전역적 셋팅에 영향을 받는것입니다

 

예를들어 대부분 css를 사용하기 전에 css reset을 해놓고 시작을 할텐데

 

환경이 분리되지 않아 영향을 받는다는것이죠

 

장점으로는 상대적으로 구현하기 편한거 같습니다

 

다음 포스트에서는 iframe에서 구현하는 부분을 다뤄보도록 하겠습니다

 

iframe으로 구현 : https://devmemory.tistory.com/122

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

반응형

'WEB' 카테고리의 다른 글

React - code editor 3(feat. Prism.js)  (1) 2023.12.15
React - code editor 2  (0) 2023.12.15
React - localization without library  (2) 2023.11.18
React - image resize example  (0) 2023.07.16
React, Node - SSE example  (0) 2023.03.04
Comments