가희의자기개발블로그

[Next.js] next start를 하면 어떤 일이 발생할까? 본문

프론트엔드

[Next.js] next start를 하면 어떤 일이 발생할까?

가희gahui 2022. 10. 5. 00:00
반응형

원티드에서 주최하는 프론트엔드 프리온보딩 코스에 참여했습니다.

해당 포스팅은 프리온보딩 코스의 사전과제 내용으로, Next.js를 초기 생성했을때 package.json에 명시되어있는 script "next start"를 하게 되면 어떤 일이 발생하는지 그리고 이를 실행하는 파일을 파악해 보고자 하는 글입니다. 


Next.js란 SPA(Single Page Aplication)에서 SSR이 가능하도록 해주는 React의 라이브러리입니다. (Vue에서는 nuxt.js가 있죠.)
SSR에 대해 간략히 한줄로 표현하자면, 서버에서 미리 HTML을 만들어 클라이언트에게 전달해주는 것을 SSR이라고 합니다.

yarn create next-app --typescript 을 입력하여 생성되는 next.js의 초기 파일 구성은 다음과 같습니다.  

 

1. _app.tsx 파일

pages 폴더의 index.tsx와 _app.tsx파일이 생성된 것을 볼 수 있습니다. 

__app.tsx 파일은 next.js에서 각각의 페이지가 초기화 될때 로딩되는 파일입니다. 해당 파일에서 앱의 전역으로 사용할 변수나 글로벌 CSS 그리고 공통 Layout 같은 것을 설정 할 수 있습니다.

import '../styles/globals.css'
import type { AppProps } from 'next/app'

function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />
}

export default MyApp

 

2.  command 명령어 설정 파일

이제 next.js의 레파지토리에서 next start 명령어의 동작 방식을 살펴보겠습니다. start 명령어를 입력했을 때 실행 되는 파일은 다음 경로에서 찾을 수 있습니다. next.js > packages > cli > next-start.ts  

그렇다면 우리가 next start 를 입력했을 때, next.js는 어떻게 알고 해당 파일을 찾아갈까요? 이것에 대한 정의는 next.js > packages > lib > commands.ts 파일에 정의 되어있는 것을 확인할 수 있습니다. 

저기 start 명령어를 입력했을 때 ../cli/next-start 파일에 대한 경로가 적혀있습네요!

"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.commands = void 0;
const commands = {
    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)
};
exports.commands = commands;

3. start 명령어의 options과 aliases 정의

next-start 파일에서 가장 먼저 시작 되는 코드는 바로 start 명령어의 옵션과 그 aliases에 대한 정의입니다.

  const validArgs: arg.Spec = {
    // Types
    '--help': Boolean,
    '--port': Number,
    '--hostname': String,
    '--keepAliveTimeout': Number,

    // Aliases
    '-h': '--help',
    '-p': '--port',
    '-H': '--hostname',
  }

  4. options의 유효성 검사와 에러처리

다음은 사용자가 입력한 명령어가 유효한 명령어인지 확인합니다. 만약 유효하지 않다면 발생된 에러가 적절한지 그 code가 'ARG_UNKNOWN_OPTION'인지 확인후 적절한 error를 발생하고 프로그램을 종료합니다.

  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
  }
// '../lib/is-error'
export default function isError(err: unknown): err is NextError {
  return (
    typeof err === 'object' && err !== null && 'name' in err && 'message' in err
  )
}
export function printAndExit(message: string, code = 1) {
  if (code === 0) {
    console.log(message)
  } else {
    console.error(message)
  }

  process.exit(code)
}

5.  help옵션에 대한 처리

다음 코드는 --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)
  }

6. 옵션값 설정 

사용자가 host나 port의 옵션값을 명시해 줬다면, 그 값으로 그렇지 않다면 next.js에서 기본적으로 정해놓은 값으로 설정됩니다. 

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

7. keepAliveTimeout 설정 및 유효성 검사

명령어 입력시 사용자가 옵션으로 설정해둔 keepAliveTimeout가 존재한다면 그 값을 선언하고, 유효하지 않은 값일 경우 에러를 발생시킵니다.

  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

8.  server 실행

모든 옵션값이 정상적인것으로 판단 되면 startServer 함수를 호출해 NextServer를 반환받습니다. 이 값으로 prepare 함수를 호출해 서버를 실행해 줍니다. 

  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함수의 경우 아래와 같이 구현되어있습니다. 파라미터값으로 서버의 options 를 받아 서버를 기본 설정을 하고 NextServer를 Promise로 리턴해줍니다. 

import type { NextServerOptions, NextServer, RequestHandler } from "../next"
import next from "../next"

export function startServer(opts: StartServerOptions) {
  let requestHandler: RequestHandler

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

  return new Promise<NextServer>((resolve, reject) => {
    let port = opts.port
    let retryCount = 0
    let upgradeHandler: any

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

      const app = next({
        // 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)
  })
}

 

반응형
Comments