본문 바로가기
FrontEnd/Next.js

[Next.js] Next.js 초기세팅에서 yarn start 하면 생기는 일

by Chaedie 2022. 10. 6.
728x90

원티드 챌린지 10월 (SSR, CSR, Next.js)에서 내준 과제입니다.
해당 과제를 이해조차 못했는데, 수강하시는 분들 중 좋은 블로그 글이 있어 참고하였습니다.
아니 그냥 거의 따라 해봤습니다. 😭
개발세계엔 대단하신 분들이 많아요~! 존경 존경
과제를 잘 해주셨다고 칭찬받으신 포스팅

Next.js 초기세팅에서 Yarn Start 하면 생기는 일

import '../styles/globals.css'

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

export default MyApp
  • yarn start를 하면 '_app.js' 파일이 먼저 실행되나 봅니다.
  • 해당 파일은 프롭스로 { 컴포넌트와 페이지 프롭스를 받습니다. }

솔찍히

  • 과제를 다시 하는 입장이고, 다른 분 블로그 보면서 하는건데도 뭔말인지 모르겠습니다.
  • index.jsx 파일이 없는건 뭘까요? 핳ㅎㅎ
  • 깃헙 레포에서 next-start.ts 파일을 봐도 전혀 무슨 말인지 모르겠습니다.
  • 저만 이렇게 어려운가요? ㅎㅎㅎㅎㅎ 멘붕~ 😭
#!/usr/bin/env node

import arg from 'next/dist/compiled/arg/index.js'
import { startServer } from '../server/lib/start-server'
import { getPort, printAndExit } from '../server/lib/utils'
import * as Log from '../build/output/log'
import isError from '../lib/is-error'
import { getProjectDir } from '../lib/get-project-dir'
import { cliCommand } from '../lib/commands'

const nextStart: cliCommand = (argv) => {
  const validArgs: arg.Spec = {
    // Types
    '--help': Boolean,
    // Aliases
 }

  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
  }
    process.exit(0)
  }

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

  const keepAliveTimeoutArg: number | undefined = args['--keepAliveTimeout']


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

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

export { nextStart }

Server

  • packages/next/server/lib/start-server.ts
  • 솔찍히 잘 모르겠어서
{
...

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

    // 서버를 리스닝 하나보다.
    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)
  })
}

next가 나와서 next를 찾아 보셨단다.

  • 따라가보자. 공포영화 느낌 난다. 뭐가 나올지 모르고 그냥 따라가는 주인공이 된 느낌.
export class NextServer {

  constructor(options: NextServerOptions) {
    this.options = options
  }

  get hostname() {
    return this.options.hostname
  }

  get port() {
    return this.options.port
  }

  getRequestHandler(): RequestHandler {
    return async (
      req: IncomingMessage,
      res: ServerResponse,
      parsedUrl?: UrlWithParsedQuery
    ) => {
      const requestHandler = await this.getServerRequestHandler()
      return requestHandler(req, res, parsedUrl)
    }
  }

  getUpgradeHandler() {
    return async (req: IncomingMessage, socket: any, head: any) => {
      const server = await this.getServer()
      // @ts-expect-error we mark this as protected so it
      // causes an error here
      return server.handleUpgrade.apply(server, [req, socket, head])
    }
  }



  async prepare() {
    const server = await this.getServer()
    return server.prepare()
  }


  private async createServer(options: DevServerOptions): Promise<Server> {
    if (options.dev) {
      const DevServer = require('./dev/next-dev-server').default
      return new DevServer(options)
    }
    const ServerImplementation = await getServerImpl()
    return new ServerImplementation(options)
  }

  private async getServer() {
    if (!this.serverPromise) {
      setTimeout(getServerImpl, 10)
      this.serverPromise = this.loadConfig().then(async (conf) => {
        this.server = await this.createServer({
          ...this.options,
          conf,
        })
        if (this.preparedAssetPrefix) {
          this.server.setAssetPrefix(this.preparedAssetPrefix)
        }
        return this.server
      })
    }
    return this.serverPromise
  }

  private async getServerRequestHandler() {
    // Memoize request handler creation
    if (!this.reqHandlerPromise) {
      this.reqHandlerPromise = this.getServer().then((server) =>
        server.getRequestHandler().bind(server)
      )
    }
    return this.reqHandlerPromise
  }
}

// This file is used for when users run `require('next')`
function createServer(options: NextServerOptions): NextServer {
  if (options == null) {
    throw new Error(
      'The server has not been instantiated properly. https://nextjs.org/docs/messages/invalid-server-options'
    )
  }

  if (
    !('isNextDevCommand' in options) &&
    process.env.NODE_ENV &&
    !['production', 'development', 'test'].includes(process.env.NODE_ENV)
  ) {
    log.warn(NON_STANDARD_NODE_ENV)
  }

  if (options.dev && typeof options.dev !== 'boolean') {
    console.warn(
      "Warning: 'dev' is not a boolean which could introduce unexpected behavior. https://nextjs.org/docs/messages/invalid-server-options"
    )
  }

  if (shouldUseReactRoot) {
    ;(process.env as any).__NEXT_REACT_ROOT = 'true'
  }

  return new NextServer(options)
}

// Support commonjs `require('next')`
module.exports = createServer
exports = module.exports

// Support `import next from 'next'`
export default createServer

여기서부터 아득해지셨다고 한다. 난 시작부터 아득했다.

  • 그래도 따라가보자...
  • 안해보는것보단 나으니까 시키셨겠지...

  protected async renderHTML(
    req: NodeNextRequest,
    res: NodeNextResponse,
    pathname: string,
    query: NextParsedUrlQuery,
    renderOpts: RenderOpts
  ): Promise<RenderResult | null> {
    // Due to the way we pass data by mutating `renderOpts`, we can't extend the
    // object here but only updating its `serverComponentManifest` field.
    // https://github.com/vercel/next.js/blob/df7cbd904c3bd85f399d1ce90680c0ecf92d2752/packages/next/server/render.tsx#L947-L952
    renderOpts.serverComponentManifest = this.serverComponentManifest
    renderOpts.serverCSSManifest = this.serverCSSManifest
    renderOpts.fontLoaderManifest = this.fontLoaderManifest

    return renderToHTML(
      req.originalRequest,
      res.originalResponse,
      pathname,
      query,
      renderOpts
    )
  }

renderToHTML()

  • 아 이제 좀 알게 된게 있다.
  • 일단 뭔가 나오면 해당 함수로 타고 들어 가면 된다.
  • 물꼬만 트이면 그냥 의심되는 함수의 위치로 엄청 찾아 들어가면 된다.
  • 그리고 if문이 엄청나게 많은데, 에러 처리 같은것들이 엄청 많은거라서 살짝 무시해주면 핵심 로직만 따라가는것이 훨씬 쉬워진다.
  • 물론 진짜 쉬워지는건 아니지만, 그냥 훨씬 마음이 편해진달까...

  // DOM 을 찾았습니다. 
  // 저는 이거 찾다 못찾아서 그냥 검색했어요. 
  // 허허허허허허허허탈하네요 ㅎㅎ 
  // 이것도 다 경험이겠죠 ㅎㅎㅎㅎㅎㅎ
  const Body = ({ children }: { children: JSX.Element }) => {
    return inAmpMode ? children : <div id="__next">{children}</div>
  }

  const renderDocument = async () => {
    // Document를 Render해주는 함수 발견..... 😭

    async function loadDocumentInitialProps(
      renderShell?: (
        _App: AppType,
        _Component: NextComponentType
      ) => Promise<ReactReadableStream>
    ) {
      const renderPage: RenderPage = async (
        options: ComponentsEnhancer = {}
      ): Promise<RenderPageResult> => {

        // 그러고 보면 if 문으로 분기해서 return 하는 애들은 다 생략하시네요.
        // 예외처리 하는애들은 중요한 핵심 로직이 아니니 일단 제낀다? 
        // 이말인가요?

        const html = await renderToString(
          <Body>
            <AppContainerWithIsomorphicFiberStructure>
              {renderPageTree(EnhancedApp, EnhancedComponent, {
                ...props,
                router,
              })}
            </AppContainerWithIsomorphicFiberStructure>
          </Body>
        )
        return { html, head }
      }
      const documentCtx = { ...ctx, renderPage }
      const docProps: DocumentInitialProps = await loadGetInitialProps(
        Document,
        documentCtx
      )

      return { docProps, documentCtx }
    }

    const renderContent = (_App: AppType, _Component: NextComponentType) => {
      const EnhancedApp = _App || App
      const EnhancedComponent = _Component || Component

      return ctx.err && ErrorDebug ? (
        <Body>
          <ErrorDebug error={ctx.err} />
        </Body>
      ) : (
        <Body>
          <AppContainerWithIsomorphicFiberStructure>
            {renderPageTree(EnhancedApp, EnhancedComponent, {
              ...props,
              router,
            })}
          </AppContainerWithIsomorphicFiberStructure>
        </Body>
      )
    }

  const documentResult = await renderDocument()

  const document = (
    <AmpStateContext.Provider value={ampState}>
      <HtmlContext.Provider value={htmlProps}>
        {documentResult.documentElement(htmlProps)}
      </HtmlContext.Provider>
    </AmpStateContext.Provider>
  )

  const documentHTML = await renderToStaticMarkup(document)

  const [renderTargetPrefix, renderTargetSuffix] = documentHTML.split(
    '<next-js-internal-body-render-target></next-js-internal-body-render-target>'
  )

이제 클라이언트를 보신답니다...

  • 폴더 구조도 잘 봐야하는구나...
  • 폴더 구조를 먼저 살펴 보면서 어떤 내용이 있는지 큰 그림을 그리는건가??
  • 아래 코드에서 이니셜라이즈 한 뒤에 하이드레잇 한다고 나와있다.
  • 그래서 하이드레잇을 찾아 간다.
  • import {} from './'이라 어떤 파일을 찾아야 되는지 이해가 안됐는데 index.tsx파일의 경로라고 한다. 신기하다. 처음 알았다.
import { initialize, hydrate, version, router, emitter } from './'

window.next = {
  version,
  // router is initialized later so it has to be live-binded
  get router() {
    return router
  },
  emitter,
}

initialize({})
  .then(() => hydrate())
  .catch(console.error)

client/index.tsx

  • 막바지에 다왔다.
  • 그냥 막무가내로 따라 가고 있지만, 그냥 또 한가지 배운건 있다.
  • 일단 다 접고 함수 이름부터 보는게 은근히 도움이 될지도 모른다는 점?
export async function initialize(opts: { webpackHMR?: any } = {}): Promise<{...}> {} 

function renderApp(App: AppComponent, appProps: AppProps){...}

function renderReactElement(){}

function doRender(input: RenderRouteInfo): Promise<any> {...}

async function render(renderingProps: RenderRouteInfo): Promise<void> {...}

export async function hydrate(opts?: { beforeRender?: () => Promise<void> }) {...}

...
...
...
...
...
...

function doRender(input: RenderRouteInfo): Promise<any> {
  let { App, Component, props, err }: RenderRouteInfo = input

  const appProps: AppProps = {
    ...props,
    Component,
    err,
    router,
  }

  // ...뭔가 <style> 을 붙이고 있는 것 같음...

  const elem: JSX.Element = (
    <>
      <Head callback={onHeadCommit} />
      <AppContainer>
        {renderApp(App, appProps)}
        <Portal type="next-route-announcer">
          <RouteAnnouncer />
        </Portal>
      </AppContainer>
    </>
  )

  // We catch runtime errors using componentDidCatch which will trigger renderError
  renderReactElement(appElement!, callback => (
    <Root callbacks={[callback, onRootCommit]}>
      {process.env.__NEXT_STRICT_MODE ? (
        <React.StrictMode>{elem}</React.StrictMode>
      ) : (
        elem
      )}
    </Root>
  ))

  return renderPromise
}

마치며...

  • 그러라고 주신 과제는 아니겠지만... 나의 부족함을 많이 느낀 과제였다.
  • 부트캠프 출신 님들아!! 라는 이야기가 있는 것처럼 나 또한 부트캠프를 아직 졸업조차 못한 사람이라 ㅎㅎ 정말 한없이 부족함을 많이 느꼈다.
  • 화이팅 해야지... 😭

다시 한번 언급 하지만 과제는 "지니코딩" 님의 과제를 참고하며 따라 가본 것입니다. 지니코딩 님 챌린지 과제 링크

댓글