CSR vs SSR, 그리고 Next.js

ZzoeE·2022년 9월 30일
0

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

CSR의 정의

CSR이란 웹페이지의 렌더링이 클라이언트 브라우저에서 발생하는 것으로, 어플리케이션 구동에 필요한 파일 (HTML, CSS, JS 등)을 최초 요청에서 모두 다운로드하여 뷰가 구성된다.

장점

  • 서버 부담 / 후속 페이지 로드 시간 감소
    최초에 필요한 파일을 모두 받아오기 때문에 사이트의 다른 페이지로 이동 시에 이미 받아놓은 JS 실행을 하기 때문에 서버에 요청할 필요가 없고, 로딩 시간 또한 빠르다.
  • 사용자 경험
    새로고침이 발생하지 않기 때문에 내부 컨텐츠 변경으로 인한 깜빡임 현상 등이 발생하지 않아 사용자 친화적인 환경을 제공할 수 있다.

단점

  • 초기 로딩 속도 느림
    초기에 모든 파일을 로드한 후에 뷰를 구성해야하기 때문에 앱의 규모가 커질 수록 초기 구동시간이 크게 느려진다.
  • SEO에 불리
    웹 페이지의 콘텐츠가 JS를 통해 동적으로 변경되기 때문에, 과거의 검색 엔진 크롤러는 JS 실행을 하지 않아서 빈페이지를 마주하기 때문에 CSR 방식은 SEO에 매우 불리하다.

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

일반적인 SPA 의 렌더링 방식은 CSR을 사용하나, CSR은 SEO에 불리하다는 단점이 있다. 사용자 유입이 중요하여 검색 엔진 최적화가 우선되어야 할 경우, SSR 방식이 적합하다.

SSR은 서버에서 사용자에게 보여줄 페이지를 모두 렌더링하여 전달하는 방식이다. 서버에서 페이지 구성이 모두 이뤄진 뒤 전달되기 때문에 CSR 방식에 비해 SEO에 훨씬 유리하다.

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

(1) PROJECT/package.json

{
	...
    "scripts" : {
    	"dev": "next dev",
      	"build": "next build",
      	"start": "next start"
    },
  	...
}

create-next-app 를 통해 프로젝트 생성하고, package.json 파일의 scripts 부분을 확인하였다.
yarn start 스크립트 실행 시 "next start"가 실행되는 것을 볼 수 있다.

(2) next.js/packages

next.js/packages 폴더에서 next.js 관련 package들이 나뉘어 있는 것을 발견할 수 있었다. 프로젝트 세팅하며 사용했던 create-next-app 패키지 등이 담겨 있다. 여기서 next 내부를 확인해 본다.

(3) next.js/packages/next/lib/commands.ts

export type cliCommand = (argv?: string[]) => void

export const commands: { [command: string]: () => Promise<cliCommand> } = {
  build: () => Promise.resolve(require('../cli/next-build').nextBuild),
  start: () => Promise.resolve(require('../cli/next-start').nextStart),
  export: () => Promise.resolve(require('../cli/next-export').nextExport),
  dev: () => Promise.resolve(require('../cli/next-dev').nextDev),
  lint: () => Promise.resolve(require('../cli/next-lint').nextLint),
  telemetry: () =>
    Promise.resolve(require('../cli/next-telemetry').nextTelemetry),
  info: () => Promise.resolve(require('../cli/next-info').nextInfo),
}

commands.ts 파일에서 command가 나열되어 있는 것을 확인할 수 있었다. 그 중에서 nextStart 확인해보자.

(4) next.js/packages/next/cli/next-start.ts

next-start.tsnextStart 라는 함수가 선언되어 있었다. 함수의 구성은 아래와 같다.

1. 옵션 유효성 검사

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
  }

arg의 경우 'next/dist/compiled/arg/index.js'에서 import 해오는데, next [command] [option]의 형태로 받은 입력 및 여러 설정들을 처리한 것으로 보인다. 받은 입력이 정의된 형태로 들어왔는지 검사하는 부분이다.

2. help 옵션에 대한 처리

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

--help 또는 -h 옵션이 있는 경우, next start에 대한 설명을 로그 찍고 실행을 종료한다.

3. 변수 선언

 const dir = getProjectDir(args._[0])
  const host = args['--hostname'] || '0.0.0.0'
  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
    )
  }

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

dir, host, port 등 서버 실행에 필요한 변수들 선언하고 있다.
keepAliveTimeout은 프로세스 유지 시간으로, 값이 유효하지 않을 경우 에러메세지 프린트 후 종료한다.

4. startServer 실행

startServer({
    dir,
    hostname: host,
    port,
    keepAliveTimeout,
  })
    .then(async (app) => {
      const appUrl = `http://${app.hostname}:${app.port}`
      Log.ready(`started server on ${host}:${app.port}, url: ${appUrl}`)
      await app.prepare()
    })
    .catch((err) => {
      console.error(err)
      process.exit(1)
    })

startServer 함수는 start-server.ts 에 정의되어 있다.

    const server = http.createServer((req, res) => {
        return requestHandler(req, res)
  	})

    server.on('listening', () => {
      const addr = server.address()
      const hostname =
        !opts.hostname || opts.hostname === '0.0.0.0'
          ? 'localhost'
          : opts.hostname

      const app = next({
        ...opts,
        hostname,
        customServer: false,
        httpServer: server,
        port: addr && typeof addr === 'object' ? addr.port : port,
      })

      requestHandler = app.getRequestHandler()
      upgradeHandler = app.getUpgradeHandler()
      resolve(app)
    })

    server.listen(port, opts.hostname)

createServer를 통해 server라는 웹 서버 객체를 생성했다. next-start.ts 에서 startServer를 호출하며 넘겨준 인자들로 세팅한 server를 실행한다.

0개의 댓글