공부하자

원티드 프리온보딩 프론트엔드 7월 챌린지 사전과제 3번 본문

Next.js

원티드 프리온보딩 프론트엔드 7월 챌린지 사전과제 3번

dev_riley 2023. 7. 6. 01:23

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

Next.js 세팅 가이드 👉 https://nextjs.org/docs/getting-started

 

Docs | Next.js

Using App Router Features available in /app

nextjs.org

Next.js Github 레포지토리 👉 https://github.com/vercel/next.js/ 

 

GitHub - vercel/next.js: The React Framework

The React Framework. Contribute to vercel/next.js development by creating an account on GitHub.

github.com

 


🌟 시작 

일단, create-next-app으로 Next.js를 설치한 후, yarn start 스크립트를 실행하면 packages.json에서 정의된것처럼 next start가 실행된다.

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

그렇담 next start는 어떤 파일을 불러오는걸까..!

궁금해서 좀 더 찾아보니 packages/next/src/lib/command.ts라는 파일에서 그 답을 알 수 있었다.(이 파일은 next.js의 CLI 명령어를 정의하는 파일이다.)

좀 더 위 코드를 자세히 살펴보면, CliCommand 타입과 commans라는 객체를 정의하고  있다.

 

CliCommand 타입 정의

CliCommand는 함수 타입으로 ‘argv’라는 문자열 배열 타입의 매개변수를 받고 아무 값도 반환하지 않는다. 이 타입은 CLI 명령어들의 실행 함수의 형식을 나타낸다.

 

commans 객체 정의

‘command’객체는 CLI에서 사용할 수 있는 명령어와 해당 명령어를 실행하는 함수를 매핑한 객체이다. 위 코드에서 start부분만 보면 해당 코드는 ‘next-start’파일을 비동기로 불러오고, ‘nextStart’함수를 반환하는데, 이 함수가 실제로 ‘yarn start’ 명령어를 실행할 때의 동작을 수행하는 걸 알 수 있다.

‘nextStart’함수는 ‘CliCommand’타입의 함수로 ‘argv’매개변수를 받지 않고 어떤 값도 반환하지 않는다. 실제로는 내부적으로 Next.js 개발 서버를 시작하면서 프로젝트를 실행하게 된다.

 

여기까지 이해했으니, 비동기로 불러올 next-start 함수를 한 번 살펴보자.

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

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

  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함수를 실행하기위해 같이 전달될 옵션들을 정의하거나 유효성검사를 해준 것 같다.

  • ‘nextStart’함수는 ‘CliCommand’타입의 비동기 함수이고 인수로 ‘argv’를 받아온다.
  • [—help]옵션이 전달된 경우 도움말 메세지를 출력하고 프로세스를 종료한다.
  • 인수 ‘argv’로 전달된 호스트와 포트 값을 가져와 각각을 host와 port 변수에 저장해서 사용한다.
  • ‘keepAliveTimeout’이 무엇을 하는 함수인지 자세히 알수는 없지만 유효성 검사까지 하는 것 같다.

그리고 startServer함수를 사용해서 전달된 옵션들과 함께 개발 서버를 설정하고 실행하는 것이다.

 

이제는 startServer 함수에 대해서 알아볼 차례이다. 코드의 양이 360줄 정도 되어서 다 해석할 수는 없지만 부분적으로 해석해보았다. (사실 해석이 좀 어려워서 chatGPT의 도움을 받았다..ㅎㅎ chatGPT 짱)

startServer함수는 개발 서버를 시작하는 메인 함수이고, 인자로 위의 설정 객체를 받아온다.

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

  if (keepAliveTimeout) {
    server.keepAliveTimeout = keepAliveTimeout
  }
  server.on('upgrade', async (req, socket, head) => {
    try {
      sockets.add(socket)
      socket.on('close', () => sockets.delete(socket))
      await upgradeHandler(req, socket, head)
    } catch (err) {
      socket.destroy()
      Log.error(`Failed to handle request for ${req.url}`)
      console.error(err)
    }
  })
let portRetryCount = 0

  server.on('error', (err: NodeJS.ErrnoException) => {
    if (
      allowRetry &&
      port &&
      isDev &&
      err.code === 'EADDRINUSE' &&
      portRetryCount < 10
    ) {
      Log.warn(`Port ${port} is in use, trying ${port + 1} instead.`)
      port += 1
      portRetryCount += 1
      server.listen(port, hostname)
    } else {
      Log.error(`Failed to start server`)
      console.error(err)
      process.exit(1)
    }
  })

  let targetHost = hostname

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

      if (isNodeDebugging) {
        const debugPort = getDebugPort()
        Log.info(
          `the --inspect${
            isNodeDebugging === 'brk' ? '-brk' : ''
          } option was detected, the Next.js proxy server should be inspected at port ${debugPort}.`
        )
      }

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

http.createServer를 호출하여 서버를 생성한 뒤, server.on()메서드를 통해 클라이언트의 요청을 upgrade할 때, error 상태일 때, 그리고 listening상태일 때에 실행할 콜백 함수를 정의해준 것 같다.

서버가 시작되면 server.on('listening', () => {...}) 에 정의된 콜백함수가 호출된다.

 

Log.ready(...)를 사용해 서버가 성공적으로 시작되었음을 알리기 위해 적혀진 메세지를 로그에 출력하고, resolve()함수를 호출함으로써 Promise를 완료 상태로 처리하고 서버 시작을 완료한다.

 

여기까지가 yarn start 스크립트를 입력했을 때부터 서버가 시작될때까지의 과정이다.

 


💡느낀 점

사실 이 사전과제를 받았을 때 굉장히 막막했는데, 차근차근 chatGTP와 함께 하다보니 생각보다 서버 시작 과정을 잘 따라갔던 것 같다. 또 이렇게 큰 오픈소스 분석은 처음해보는데 꾸준히 보다보면 확실히 도움이 될 거 같다는 생각과 함께 방대한 코드를 분석할때 접근하는 방법을 알게된 것 같아 이 사전과제 쉽지 않았지만 하길 잘했다는 생각이 들었다 😊

Comments