[React.js]리액트 만들기.part4.Server Side Rendering and its Challenges

유선향·2025년 10월 11일

<React>

목록 보기
5/5

바닐라 자바스크립트로 직접 리액트의 주요 기능을 만들어보며, 동작원리를 좀더 심층적으로 이해하기 위해 아래 글을 따라가며 얻은 인사이트와 학습을 기록하는 글입니다. 또한 아래 글의 4가지 챕터로 리액트의 동작원리를 이해하는 단계를 그대로 따라가기에, 아래 Medium 글을 참고하시는게 직접 해보시는 것에는 도움이 되실 겁니다.

lets-build-a-react-from-scratch-part-4-server-side-rendering

git hub repo : part4 branch(파트에 따라서 브랜치를 분배 했습니다)


이번 섹션은 혼자 해본다...

사실 이번 섹션 내용은 뭔가 갑자기 난이도가 훅 올라간 느낌이라 해당 글 자체를 이해하기가 어려웠다...
그래서 좀더 간단한? 구현을 해본뒤에 다시 해당 섹션 내용을 읽어보기로 했다.

V1. 일단 냅다 해보기

먼저, 아래와 같은 내용을 가진 서버 로직을 작성했다.

서버에서 내려줄 컴포넌트

import React from "react";

export default function App() {
  return React.createElement(
    "div",
    null,
    React.createElement("h1", null, `이 글과 아래 버튼은 서버에서 보내줌`),
    React.createElement(
      "button",
      { onclick: () => alert("하이드레이션이 잘 되었다!!") },
      `하이드레이션이 필요한 버튼`
    )
  );
}

서버에서 어떤 응답을 보낼지를 정하는 로직

// server.mjs (Node ESM)
...
const app = express();

app.use("/dist", express.static(path.join(__dirname, "../dist")));

app.get("/*", (req, res) => {
  console.log("request in server.mjs", req);
  const reactApp = renderToString(React.createElement(App));

  return res.send(
    `<html>
      <body>
        <div id="root"> ${reactApp}</div>
           <script type="module" src="/server/clientEvent.js"></script>
            <div id="myapp"></div>
           <script type="module" src="/dist/main.js"></script>
      </body>
    </html>
    `
  );
});
app.listen(3000, () => {
  console.log("server is running");
});

아래와 같이 서버에서 받은 html과, 내가 클라이언트에서 만든 html 이 잘 공존하게 되었다.


하이드레이션 하기

서버에서 보내준 html 문서를 살펴보자, 분명 나는 button 태그에 props로 { onclick: () => alert("하이드레이션이 잘 되었다!!") } 을 남겨주었는데, html 에는 그 어떤 비슷한 흔적도 찾아 볼 수 없었다.
이유를 찾아보니 아래와 같았다.

서버가 React.createElement("button", { onClick: ... })를 렌더링할 때, 함수는 직렬화(serialize) 될 수 없는 “실행 코드”라서, HTML 문자열로 내려줄 수 없다.

따라서 내가 작성한 onClick은 어떤 데이터에도, 어떤 data-attribute에도 들어가지 않았다.
함수는 JSON처럼 문자열로 안전하게 전달할 수 없기 때문이다.

그럼 리액트는 하이드레이션이 필요한 컴포넌트와 함수를 어떻게 아는거지?

일단 내가 알고있기로는 next.js 에서는 html에 + 하이드레이션 대상이 되는 핸들러 js 파일 두가지를 이어주는 것이 하이드레이션이라고 알고 있다. 그럼 위에 처럼 적힌 단순 html에 어떤 핸들러를 연결해야 할지를 어떻게 구분한단 말인가? 한가지 아이디어는 일단 하이드레이션이 필요한 함수를 따로 js 로 브라우져에 보내주고, 이 두가지를 id, data-set 같은 유니크한 값으로 아, 이 두개가 짝이구나! 라는걸 브라우져에서 알수 있으면 될것 같아 보였다.

V2. data-set으로 어떤 이벤트인지 알려주기

서버에서 내려줄 컴포넌트


export default function App() {
  return React.createElement(
    "div",
    null,
    React.createElement("h1", null, `이 글과 아래 버튼은 서버에서 보내줌`),
    React.createElement(
      "button",
      {
        "data-hydrate-event": "click",//어떤 이벤트인지
      },
      `하이드레이션이 필요한 버튼`
    )
  );
}

서버에서 어떤 응답을 할지 정한다.

// server.mjs (Node ESM)
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = express();

app.use("/server", express.static(__dirname));
app.use("/dist", express.static(path.join(__dirname, "../dist")));

app.get("/*", (req, res) => {
  console.log("request in server.mjs", req);
  const reactApp = renderToString(React.createElement(App));

  return res.send(
    `<html>
      <body>
        <div id="root"> ${reactApp}</div>
           <script type="module" src="/server/clientEvent.js"></script> 
			//따로 구분한 이벤트 핸들러 함수 자바스크립트 파일
            <div id="myapp"></div>
           <script type="module" src="/dist/main.js"></script>

      </body>
    </html>
    `
  );
});

app.listen(3000, () => {
  console.log("server is running");
});

클라이언트에서 렌더링 엔트리 포인트가 되는 파일

//client/main.js
import { App } from "./app.js";
import React from "./core/react.js";
import { render } from "./core/render.js";

// 어플리케이션 렌더링 entry point
render(<App />, document.getElementById("myapp"));
(async () => {
  // @ts-ignore
  const { handleClickInClient } = await import("/server/clientEvent.js");
//server/clientEvent.js 로 요청을 보내면, 서버에서 해당 파일을 응답하게끔 설정 해놨기에, 접근할 수 있다.
  console.log(handleClickInClient); //콘솔에서 찍어 보자
})();

위의 사진과 같이 내가 의도한 대로 html에서 data-set과 js에 접근하는 것까지, 모든것이 잘 동작했다.

자, 그럼 이제 이걸 기반으로 엔트리 포인트에서 이벤트 핸들러를 연결 해보자.


render(<App />, document.getElementById("myapp"));
(async () => {
  // @ts-ignore
  const { handleClickInClient } = await import("/server/clientEvent.js");

  const elements = document.querySelectorAll("[data-hydrate-event]");
  //data-hydrate-event 라는 data-속성을 가진 요소를 다 찾는다.

  elements.forEach((el) => {
    const event = el.getAttribute("data-hydrate-event");
	//여기서 data-hydrate-event의 값인 click 이 할당 될 것이다.
    if (!handleClickInClient) return;
    el.addEventListener(event, handleClickInClient);
    // 이벤트를 등록해 준다. 
  });
})();

하이드레이션이 잘 되었다!!!


그런데, 위의 과정중에서 두가지 의구심이 남았다.

1. 근데 우리가 next.js에서 클라이언트 컴포넌트 상단에 'use client' 를 선언하고, 해당 모듈은 html 만드는 것 까지 브라우져에서 실행하게끔 하는 것 아니였나??

  • 우리가 button 태그를 next.js 에서 작성했을때를 떠올려 보자, 상단에 'use client'를 적고, 해당 라우트에 접근했을때는 분명 html 문서에 해당 버튼은 보이지 않았다. 서버로부터 html을 응답 받고 나서, 브라우져에서 그려낸 것이다.

알아보니, Next 에서 'use client'로 선언한 파일은 아래와 같이 처리한다는 것을 알수 있었다.

1. 서버는 "use client" 컴포넌트의 HTML을 "일부" 미리 만들어서 브라우저에 전송.

  • 해당 클라이언트 컴포넌트 위치에 자리 표시용 html 을 만들어 둔다.
    2. 해당 클라이언트 컴포넌트에서 브라우져에서 실행될 자바스크립트 번들을 따로 빌드 한다.
  • 내가 임의로 해당 핸들러 파일을 script로 삽입 한 내용과 유사하다.
    3. Hydration
  • 이때 내부적으로 각 DOM 노드에 있는 data-attribute 를 이용해서 고유 식별자 키로 페어를 맞춘다.
  • 이것 또한 내가 이렇게 하면 되지 않을까? 라고 생각한 것과 유사하다.

2. 지금 구조는 서버에서 내려준 컴포넌트에, 바로 하단에 클라이언트에서 만든 컴포넌트를 씌우는 구조이다. 즉, 병렬 위치 일 수밖에 없다.

  • next.js 의 App Router는 (물론 내가 지금 하는것이 next.js 를 만드는 것은 아니지만) 한 페이지가 존재하면, 컴포넌트 레벨에서 서버컴포넌트는 미리 만들어서 보내주고, 클라이언트 컴폰넌트는 브라우져에서 만들고
    이렇게 초기 렌더링이 되는 것으로 알고 있는데, 내가 만든 구조는 클라이언트 컴포넌트는 무조건 하단에 위치 할 수 밖에 없게 되었다.

이 의구심은 1번 질문에 답이 있다. 클라이언트 컴포넌트는 아예 배제 하는 것이 아니라 자리를 표시하는 용도의 html 을 만들었기 때문에 브라우져에서 클라이언트 컴포넌트를 그리더라도, 해당 위치에 그리게끔 하면 되기에 가능한 것이였다.


생각보다도 더 React.js 와 Next.js 는 많은 일을 하고 있었다.

이전에 다룬 섹션인 useEffect와, Suspense 를 제외하고, 이번 섹션의 내용만 보더라도, 저렇게 간단한 Button 컴포넌트를 작성하는 데에도 수많은 코드가 쓰인다.
지금 경험용을 위해서 위에 처럼 작성했지만, 사실상 한 태그에서 일어나는 인터랙션은 너무나 다양하다. 그 이벤트를 구분하고, 어떤 핸들러인지, 또 그 수많은 핸들러 들에 대한 관리, 브라우져의 js 전송 등 지금 저 버튼 컴포넌트 하나만으로도 좀더 세밀하게 제어하기 위해서 얼마나 많은 코드를 작성해야할지 사실 감도 잘 안온다.

개발자들은 효율을 위해 어쩔수 없이 대형 라이브러리, 프레임 워크에 엄청나게 의존적이다. 리액트에 중대한 오류가 생기면 서비스 자체가 불가한 도메인들이 얼마나 많을까?

나름 Next.js의 수많은 기능을 경험 해봤다고 생각했는데도, 실제 내부 구조에 대해서 알거나, 집요하게 궁금해 한적은 없었던것 같다. 오히려 이번 섹션을 경험하면서 아마도 오늘 알게 된 내용도 얕은 지식일 것이라는 생각이 들며, 오히려 Next.js , React가 더 궁금해 졌다.

0개의 댓글