[Next.js] CSR / SSR with Next.js

dolfin·2023년 6월 19일
1

Next.js

목록 보기
1/4

리액트는..클라이언트..사이드..렌더링...

리액트를 처음 시작했을때 가장 많이 들었던 단어 중 하나가 CSR 인 거 같다.
next.js로 넘어오면서 한번 정리를 해보고 넘어가려고 한다 (!)

CSR 등장 전

웹은 크게 서버와 클라이언트로 나눌 수 있다.

사용자 즉 클라이언트측에서 위와 같이 서버에 요청을 보내면

서버는 요렇게 응답을 해준다. 기본적으로 html 문서를 응답 해준다.

계속 이러한 요청과 응답 과정을 반복하게 된다.

하지만, 웹이 점점 발전함에 따라 주고 받아야할 정보가 늘어나게 되면서

서버 측에서 점점 과도한 요청을 처리하는 부담을 갖게 된다.

또한 클라이언트 측에서도 응답을 받아올때마다

페이지 전체가 렌더링이 일어나면서 계속 깜빡 거리는 현상을 겪게 된다.

그러면서 등장하게 된 것이 바로 SPA이다.

html 문서를 계속해서 내려 받지 않고 하나의 문서안에서 필요한 부분만 수정해나가는 기술이다.

CSR (client side rendering)

바로 SPA에서 쓰이는 기법이 CSR이다. 서버에서 화면을 받아서 구성하던 기존 방식(SSR)과 달리 처음 한번 서버에 요청하여 데이터 전체를 로드하고, 변경사항이 있을 때만 해당 부분을 수정하는 방식 이다. react, vue 등에서 쓰이는 방식 이다.

페이지 전체를 받아오는게 아니기 때문에 서버 부하가 적고 구동 속도가 빠르다는 장점이 있다 (!)

하지만 장점만 있지는 않다.

리액트로 작업한 웹페이지가 응답 받아온 것을 보면 html 파일에 딱히 어떠한 작업 내용을 볼 수가 없다.

<script defer src="/static/js/bundle.js"></script>

작업한 파일들은 모두 bundle.js라는 파일로 한번에 번들되어 오고 있기 때문이다. 번들된 파일들은 처음 로딩시 받아와야하기 때문에 초기 로딩 시간을 꽤 잡아 먹게 되고.. 사용자는 빈 화면만 보는 경우가 생길 수 있다. (code splitting 이라는 기술로 어느정도 해결 가능 하다 - url 마다 js 파일을 분할 시켜 파일의 크기를 줄여준다.)

FCP(First Contentful Paint) : 처음 사용자에게 표기되는 시점
TTI(Time To Interactive) : 페이지가 사오작용 가능하게 될 때 까지의 시간

또한, 비어있는 Html 파일을 받아오면서 검색 봇이 빈 페이지로 착각하여 검색엔진 최적화에 취약 하기도 하다. (구글 제외..)

SPA with SSR

위에서 설명한 것 처럼 CSR 방식에는 몇 가지 단점이 존재한다.

  1. 초기 로딩 속도가 느림
  2. 검색 엔진 최적화에 취약

이러한 단점들을 해결하기 위해 등장한 것이 바로 Next.js

Next.js는 리액트의 SSR을 구현할 수 있게 도와주는 프레임워크이다.

Next.js의 pre-rendering

Next.js는 두 가지 형태의 pre-rendering 방식을 가지고 있다.
1. Static
2. Server-side Rendering

두 방식의 차이는 html 문서가 생성되는 시점이다.

static 생성방식은 빌드시에 생성되며, 요청때마다 재사용되는 특징이 있다.

SSR 방식은 요청이 있을때 html을 생성한다.

이러한 생성방식은 개발시 개발자가 선택하여 고를수 있다.

그렇다면 언제 static 쓰고 언제SSR을 써야될까?

Nest.js 개발팀에서는 대부분의 경우 static 방식을 추천한다고 한다.

"사용자 요청이 있기전에 미리 렌더링 해도 될까?" 라는 질문에 "응, 가능할거 같은데?" 싶으면 static을 하라고 한다(ㅋㅋ)

대신에 데이터 변동이 잦고 항상 최신 데이터를 보여줘야하는 사이트의 경우에는 SSR 방식을 사용해야한다.

yarn start

next js 프로젝트를 다운받고 스크립트를 실행해보았다.

Next.js Github 레포지토리
아래 경로를 통해 next start 실행 코드를 찾을 수 있었다.

next.js/packages/next/src/cli /next-start.ts


const nextStart: CliCommand = async (argv) => {
  const validArgs: arg.Spec = {
    // Types
    '--help': Boolean,
    '--port': Number,
    '--hostname': String,
    '--keepAliveTimeout': Number,

    // Aliases
    '-h': '--help',
    '-p': '--port',
    '-H': '--hostname',
  }
  let args: arg.Result<arg.Spec>
  try {
    args = arg(validArgs, { argv })
  } catch (error) {
    if (isError(error) && error.code === 'ARG_UNKNOWN_OPTION') {
      return printAndExit(error.message, 1)
    }
    throw error
  }
  if (args['--help']) {
    console.log(`
      Description
        Starts the application in production mode.
        The application should be compiled with \`next build\` first.

      Usage
        $ next start <dir> -p <port>

      <dir> represents the directory of the Next.js application.
      If no directory is provided, the current directory will be used.

      Options
        --port, -p          A port number on which to start the application
        --hostname, -H      Hostname on which to start the application (default: 0.0.0.0)
        --keepAliveTimeout  Max milliseconds to wait before closing inactive connections
        --help, -h          Displays this message
    `)
    process.exit(0)
  }
  

상단에는 yarn start 옵션을 옵션들에 대해 타입과, alias 등을 명시하고 있다.

  const dir = getProjectDir(args._[0])
  const host = args['--hostname']
  const port = getPort(args)

  const keepAliveTimeoutArg: number | undefined = args['--keepAliveTimeout']
  if (
    typeof keepAliveTimeoutArg !== 'undefined' &&
    (Number.isNaN(keepAliveTimeoutArg) ||
      !Number.isFinite(keepAliveTimeoutArg) ||
      keepAliveTimeoutArg < 0)
  ) {
    printAndExit(
      `Invalid --keepAliveTimeout, expected a non negative number but received "${keepAliveTimeoutArg}"`,
      1
    )
  }

keepAliveTimeoutArg 값이 유효하지 않을 경우 에러 메시지와 함께 종료된다.

  const keepAliveTimeout = keepAliveTimeoutArg
    ? Math.ceil(keepAliveTimeoutArg)
    : undefined

  const config = await loadConfig(
    PHASE_PRODUCTION_SERVER,
    resolve(dir || '.'),
    undefined,
    undefined,
    true
  )

  await startServer({
    dir,
    isDev: false,
    hostname: host,
    port,
    keepAliveTimeout,
    useWorkers: !!config.experimental.appDir,
  })
}

export { nextStart }

값이 유효하다면 startServer라는 함수를 실행 시킨다.
startServer 함수를 살짝 엿보도록 하쟈

next.js/packages/next/src/server/lib /start-server.ts 전체 코드


  // 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)
    }
  })



  await new Promise<void>((resolve) => {
    server.on('listening', () => {
      const addr = server.address()
      port = typeof addr === 'object' ? addr?.port || port : port

      let host = !hostname || hostname === '0.0.0.0' ? 'localhost' : hostname

      let normalizedHostname = hostname || '0.0.0.0'

      if (isIPv6(hostname)) {
        host = host === '::' ? '[::1]' : `[${host}]`
        normalizedHostname = `[${hostname}]`
      }
      targetHost = host

      const appUrl = `http://${host}:${port}`

      Log.ready(
        `started server on ${normalizedHostname}${
          (port + '').startsWith(':') ? '' : ':'
        }${port}, url: ${appUrl}`
      )
      resolve()
    })
    server.listen(port, hostname)
  })


      const getProxyServer = (pathname: string) => {
        const targetUrl = `http://${
          targetHost === 'localhost' ? '127.0.0.1' : targetHost
        }:${routerPort}${pathname}`
        const proxyServer = httpProxy.createProxy({
          target: targetUrl,
          changeOrigin: false,
          ignorePath: true,
          xfwd: true,
          ws: true,
          followRedirects: false,
        })

        proxyServer.on('error', (_err) => {
          // TODO?: enable verbose error logs with --debug flag?
        })
        return proxyServer
      }

해당 함수는 서버를 구동하기 위한 코드들이 작성 되어 있다.
받아온 데이터 (hostname, port 등) 을 통해 서버를 실행시킨다.

결말

아직 Next js를 배운적이 없어서 막연히 낯설고 두려운 느낌이 있었는데 구동방식에 대해 먼저 공부를 하고 나니까 정말 획기적인 프레임워크라는 생각이 들어 얼른 배워보고 싶다. 많은분들이 찬양하는 이유를 좀 알 거 같다. 또 그동안 공식 레포지토리를 볼 생각을 한 적이 없는데 이해하기는 물론 어려웠지만..(ㅜ) 재밌는 경험이었다.

profile
no risk no fun

0개의 댓글