제목 : HTML 파일에서 React 사용하는 법


이 글을 작성하는 이유

npm, node 패키지로 npx 명령어를 실행하여 손쉽게 React 시작 템플릿을 가져올 수 있다.

즉, 일종의 프로젝트 초기화 명령어인데, 리액트로 프로젝트를 시작한다면, 이를 통해 손쉽게 구축이 가능하다.


그러나, React 는 컴퓨터 프로그램의 역사를 따져봤을 때, 긴 역사를 가지고 있지는 않다.

즉, 아직 tomcat 서버를 이용하거나, httpd 와 같은 오래된 웹 서버로

(물론, tomcat 은 웹과 WAS 의 역할을 동시에 수행한다.)

바닐라 JavaScript 와 HTML, CSS 파일로 스타일링을 진행하며,

동적 DOM 구축을 위해 JQuery 를 사용하고 있다.


이미 내부적으로 데이터셋을 명시하고, 이를 어떻게 보여 줄 지

복잡한 코드로 구현된 이 HTML, CSS, JavaScript 레이어를,

다시 React 로 시작해버리겠다는 것은 정말 터무니 없는 일에 가까울 것이다.

차라리, 회사나 특정 조직, 혹은 팀에서 새로운 도메인의 웹 페이지를 만들겠다는 것이 훨씬 현실적이다.


그런 의미에서, 현재 이미 구축되어 있는 웹 서버 내부의 코드를 React 형식으로 바꾸겠다는 것은,

DevOps 측면이나, 스타일링, 컴포넌트 계층 형성 자체를 전부 바꿔버리는 것과 동일하다고 생각한다.


따라서, 거대하면서 복잡한 웹 서버 코드를 "점진적으로" 변경하는 방법이 존재하는데,

바로 CDN 을 이용한 React 패키지 코드를 가져오는 것이다.

이 방식을 이용하면, 현재 복잡하게 구축된 HTML 계층과 더불어,

React cdn 이 제공하는 패키지의 클래스와 메서드를 이용하여 특정 부분을 React 화 시킬 수 있다.

참고로, CDN 이란, Content Delivery Network 의 약어 로,

반복 요청되는 컨텐츠에 대해 가장 가까운 호스팅 서버에서 컨텐츠를 빠르게 전달해 주는 네트워크이다.

요즘 웹 서버를 띄우는 방식이 대부분 호스팅 사이트의 CDN 네트워크를 통해 올라오기 때문에,

이 개념에 대해서 잘 알고 있는 것이 중요하다. (Cloudflare 사이트가 잘 설명 해 놓음.)


React 와 같이 사용되는 수많은 라이브러리와 방법론으로 시작하는 방법은 공식 문서에 잘 나와 있다.

그러나, 순수 React 를 일반 HTML 파일에서 시작하는 것은

Legacy 시스템을 바꾸는 데 매우매우 중요한 지식이라고 생각하여 이 글을 작성하게 되었다.


일반 HTML 파일에서 React 구현해 보기


위에서 HTML 파일 내부에 CDN 으로 패키지는 왜 가져올까?

HTML 파일에서 우리는

<script src="{cdn 패키지 경로}" crossorigin></script>

이 태그를 통하여 HTML 파일에서 JavaScript 를 이용 할 때,

해당 패키지 경로로 JS 파일을 요청 한 뒤, 곧바로 파일을 실행한다.

그리고 이제, 해당 페이지 내부의 Script 문으로 React 컴포넌트를 사용 할 수 있다.


패키지는 총 2 개가 필요하다.

  1. https://unpkg.com/react@18/umd/react.development.js
  2. https://unpkg.com/react-dom@18/umd/react-dom.development.js
<!DOCTYPE html>
<html>
  <head>
    <title>html 파일에서 react 사용하기</title>
    <script
      src="https://unpkg.com/react@18/umd/react.development.js"
      crossorigin
    ></script>
    <script
      src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
      crossorigin
    ></script>
  </head>
  <body>
    <div id="test-container"></div>
    <script>
      // 리액트 렌더링을 위한 DOM 을 가져오기
      const testContainer = document.querySelector("#test-container");

      // 구축될 리액트 루트를 생성 한 뒤, root 에 할당한다.
      const root = ReactDOM.createRoot(testContainer);

      root.render(
        React.createElement("p", null, "p 태그로 제작된 리액트 컴포넌트이다."),
      );
    </script>
  </body>
</html>

렌더링 결과 :

개발자 도구 Elements 결과 :

<!-- 코드 엘리먼트 구조 -->
<div id="test-container"></div>

<!-- 렌더링 엘리먼트 구조 -->
<div id="test-container">
    <p>p 태그로 제작된 리액트 컴포넌트이다.</p>
</div>

렌더링 이후의 화면과, 엘리먼트 계층을 보면

React 가 성공적으로 실행 되어 JavaScript 로 DOM 을 구축 한 것을 볼 수 있다.

특히, 원하는 버전이 있다면, cdn 경로에서 숫자 "18" 을 "xx" 로 변경 해 주면 된다.

그러나, 프로덕션 버전으로 배포 할 생각이라면,

마지막 경로 문자열들에서

  • react.development.js --> react.production.min.js
  • react-dom-development.js --> react-dom.production.min.js

이렇게 바꿔주면 된다. (최소 용량, 성능 최적화 버전)


React 의 상태 관리는 어떻게 할까?

템플릿에서는 함수형 컴포넌트와 useEffect Hook 으로 생명주기와 렌더링을 매우 쉽게 조작 할 수 있다.

그러나, 그 이전에는 class 형식의 컴포넌트가 자주 사용되기도 했다.

컴포넌트의 상태 변수 관리는 컴포넌트 함수나 컴포넌트 클래스에서 이루어지는데,

공식 문서에는 컴포넌트 클래스를 이용한 예제만 존재한다.

클래스 형식의 컴포넌트를 알고 있는 사람들은 이미 알고 있겠지만,

useEffect 는 클래스 컴포넌트가 가지고 있는

  • componentDidMount
  • shouldComponentUpdate
  • 등등..

과 같은 수많은 내장 메서드를

useEffect 콜백 함수로 쉽게 표현 할 수 있다.

만약에,

정말로 기본 html 파일에서 리액트 컴포넌트 내에서 상태별 상황을 표현 할 것이라면,

위에서 제시한 리액트 클래스 컴포넌트 내장 메서드 를 사용하거나,

함수 컴포넌트 를 사용할 수 있다 :


const {useState, useEffect} = React;

...

function TestFunctionalComponent = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("함수형 컴포넌트가 안착됨!");
  }, []);

  return React.createElement(
    ...
  );
}

이러한 형식으로도 만들 수 있다!


Component Class 를 이용한 예제

react-test.html :

<!DOCTYPE html>
<html>
    <head>
        <title>html 파일에서 react 사용하기</title>
        <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
        <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
    </head>
    <body>
        <div id="test-container"></div>
        <script src="p_tag_test.js"></script>
    </body>
</html>

같은 계층에 존재하는 p_tag_test.js :

// 리액트 렌더링을 위한 DOM 을 가져오기
let testContainer = document.querySelector("#test-container");

class CountArea extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count : 1,
    }
  }

  render() {
    return React.createElement(
      'p',
      {onClick : () => this.setState({count : this.state.count + 1})},
      `현재 이 글을 클릭 한 횟수는, ${this.state.count} 번 입니다.`
    )
  }
}

// 구축될 리액트 루트를 생성 한 뒤, root 에 할당한다.
const root = ReactDOM.createRoot(testContainer);

root.render(
  React.createElement(
    React.Fragment,
    null,
    React.createElement(
      "p",
      null,
      "p 태그로 제작된 리액트 컴포넌트이다."
    ),
    React.createElement(
      CountArea,
      null,
    )
  )
);

결과 :


해석 :

React 를 홈페이지 단에서 사용하기 위해서,

React 패키지 CDN 소스를 먼저 불러와야 한다. (그래야 그 다음 소스코드에서 React 를 인식하므로)


<div id="test-container"></div> 는, 리액트가 Root 로 삼을 DOM 을 의미한다.


<script src="p_tag_test.js"></script>

이 부분은, 위에 작성 해 놓은 p_tag_test.js 소스코드를 실행시켜서,

불러온 React 라이브러리를 통하여 컴포넌트를 실제로 렌더링 시키는 부분이다.


내가 작성한 코드에서 아쉬운 부분 :

컴포넌트 클래스 부분과, 실제 DOM 과 React 결합 부분은 따로 나누는 것이 좋겠다고 생각한다.

class CountArea... 는, <head> 태그 내부에 존재하지만,

React CDN 라이브러리 부분의 하단에 선언하고,

나머지 JS 코드 부분은 Root 가 될 DOM 인 #test-container 속성 하단에

선언하면, React 컴포넌트가 안정적으로 부착되는 것을 볼 수 있다.


왜 태그를 사용하지 않고, createElement 를 사용하나?

현재 이 글을 작성하는 이유는, Legacy (오래된) 프로젝트에

React 를 도입해야 할 경우, 어떻게 도입해야 하는가를 다루고 있다.

물론 JSX 는 매우 편하다. 이 표현식을 일부로 사용하지 않는 것이 아니라,

JSX 는 결국 JS 로 파싱되는데, 이 부분은 Webpack 과 같은 번들러 프로그램이 필요하기 때문이다.


만약에 복잡한 Legacy 시스템에 JSX or TSX 를 도입하려면, (물론 TSX 도입 시 설정 복잡성이 미친듯이 증가할듯.)

JSX 를 파싱하는 번들러 프로그램의 설정 파일에 대한 이해와, package.json, tsconfig.json

이 두 파일의 설정값 속성들을 이해하고 있어야 하며, 오래된 시스템과 맞물리는지 꼼꼼하게 확인해야 한다.


그래도 무조건 JSX 표현식을 도입해야 한다면?

내가 만약에 오래된 웹 프로젝트에 React 를 추가해야 한다면, (일단 머리가 아프겠지만,)

  1. React JSX 가 파싱되는 프로젝트는 기존 웹 페이지 소스의 하단 계층에서 제작한다.
    • 즉, npm 설정 환경을 기존 레거시 환경과 "격리" 한다.
    • 이게 가능한 이유는, 프로젝트는 가장 가까운 npm 설정과 모듈을 레퍼런스하기 때문이다.
    • 하단 계층이란, 직접 하위 디렉토리에 React 프로젝트를 생성한다는 의미이다.

  1. React DOM 이 안착될 Root DOM 을 레거시 웹 페이지에서 미리 선언 해 놓는다.
    • 그래야 React 가 시작할 Root 를 알고 가져올 수 있기 때문.

  1. Webpack 설정을 바꾸어서, 결과물을 프로젝트 내부 dist 가 아니라, 상단 계층에 생성하도록 만든다.
    • 이를 수행하기 위해서는 npm cli 명령으로 웹팩 eject 를 사용해야 파일을 볼 수 있다.
    • 목적 파일의 경로 뿐만 아니라, 생성된 React 청크 파일을 기존 홈페이지에 어떻게 적용할지 생각해야 한다.

  1. 생성된 js, css 청크 파일을, 기존의 홈페이지의 태그에 추가해야 한다.
    • Webpack 이라는 시스템은 새로운 React 프로젝트 내부의 시스템으로, Legacy 와는 격리되어 있다.
    • 그렇다면, 생성된 청크 파일들을 로드하기 위해서, Legacy 프로젝트에서 DOM 에 추가해 줘야 한다.
    • 이를 위해서는, 파일 이름 난독화를 해제하고, 코드 난독화도 해제 할 지 생각해야 한다.
    • 결국, 설정에 대한 깊은 이해와, 프로젝트 구조의 이해력에 이 문제가 달려 있다.

위에 레거시 프로젝트에 리액트를 추가하는 법을 작성했는데,

"어떤 기능과 DOM 까지 바꿀건지" 가 제일 중요할 것 같다..

또한, 이 시스템을 점차 React 로 변환 할 것 이라면,

기존 레거시 시스템 관리자와, 리액트 관리자의 소통이 매우매우 잘 되거나, or (세세한 문서화)

기존 레거시 시스템에 대한 이해와,

리액트 "전체" 시스템에 대한 이해도가 높은 사람이 진행해야 한다고 생각한다.

물론, React 템플릿으로 Legacy 프로젝트 내부에 삽입해야 한다면,

Webpack 설정으로 충분히 커버가 가능하다.


그러나, babel.js 이라는 Webpack 내장 모듈이 존재한다.

이를 통해서 JSX 를 충분히 JS, CSS 로 변환시킬 수도 있다.

하지만 항상 그렇듯이, 이 라이브러리 또한 설정 파일에 대한 공부가 필요하다.


이런 복잡한 과정 말고, 그냥 홈페이지 소스 파일에 JSX 를 넣고싶다면?

공식문서에서 말하길, 공부 목적과 간단한 데모 사이트 에서는 괜찮지만,

일반 홈페이지에서 JSX 파일 인식하게 만들기 (공식 문서)


성능 이슈로 인해 프로덕션 상황에서는 맞지 않다고 한다.

<script src="https://unpkg.com/babel-standalone@6/babel.min.js">
</script>

즉, 홈페이지 사이트 자체에, Babel 라이브러리를 장착시켜서,

클라이언트 브라우저가 JSX 파일 파싱을 직접 수행하도록 만드는 것이다.

만약에 홈페이지에서 직접 JSX 파일을 불러오려면,

<script type="text/babel" src="{JSX 파일}"></script>

형식으로 불러 올 수 있다.


다시 한번 말하지만,

프로덕션 환경에서는 "절대로" 권장하지 않는다.

안그래도 렌더링이 빨라야 하는데, JSX 파싱까지 하면

클라이언트가 JSX 파일과 상호작용 할 때 마다, 버벅임을 보일 수 있다.


함수형 컴포넌트 예시

역시, 클래스형 컴포넌트 말고, 함수형 컴포넌트도 예시로 들고 마무리하는 것이 좋을 것 같다.

cdn 으로 가져온 패키지를 변경 할 필요는 없고,

대신, 우리가 사용할 js 코드를 변경해야 한다.


p_tag_test.js :

// 리액트 렌더링을 위한 DOM 을 가져오기
let testContainer = document.querySelector("#test-container");

const root = ReactDOM.createRoot(testContainer);

const { useState, useEffect } = React;

function CountArea () {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("함수형 컴포넌트가 안착됨");
    return () => {
      console.log("함수형 컴포넌트가 해제됨");
    }
  }, []);

  return React.createElement(
    "div",
    {
      style : {
        color : "blue",
        backgroundColor : "darkgray",
        margin: 20
      }
    },
    React.createElement(
      "p",
      null,
      `현재 이 부위가 클릭 된 횟수는 : ${count} 번 입니다.`
    ),
    React.createElement(
      "button",
      { onClick : () => setCount(count + 1)},
      "Count + 1"
    )
  )
}

root.render(
  React.createElement(
    React.Fragment,
    null,
    React.createElement(
      "p",
      null,
      "p 태그로 제작된 리액트 컴포넌트이다."
    ),
    React.createElement(
      CountArea
    )
  )
);

Result :


설명 :

우리가 JSX 로 반환하는 형식 말고, Reacr.createElement 메서드를 사용하면,

결국 우리는 거의 동일한 형식으로 컴포넌트를 만들 수 있다!


이 글을 조사 및 작성하며 배운 것

요즘 나는 "방법론" 에 꽂혔다.

이 말은, "나는 Webpack, Babel, npm, node, Library 를 통해 편하게 제작하는 것이 좋아!"

가 아니고, "방법론" 에 휘둘리는 나는 진정한 소프트웨어 엔지니어가 될 수 없겠구나 판단을 내린 것이다.


그런 의미에서, Legacy 시스템에 어떻게 React 를 적용 할 수 있을까? 뿐만이 아니라,

React 템플릿 내부에 갇혀, 이 도구들이 행하는 진정한 의미를 모른 채 사용하는 것이 스스로 안타까웠다.

매우 편하고 생산성 있지만, 결국 이 도구들의 로직을 파악해야 중-고급 개발자로 넘어갈 수 있다는 것이

나의 판단이었다.


차라리 이 노력으로 편하게 프로젝트를 만들면 포트폴리오에 좋지 않나? 취업해야지!

맞다. 너무나도 맞는 말이다.

그런데, 먼저 내 블로그 최신 글 리스트를 보고,

  • "이 글을 작성하는 이유"
  • "이 글을 작성하고 나서 배운 것"

이 2 가지를 보면 내가 바라보는 소프트웨어 프로그래밍의 미래 예측과 방향성을 알 수 있을 것이다.

나는 단순히 특정 프레임워크와 라이브러리에 종속되어 나의 한계를 지정하고 싶지 않다.

나는 예측이 안 되는 모르는 지식을 넘기지 말고, 꼭 공부하여

진정한 의미의 컴퓨터 엔지니어가 되겠다.



참고 사이트

이전 개발 문서 (웹 사이트에 React 추가하기)

https://ko.legacy.reactjs.org/docs/add-react-to-a-website.html


이전 개발 문서 (CDN 링크)

https://ko.legacy.reactjs.org/docs/cdn-links.html


이전 개발 문서 (JSX 없이 사용하는 React)

https://ko.legacy.reactjs.org/docs/react-without-jsx.html