Loading...
본문 바로가기
👥
총 방문자
📖
0개 이상
총 포스팅
🧑
오늘 방문자 수
📅
0일째
블로그 운영

여러분의 방문을 환영해요! 🎉

다양한 개발 지식을 쉽고 재미있게 알려드리는 블로그가 될게요. 함께 성장해요! 😊

코딩 정보/NextJs

nextjs 동작원리 구현 통해 알아보자

by 꽁이꽁설꽁돌 2026. 4. 5.
728x90
SMALL
     
목차

     

     

     

    https://github.com/reactwg/server-components/discussions/5

     

    RSC From Scratch. Part 1: Server Components · reactwg server-components · Discussion #5

    RSC From Scratch. Part 1: Server Components In this technical deep dive, we'll implement a very simplified version of React Server Components (RSC) from scratch. This deep dive will be published in...

    github.com

     

    구현하기 전

    역할 분리를 위해서 다음과 같이 두개의 서버로 구현했습니다.

    실제로 두 서버는 nextjs에서 하나의 프로세스로 통합되어 있습니다.

     

    nextjs 다른 방식으로 보완

    • Edge Runtime 설정으로 특정 라우트를 엣지에서 실행 가능
    • ISR, 캐싱 등으로 RSC 결과를 캐시해서 DB 왕복을 줄임
    • Vercel 같은 플랫폼에서 서버리스 함수 단위로 배치 위치를 조절

     

    문서에서는 다음과 같이 서버를 나누면 이런 장점이 있을 수 있다고 합니다.

    RSC 서버 — 데이터센터에 배치

    • DB, API 등 데이터 접근이 필요
    • 데이터센터 가까이 있어야 DB 쿼리 지연시간이 낮음
    • 컴포넌트를 실행하고 JSX 트리를 만드는 무거운 연산 담당

    SSR 서버 — 엣지(유저 가까이)에 배치

    • RSC에서 받은 JSX를 HTML로 변환하거나 패스스루하는 가벼운 작업
    • DB 접근이 필요 없음
    • 유저와 가까울수록 HTML 응답이 빨리 도착

     

      유저 (서울) ←── 가까움 ──→ SSR 서버 (엣지, 도쿄)                                                                                                                                      
                                    │                                                                                                                                                       
                                    │ 내부 네트워크                                                                                                                                         
                                    ▼                                                                                                                                                       
                               RSC 서버 (데이터센터, 미국)                                                                                                                                  
                                    │                                                                                                                                                       
                                    ▼                                                                                                                                                       
                                  DB

     

     

    완성된 모습

     

    다음 레포를 통해 확인 가능합니다.

    https://github.com/sins051301/build-self-nextjs

     

    GitHub - sins051301/build-self-nextjs

    Contribute to sins051301/build-self-nextjs development by creating an account on GitHub.

    github.com

     

     

    초기 랜더링

     

    ### 1. 첫 페이지 로드 (SSR)
    
    ```
    브라우저가 http://localhost:8080/ 요청
            │
            ▼
        SSR 서버 (ssr.js)
            │
            │ fetchFromRSC() → http://localhost:9090/ 요청
            ▼
        RSC 서버 (rsc.js)
            │
            │ renderJSXToClientJSX(<Router />)
            │   → 서버 컴포넌트(Router, BlogLayout 등) 실행
            │   → 클라이언트 컴포넌트(Counter)는 "$C:Counter"로 마킹
            │
            │ JSON.stringify(clientJSX, stringifyJSX)
            │   → Symbol(react.transitional.element) → "$RE"
            │   → "$C:Counter" → "$$C:Counter" ($ 이스케이프)
            │
            ▼ JSX JSON 응답
        SSR 서버
            │
            │ renderJSXToHTML(clientJSX) → HTML 문자열 생성
            │ + <script>window.__INITIAL_CLIENT_JSX_STRING__ = ...</script> 삽입
            │
            ▼ HTML 응답
        브라우저
            │
            │ 1. HTML 화면 표시 (빠른 첫 화면)
            │ 2. client.bundle.js 로드
            │ 3. getInitialClientJSX()로 초기 JSX 파싱
            │    → "$RE" → Symbol 복원
            │    → "$$C:Counter" → clientComponentMap["Counter"] (실제 함수)
            │ 4. hydrateRoot(document, clientJSX) → React가 DOM에 연결
            │ 5. Counter 컴포넌트의 useState, onClick 활성화
            ▼
        인터랙티브한 페이지 완성
    ```

     

     

    ssr 서버의 요청

    // RSC 서버에서 JSX JSON을 가져옴
    async function fetchFromRSC(url) {
      const rscUrl = new URL(url.pathname, RSC_URL);
      const response = await fetch(rscUrl);
      if (!response.ok) {
        const err = new Error(`RSC server responded with ${response.status}`);
        err.statusCode = response.status;
        throw err;
      }
      return await response.text();
    }

     

     

    아래는 데이터 예시

     export function BlogLayout({ children }) {
        const author = "Jae Doe";
        return (
          <html>
            <head>
              <title>My blog</title>
            </head>
            <body>
              <nav>
                <a href="/">Home</a>
                <hr />
                <input />
                <Counter />
                <hr />
              </nav>
              <main>
                {children}
              </main>
              <Footer author={author} />
              <script src="/client.js"></script>
            </body>
          </html>
        );
      }

     

    rsc 서버의 직렬화

    1. renderJSXToClientJSX는 서버 컴포넌트를 실행해서 JSX 객체 트리를 만듭니다.
    • 이 함수는 jsx객체를 $$typeof: jsx.$$typeof (Symbol.for("react.transitional.element"))으로 반환한다.
    • type: "$C:" + componentName 클라이언트 컴포넌트의 경우 다음과 같이 표시하고 진행한다.

    이때 $C:는 이 학습용 프로젝트에서 임의로 정한 접두사입니다. React 내부에서는 $L, $ 같은 다른 마커를 사용합니다.

    //구조적으로 이 함수는 와 유사 renderJSXToHTML하지만, HTML 대신 객체를 순회하고 반환합니다.
    //renderJSXToClientJSX: 이후 페이지 이동 시 CSR용 → JSX를 객체 트리로 변환해서 React root.render()에 전달
    
    import { clientComponentMap } from "./clientComponentMap.js";
    
    export async function renderJSXToClientJSX(jsx) {
      if (jsx == null || typeof jsx === "boolean" || typeof jsx === "string" || typeof jsx === "number") {
        // 원시 값은 그대로 반환 (클라이언트에서 React가 처리)
        return jsx;
      } else if (Array.isArray(jsx)) {
        return await Promise.all(jsx.map((child) => renderJSXToClientJSX(child)));
      } else if (typeof jsx === "object") {
        if (jsx.$$typeof === Symbol.for("react.transitional.element")) {
          const props = jsx.props ?? {};
    
          if (typeof jsx.type === "function") {
            const Component = jsx.type;
            const componentName = Component.name;
    
            // 클라이언트 컴포넌트: 실행하지 않고 참조로 남김
            // 예: <Counter /> → { type: "$C:Counter", props: {...} }
            // 클라이언트에서 이 참조를 실제 컴포넌트로 교체하여 렌더링 - 클라이언트 컴포넌트 맵
            if (componentName in clientComponentMap) {
              const resolvedProps = {};
              for (const [propName, value] of Object.entries(props)) {
                resolvedProps[propName] = await renderJSXToClientJSX(value);
              }
              return {
                $$typeof: jsx.$$typeof,
                type: "$C:" + componentName,
                props: resolvedProps,
                ref: jsx.ref,
              };
            }
    
            // 서버 컴포넌트: 실행하여 해석
            const returnedJsx = await Component(props);
            return await renderJSXToClientJSX(returnedJsx);
          }
    
          // HTML 엘리먼트는 JSX 객체 형태로 유지하되, children만 재귀 처리
          const resolvedProps = {};
          for (const [propName, value] of Object.entries(props)) {
            resolvedProps[propName] = await renderJSXToClientJSX(value);
          }
    
          return {
            $$typeof: jsx.$$typeof,
            type: jsx.type,
            props: resolvedProps,
            ref: jsx.ref,
          };
        } else {
          // $$typeof가 없는 일반 객체 - 내부에 JSX가 있을 수 있으므로 재귀 처리
          // 예: BlogLayout의 props 객체 { children: <Main />, author: "Jae Doe" }
          //   → children에 JSX 컴포넌트가 있으므로 해석 필요
          //   → { children: { $$typeof: ..., type: "main", ... }, author: "Jae Doe" }
          return Object.fromEntries(
            await Promise.all(
              Object.entries(jsx).map(async ([propName, value]) => [
                propName,
                await renderJSXToClientJSX(value),
              ])
            )
          );
        }
      } else {
        throw new Error(`Not implemented: unsupported JSX type "${typeof jsx}".`);
      }
    }

     

     

    2. JSON.stringify(clientJSX, stringifyJSX)가 그 트리를 직렬화합니다.

    이때 Symbol은 JSON으로 변환이 안되기 때문에 직렬화가 필요합니다.

    JSON.stringify(Symbol.for("react.transitional.element"))  // undefined (사라짐)
    
    //React가 JSX 객체의 식별자로 일부러 Symbol을 선택했다는 뜻입니다. 
    //Symbol은 JSON.stringify에서 자동으로 사라지니까, 
    //외부에서 악의적인 JSON을 보내더라도$$typeof가 Symbol이 아닌 이상 React가 JSX로 인식하지 않습니다.
    //핵심은 Symbol.for('react.element')`<Symbol>`과 같은 심볼 값이 JSON 직렬화 과정에서 제거됩니다.

     

    그래서 "$RE" 문자열로 대체하고, 클라이언트에서 다시 Symbol로 복원합니다.

    직렬화 함수
    function stringifyJSX(key, value) {
      if (value === Symbol.for("react.transitional.element")) {
        return "$RE";
      } else if (typeof value === "string" && value.startsWith("$")) {
        return "$" + value;
      } else {
        return value;
      }
    }

     

    직렬화 이전 모습

    {
        $$typeof: Symbol.for("react.transitional.element"),
        type: "html",
        props: {
          children: [
            {
              $$typeof: Symbol.for("react.transitional.element"),
              type: "head",
              props: {
                children: {
                  $$typeof: Symbol.for("react.transitional.element"),
                  type: "title",
                  props: { children: "My blog" },
                  ref: null
                }
              },
              ref: null
            },
            {
              $$typeof: Symbol.for("react.transitional.element"),
              type: "body",
              props: {
                children: [
                  {
                    $$typeof: Symbol.for("react.transitional.element"),
                    type: "nav",
                    props: {
                      children: [
                        // ... 생략 ...
                        {
                          $$typeof: Symbol.for("react.transitional.element"),
                          type: "$C:Counter",    // 아직 $ 하나
                          props: {},
                          ref: null
                        },
                      ]
                    }
                  },
                ]
              }
            }
          ]
        }
      }

     

    직렬화 시킨 모습

    {
      "$$typeof": "$RE",                          // Symbol → "$RE"
      "type": "html",
      "props": {
        "children": [
          {
            "$$typeof": "$RE",
            "type": "head",
            "props": {
              "children": {
                "$$typeof": "$RE",
                "type": "title",
                "props": { "children": "My blog" },
                "ref": null
              }
            },
            "ref": null
          },
          {
            "$$typeof": "$RE",
            "type": "body",
            "props": {
              "children": [
                {
                  "$$typeof": "$RE",
                  "type": "nav",
                  "props": {
                    "children": [
                      // ... 생략 ...
                      {
                        "$$typeof": "$RE",
                        "type": "$$C:Counter",    // 클라이언트 컴포넌트 마킹
                        "props": {},
                        "ref": null
                      },
                    ]
                  }
                },
              ]
            }
          }
        ]
      }
    }

     

    • 리액트 심볼 타입의 경우 $RE로 직렬화를 진행합니다.
    • 클라이언트 컴포넌트의 경우 $$C:Counter로 $를 하나 더 붙여 진행합니다.

     

    다음과 같은 경우 때문에 $를 하나 더붙여 일반 문자열인지, 클라이언트 컴포넌트 참조인지를 구분 가능하게끔 만드는 것입니다.

      { "type": "$C:Counter" }     // 클라이언트 컴포넌트 참조                                                                                                                              
      { "children": "$C:Counter" } // 누군가 실제로 "$C:Counter"라는 텍스트를 쓴 경우

     

    ssr서버의 역직렬화

    다음과 같이 역직렬화 과정을 거치게 됩니다. 먼저 ssr랜더링부터 보겠습니다.

    1. parseJSX — JSON 문자열 → JSX 객체 복원 (역직렬화)
    2. renderJSXToHTML — 복원된 JSX 객체 → HTML 문자열 생성
    역직렬화 함수
    const clientJSX = JSON.parse(clientJSXString, parseJSX);
    
    // RSC 서버에서 받은 JSX JSON을 파싱할 때 Symbol 복원
    function parseJSX(key, value) {
      if (value === "$RE") {
        return Symbol.for("react.transitional.element");
      } else if (typeof value === "string" && value.startsWith("$$")) {
        return value.slice(1);
      } else {
        return value;
      }
    }

     

    {
      $$typeof: Symbol(react.transitional.element),
      type: 'html',
      props: {
        children: [
          {
            $$typeof: Symbol(react.transitional.element),
            type: 'head',
            props: {
              children: {
                $$typeof: Symbol(react.transitional.element), -> 타입 심볼형태로 역직렬화
                type: 'title',
                props: { children: 'My blog' },  // ← 문자열 그대로
                ref: null
              }
            },
            ref: null
          },
          {
            $$typeof: Symbol(react.transitional.element),
            type: 'body',
            props: {
              children: [
                {
                  $$typeof: Symbol(react.transitional.element),
                  type: 'nav',
                  props: {
                    children: [
                      // ... a, hr, input 생략 ...
                      {
                        $$typeof: Symbol(react.transitional.element),
                        type: '$C:Counter',  // 참조 클라이언트 컴포넌트 마킹
                        props: {},
                        ref: null
                      },
                      // ...
                    ]
                  }
                },
                // ... main, footer, script 생략 ...
              ]
            }
          }
        ]
      }
    }

     

    다음의 함수를 통해 jsx객체를 복원한 것을 html 문자열로 바꾸게 됩니다.

     

    크게 가져갈 부분은 3개 입니다.

    1. 재귀적으로 html문자열로 바뀌게 된다.
    2. 클라이언트 컴포넌트의 경우 빈 문자열을 통해 위치를 비워둔다.
    3. escpeHtml함수를 통해 xss공격을 막고 있다.

     

    "빈 문자열로 비우면 Counter가 화면에 아예 안 보이는 거 아닌가?" → hydration 전까지는 비어있습니다.

    따라서 콘솔 보면 하이드레이션 에러가 발생합니다.

    escapeHtml('<script>alert("xss")</script>')                                                                                                                                           
      // 결과: "&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;"

     

    //renderJSXToHTML: 첫 페이지 로드 시 SSR용 → JSX를 HTML 문자열로 변환
    
    import escapeHtml from "escape-html";
                           
    export async function renderJSXToHTML(jsx) {
      try {
        if (jsx == null || typeof jsx === "boolean") {
          return "";
        } else if (typeof jsx === "string" || typeof jsx === "number") {
          return escapeHtml(String(jsx));
        } else if (Array.isArray(jsx)) {
          return (await Promise.all(jsx.map((child) => renderJSXToHTML(child)))).join("");
        } else if (typeof jsx === "object") {
          const $typeof = jsx.$$typeof;
          const isReactElement = $typeof === Symbol.for("react.transitional.element");
    
          if (isReactElement) {
            const props = jsx.props ?? {};
            const FRAGMENT = Symbol.for("react.fragment");
    
            if (jsx.type === FRAGMENT) {
              return await renderJSXToHTML(props.children);
            }
    
            // type이 함수인 경우 = 커스텀 컴포넌트 (서버 컴포넌트)
            // 예: <BlogLayout children={...}> → type은 BlogLayout 함수
            //   → BlogLayout({ children: ... })를 호출하면 <html><body>...</body></html> JSX를 반환
            //   → 반환된 JSX를 다시 renderJSXToHTML로 재귀 처리하여 HTML 문자열로 변환
            // "div", "html" 같은 문자열 type은 아래에서 바로 HTML 태그로 렌더링하지만,
            // 함수 type은 실행해야 실제 렌더링할 JSX를 알 수 있음
            if (typeof jsx.type === "function") {
              const Component = jsx.type;
              const props = jsx.props;
              const returnedJsx = await Component(props);
              return await renderJSXToHTML(returnedJsx);
            }
    
            // 클라이언트 컴포넌트 참조: 서버에서는 렌더링할 수 없으므로
            // placeholder를 삽입하고 클라이언트에서 hydration 시 실제 컴포넌트로 교체
            if (typeof jsx.type === "string" && jsx.type.startsWith("$C:")) {
              return "";
            }
    
            let html = "<" + jsx.type;
            for (const propName in props) {
              if (
                Object.prototype.hasOwnProperty.call(props, propName) &&
                propName !== "children"
              ) {
                html += " ";
                html += propName;
                html += "=";
                html += escapeHtml(String(props[propName]));
              }
            }
            html += ">";
            html += await renderJSXToHTML(props.children);
            html += "</" + jsx.type + ">";
            return html;
          }
        } else {
          const err = new Error(`Not implemented: unsupported JSX type "${typeof jsx}".`);
          err.statusCode = 500;
          throw err;
        }
      } catch (e) {
        if (!e.statusCode) {
          e.statusCode = 500;
        }
        throw e;
      }
    }

     

     

    브라우저단 js bundle 요청을 통한 csr

    아까 ssr 랜더링 시에 위치를 비워 놓았음으로 이제 하이드레이션을 통한 csr이 일어나야 합니다.

    먼저 역직렬화가 된 뒤에 하이드레이션이 일어나게 됩니다.

     

    const root = hydrateRoot(document, getInitialClientJSX());
    
    //getInitialClientJSX의 경우 jsx트리를 제공하여 클라이언트 컴포넌트가 하이드레이트 가능하게 만듭니다.
    function getInitialClientJSX() {
      // 서버가 HTML에 심어둔 초기 JSX JSON 문자열을 파싱하여 hydration에 사용
      const raw = window.__INITIAL_CLIENT_JSX_STRING__;
      const clientJSX = JSON.parse(raw, parseJSX);
      return clientJSX;
    }

     

    1. 역직렬화를 한다.
    function parseJSX(key, value) {
      if (value === "$RE") {
        //아까 본 $RE 가 리액트 엘리먼트 타입으로 바뀜
        return Symbol.for("react.transitional.element");
        
      } else if (typeof value === "string" && value.startsWith("$$C:")) {
        // 클라이언트 컴포넌트 참조: "$$C:Counter" (서버에서 $이스케이프됨) → 실제 Counter 컴포넌트로 교체
        const componentName = value.slice(4);
        return clientComponentMap[componentName]; //클라이언트 컴포넌트 맵
        
      } else if (typeof value === "string" && value.startsWith("$$")) {
        // This is a string starting with $. Remove the extra $ added by the server.
        return value.slice(1);
      } else {
        return value;
      }
    }

     

    2. 클라이언트 컴포넌트 맵을 거친다.

     

    // 클라이언트 컴포넌트 맵
    // 서버: 이 맵에 등록된 컴포넌트는 실행하지 않고 참조("$C:이름")
    // 클라이언트: 참조를 받으면 이 맵에서 실제 컴포넌트를 찾아 렌더링
    
    
    //클라이언트: parseJSX가 "$C:Counter"를 만나면 실제 Counter 함수로 교체
    //React가 렌더링: 교체된 Counter 함수를 브라우저에서 실행 → useState, onClick 동작
    
    import { Counter } from "./Counter.js";
    
    // 클라이언트 컴포넌트를 추가하려면 여기에 등록
    export const clientComponentMap = {
      Counter,
    };

     

    3.html을 하이드레이션 한다.

    React가 hydrateRoot를 실행할 때 이 객체를 만나면 Counter 함수를 호출해서 그 위치에 렌더링합니다.

    hydrateRoot: React DOM이 제공하는 함수로, 이미 존재하는 HTML에 React를 연결합니다.

     // "$$C:Counter" → clientComponentMap["Counter"] → 실제 Counter 함수                                                                                                                  
      {                                                                                                                                                                                     
        $$typeof: Symbol(react.transitional.element),                                                                                                                                       
        type: Counter,  // ← 문자열이 아니라 실제 함수                                                                                                                                      
        props: {},                                                                                                                                                                          
        ref: null                                                                                                                                                                           
      }

     

    페이지 이동 시

    이때의 차이점은 SSR 서버는 RSC 서버의 JSON을 그대로 전달만 합니다.

    ### 2. 클라이언트 사이드 네비게이션 (CSR)
    
    ```
    유저가 링크 클릭 (예: /hello-world)
            │
            │ e.preventDefault() → 브라우저 기본 새로고침 방지
            │ history.pushState() → URL만 변경
            │
            ▼
        client.js의 navigate() 함수
            │
            │ fetch("/hello-world?jsx") → SSR 서버에 JSX 요청
            ▼
        SSR 서버 (ssr.js)
            │
            │ ?jsx 파라미터 감지 → HTML 대신 JSON으로 응답
            │ fetchFromRSC() → RSC 서버에서 JSX JSON 가져옴
            │
            ▼ JSX JSON 패스스루
        client.js
            │
            │ JSON.parse(response, parseJSX)
            │   → Symbol 복원 + 클라이언트 컴포넌트 교체
            │
            │ root.render(clientJSX)
            │   → React가 기존 DOM과 비교(diffing)
            │   → 바뀐 부분만 업데이트
            │   → <input> 값 등 기존 상태 유지
            ▼
        새로고침 없이 페이지 전환 완료
    ```

     

     

    아래를 보면 초기 랜더링 시에는 client.js를 통해 번들링을 가져옵니다.

    이동 시에는 파일이름?jsx로 이동하고 요청하게 됩니다.

      const url = new URL(req.url, `http://${req.headers.host}`);
        if (url.pathname === "/client.js") {
          // 정적 파일 서빙 (번들된 클라이언트 JS)
          await sendScript(res, "./client.bundle.js");
        } else if (url.searchParams.has("jsx")) {
          // 클라이언트 사이드 네비게이션용: RSC 서버에서 받은 JSX JSON을 그대로 패스스루
          url.searchParams.delete("jsx");
          const clientJSXString = await fetchFromRSC(url);
          res.setHeader("Content-Type", "application/json");
          res.end(clientJSXString);
        }

     

     

    client.js를 통해 다음의 navigate가 라우팅을 통해 클라이언트 랜더링을 하게끔 합니다.

    async function navigate(pathname) {
      currentPathname = pathname;
      const clientJSX = await fetchClientJSX(pathname);
      if (pathname === currentPathname) {
        root.render(clientJSX);
      }
    }
      
      window.addEventListener("click", (e) => {
        // Only listen to link clicks.
        if (e.target.tagName !== "A") {
          return;
        }
        // Ignore "open in a new tab".
        if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
          return;
        }
        // Ignore external URLs.
        const href = e.target.getAttribute("href");
        if (!href.startsWith("/")) {
          return;
        }
        // Prevent the browser from reloading the page but update the URL.
        e.preventDefault();
        window.history.pushState(null, null, href);
        // Call our custom logic.
        navigate(href);
      }, true);
      
      window.addEventListener("popstate", () => {
        // When the user presses Back/Forward, call our custom logic too.
        navigate(window.location.pathname);
      });

     

    클라이언트의 역직렬화

    이동을 하고 rsc에서 직렬화된 json 응답을 받은 후 다음과 같이 클라이언트에서 역직렬화 함수로 역직렬화를 하게 됩니다.

    ssr서버는 응답을 받은 뒤 그대로 브라우저로 넘기게 됩니다.

    async function fetchClientJSX(pathname) {
      const response = await fetch(pathname + "?jsx");
      const clientJSXString = await response.text();
      const clientJSX = JSON.parse(clientJSXString, parseJSX);
      return clientJSX;
    }

     

    브라우저는 응답을 받아 아까 봤던 역직렬화 함수를 실행하게 됩니다.

    function parseJSX(key, value) {
      if (value === "$RE") {
        //아까 본 $RE 가 리액트 엘리먼트 타입으로 바뀜
        return Symbol.for("react.transitional.element");
        
      } else if (typeof value === "string" && value.startsWith("$$C:")) {
        // 클라이언트 컴포넌트 참조: "$$C:Counter" (서버에서 $이스케이프됨) → 실제 Counter 컴포넌트로 교체
        const componentName = value.slice(4);
        return clientComponentMap[componentName]; //클라이언트 컴포넌트 맵
        
      } else if (typeof value === "string" && value.startsWith("$$")) {
        // This is a string starting with $. Remove the extra $ added by the server.
        return value.slice(1);
      } else {
        return value;
      }
    }

     

    다음과 같이 alert를 띄어보면 어떤식으로 이제 응답을 받는지 볼 수 있습니다.

    async function navigate(pathname) {
      currentPathname = pathname;
      const clientJSX = await fetchClientJSX(pathname);
      if (pathname === currentPathname) {
        alert(JSON.stringify(clientJSX, null, 2));
        root.render(clientJSX);
      }
    }

     

    이동시에 응답 받는 형태

    {                                                                                                                                                                                     
        "type": "html",                                                                                                                                                                     
        "props": {                                                                                                                                                                          
          "children": [                                                                                                                                                                     
            {                                                                                                                                                                               
              "type": "head",                                                                                                                                                               
              "props": {                                                                                                                                                                    
                "children": {                                                                                                                                                               
                  "type": "title",                                                                                                                                                          
                  "props": { ... }                                                                                                                                                          
                }                                                                                                                                                                           
              },                                                                                                                                                                        
              "ref": null                                                                                                                                                                   
            },                                                                                                                                                                              
            {                                                                                                                                                                               
              "type": "body",                                                                                                                                                               
              "props": {                                                                                                                                                                    
                "children": [                                                                                                                                                           
                  {
                    "type": "nav",
                    "props": { ... }
                  },
                  ...                                                                                                                                                                       
                ]                                                                                                                                                                           
              },                                                                                                                                                                            
              "ref": null                                                                                                                                                                   
            }                                                                                                                                                                               
          ]                                                                                                                                                                             
        }
      }

     

     

    root.render(clientJSX)가 새 JSX 트리를 받으면 React가 기존 DOM과 비교(diffing)해서 바뀐 부분만 업데이트합니다.

    root.render: 리액트 내부 구현함수로 React DOM의 reconciler(재조정기)가 새 JSX 트리와 현재 DOM을 비교해서 최소한의 변경만 적용합니다.

    async function navigate(pathname) {
      currentPathname = pathname;
      const clientJSX = await fetchClientJSX(pathname);
      if (pathname === currentPathname) {
        root.render(clientJSX);
      }
    }

     

    결론

    nextjs는 rsc서버의 직렬화와 ssr 서버 또는 클라이언트의 역직렬화의 반복이라고 볼 수 있습니다.

    서버와 브라우저는 서로 다른 환경이라 JSX 객체를 직접 주고받을 수 없기 때문입니다. Symbol은 네트워크로 전송이 안 되고, 함수도 JSON으로 보낼 수 없습니다. 그래서 직렬화로 변환하고, 받는 쪽에서 역직렬화로 복원하는 과정이 필수입니다

     

     

    stringifyJsx - parseJsx

    단계 누가 뭘하나
    직렬화 RSC 서버 JSX 객체 → JSON 문자열
    역직렬화 SSR 서버 JSON → JSX 객체 → HTML로 렌더링 (첫 페이지)
    역직렬화 클라이언트 JSON → JSX 객체 → hydrateRoot()로 DOM 연결 (첫 페이지)
    역직렬화 클라이언트 JSON → JSX 객체 → root.render()로 DOM 업데이트 (네비게이션)

     

     

    미구현된 것들..

    • JSON 대신 RSC Payload라는 스트리밍 형식 사용 (줄 단위로 점진적 전송)
    • clientComponentMap을 수동 등록하는 대신 "use client" 지시어로 자동 처리
    • 번들링, 코드 스플리팅, 캐싱 등 최적화가 추가됨

     

    방식은 유사하되 다음과 같이 rsc 페이로드를 통해 스트리밍이 가능합니다.

     // 우리 프로젝트: JSON 한 번에 전송                                                                                                                                                   
      {"$$typeof":"$RE","type":"html","props":{"children":[...]}}

     

    rsc페이로드

    Transfer-Encoding: chunked입니다. HTTP 응답의 전체 크기를 미리 알려주지 않고, 준비된 청크 단위로 계속 보내다가 끝나면 종료 신호를 보내는 방식입니다.

      // RSC Payload: 줄 단위로 스트리밍                                                                                                                                                    
      0:["$","html",null,{"children":[["$","head",null,...],["$","body",null,...]]}]                                                                                                        
      1:["$","div",null,{"children":"Hello World"}]                                                                                                                                         
      2:["$","$Lcounter",null,{"count":0}]