wanted 7월 챌린지 사전과제

하머·2023년 6월 25일
0
post-thumbnail

1. CSR(Client-side Rendering)이란 무엇이며, 그것의 장단점에 대하여 설명해주세요.

CSR은 html이나 css 전체를 전달하는것이 아닌 번들링된 자바스크립트 파일을 전달해 그 파일을 이용하여 클라이언트 측에서 화면을 렌더링하는 방식이다.

장점

  • 번들링된 파일을 초기에 모두 가져오기 때문에 첫페이지 로딩을 제외한 페이지 이동, 부분 렌더링 속도가 빠르다.

  • 서버 측에서는 첫 로딩을 제외하고 JSON만 전달하면 되는 경우가 많으므로 SSR 대비 서버의 비용이 적다.

단점

  • 첫 페이지 로딩에서 비교적 큰 자바스크립트 파일을 받아야 하므로 FP(First Paint)와 FCP(First Contentful Paint) 등의 초기 렌더링까지의 시간이 오래걸려 애플리케이션이 커질 수록 초기 페이지 로딩에서의 시간이 오래 걸린다.

  • index.html만 전달하는 경우가 대다수 이므로 메타 데이터를 봇에게 전달하지 못해 SEO 점수에서 SSR 대비 불리하다.

  • 클라이언트 측에서 대부분의 렌더링이 이루어지므로 사용자의 성능이 좋지않은 경우 (오래된 컴퓨터, 혹은 모바일 등) 장점인 페이지 이동, 부분렌더링이 느려질 수 있다.

2. SPA(Single Page Application)로 구성된 웹 앱에서 SSR(Server-side Rendering)이 필요한 이유에 대하여 설명해주세요.

  • 모두 클라이언트 측에서 렌더링이 이루어지는 것이 아닌 서버 측 렌더링이 이루어진 뒤 hydrate 과정을 거치기 때문에 첫 페이지 로딩에서 더 나은 사용자 경험을 제공 할 수 있다.
  • 봇이 인식할 수 있는 정보를 페이지마다 다이나믹하게 html로 전달 할 수 있기 때문에 SEO에 있어서 유리하다.
  • JS를 통한 렌더링이 적으므로 디바이스 성능에 있어서 비교적 자유롭다.

참고 문서 : https://web.dev/rendering-on-the-web/

3. Next.js 프로젝트에서 yarn start(or npm run start) 스크립트를 실행했을 때 실행되는 코드를 Next.js Github 레포지토리에서 찾은 뒤, 해당 파일에 대한 간단한 설명을 첨부해주세요.

next의 스크립트 파일과 서버 동작

  • next의 cli 파일 인(next-start.ts)에서 스크립트 동작이 시작된다.
    • line 85의 startServer 함수에서 서버를 킴
  • server/lib/start-server.ts의 startServer 함수에서는 http.createServer 함수를 통해 서버를 열고 'next' 인스턴스를 만들며 prepare 함수를 통해 필요한 작업들을 한다.
// setup server listener as fast as possible
  const server = http.createServer(async (req, res) => {
    try {
      if (handlersPromise) {
        await handlersPromise
        handlersPromise = undefined
      }
      sockets.add(res)
      res.on('close', () => sockets.delete(res))
      await requestHandler(req, res)
    } catch (err) {
      res.statusCode = 500
      res.end('Internal Server Error')
      Log.error(`Failed to handle request for ${req.url}`)
      console.error(err)
    }
  })
  
...

  const next = require('../next') as typeof import('../next').default
  const addr = server.address()
  const app = next({
    dir,
    hostname,
    dev: isDev,
    isNodeDebugging,
    httpServer: server,
    customServer: false,
    port: addr && typeof addr === 'object' ? addr.port : port,
  })
  • next 인스턴스의 this.prepare -> this.getServer -> this.createServer -> this.getServerImpl 로 이어지며 next-server.ts 파일의 class NextNodeServer 에서 실질적인 next서버의 구현을 확인할 수 있다.
  • next-server 파일의 눈에 띄는 코드들
    • 언더바 app, document를 가장 먼저 로드함
    // line 253
    loadComponents({
      distDir: this.distDir,
      pathname: '/_document',
      hasServerComponents: false,
      isAppPath: false,
    }).catch(() => {})
    loadComponents({
      distDir: this.distDir,
      pathname: '/_app',
      hasServerComponents: false,
      isAppPath: false,
    }).catch(() => {})
    • routerWorker를 사용 시 프로세스를 분리하여 라우팅을 생성함
      이름이 워커인걸 보아 웹 워커를 써서 라우팅하는 옵션이 존재하는 듯 했다.
    // line 267
    if (this.isRouterWorker) {
      ...
      const { createWorker, createIpcServer } =
        require('./lib/server-ipc') as typeof import('./lib/server-ipc')
    • 대부분의 route생성이 여기서 이루어진다. 따라가면 path나 fs으로 생성된다.

Next의 렌더링과 hydration과정

  • next-server.ts 파일의 renderHTMLImpl에서 render.tsx 파일의 renderToHTML로 렌더링을 구현한다.

  • render.tsx의 눈에 띄는 코드들

    • tracer를 통해 getStaticSideProps, getServerSideProps를 추적하여 별도의 데이터 생성 로직을 바인딩한다.
    • html render시 renderToString을 통해 렌더링 해주는데 이는 ReactDOMServer.renderToReadableStream 을 래핑하는 함수이다.
      async function renderToString(element: React.ReactElement) {
      const renderStream = await ReactDOMServer.renderToReadableStream(element)
      await renderStream.allReady
      return streamToString(renderStream)
      }
  • renderToReadableStream

    • renderToReadableStream는 리액트 트리를 ReadableStream형태의 html로 파싱해주는 메서드 이다.

    • ReadableStream 이란?
      동영상 스트리밍 처럼 데이터를 쪼갠 뒤 이어 받을 수 있는 Stream API의 인터페이스
      => 서버 렌더링과 클라이언트 렌더링을 나누는 hydrate를 구현할 때 사용되는 기술인듯 하다.

    • 리액트 공식문서에서는 클라이언트 측에서 hydrateRoot를 통해 서버에서 부트스트래핑된 리액트 트리를 이어서 받아 렌더링 할 수 있다고 한다.

      export default function App() {
      return (
        <html>
          <head>
            <meta charSet="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1" />
            <link rel="stylesheet" href="/styles.css"></link>
            <title>My app</title>
          </head>
          <body>
            <Router />
          </body>
        </html>
      );
      }
      ...
      import { hydrateRoot } from 'react-dom/client';
      import App from './App.js';
      
      hydrateRoot(document, <App />);
      
  • next에서도 client 측 코드를 정리한 곳에서 같은 방법으로 hydrate 해주고 있었다.

    • vercel/next.js/packages/next/src/client/app-index.tsx 의 hydrate 함수

       const reactRoot = isError
       ? (ReactDOMClient as any).createRoot(appElement, options)
       : (React as any).startTransition(() =>
           (ReactDOMClient as any).hydrateRoot(appElement, reactEl, options)
         )
      if (isError) {
       reactRoot.render(reactEl)
      }
    • vercel/next.js/packages/next/src/client/index.tsx 의 renderReactElement 함수

        function renderReactElement(
      domEl: HTMLElement,
      fn: (cb: () => void) => JSX.Element
      ): void {
      // mark start of hydrate/render
      if (ST) {
        performance.mark('beforeRender')
      }
      
      const reactEl = fn(shouldHydrate ? markHydrateComplete : markRenderComplete)
      if (!reactRoot) {
        // Unlike with createRoot, you don't need a separate root.render() call here
        reactRoot = ReactDOM.hydrateRoot(domEl, reactEl, {
          onRecoverableError,
        })
        // TODO: Remove shouldHydrate variable when React 18 is stable as it can depend on `reactRoot` existing
        shouldHydrate = false
      } else {
        const startTransition = (React as any).startTransition
        startTransition(() => {
          reactRoot.render(reactEl)
        })
      }
      }

0개의 댓글