Next.js 프로젝트에서 yarn start 스크립트를 실행하면 무슨 일이 벌어질까?

수툴리 양·2022년 9월 28일
2
  1. CSR(Client-side Rendering)이란 무엇이며, 그것의 장단점에 대하여 설명해주세요.
  2. SPA(Single Page Application)로 구성된 웹 앱에서 SSR(Server-side Rendering)이 필요한 이유에 대하여 설명해주세요.
  3. Next.js 프로젝트를 세팅한 뒤 yarn start 스크립트를 실행했을 때 실행되는 코드를 nextjs github 레포지토리에서 찾은 뒤, 해당 파일에 대한 간단한 설명을 첨부해주세요.
    https://nextjs.org/docs/getting-started (Next.js 세팅 가이드)
    https://github.com/vercel/next.js/ (Next.js Github 레포지토리)

CSR이란

Client Side Rendering, 웹 페이지의 렌더링이 클라이언트 측에서 일어나는 렌더링 방식이다.

내 컴퓨터의 브라우저는 서버에서 html을 받아오고, 자바스크립트 파일을 다운받아 필요한 데이터를 동적으로 (api 호출 등을 통해) 화면을 그려준다.

우리가 웹 페이지를 방문했을 때 첫 화면을 그려주는 일을 클라이언트에 맡기게 되므로 서버가 갖는 부담이 적어진다.
단점으로는, 웹 크롤러는 html을 읽으므로, 데이터가 없는 뼈대 상태의 html에서는 가져올 데이터가 없어 SEO 검색엔진 최적화에는 불리하다.

추가로 보안 관련해서 쿠키에 사용자 정보를 저장한다는 점도 기억할 부분이다.

*우리가 구글 사이트에 검색어를 입력했을 때 나오는 결과물들은, 대개 웹 크롤러가 html을 읽어 검색 가능한 색인을 만들어낸 것이다. 데이터가 없는 html은 색인을 만들 만한 콘텐츠(데이터)가 없을 수 있다.

SPA로 구성된 웹앱에서 SSR이 필요한 이유?

Server Side Rendering 방식은, 클라이언트(브라우저) 측에서는 유저(사용자인 나)로부터 요청이 들어오면
(즉 내가 크롬으로 어떤 웹사이트의 페이지에 접속하게 되면,)
서버로부터 이미 데이터를 포함하고 있는 html을 가져와 만들어져 있는 페이지를 화면에 바로 렌더링해 줄 수 있다.
화면 변경이 될 때마다 계속 서버에 html을 요청해야 하니, 서버에 부담이 있는 셈이다.
CSR 방식이 SEO에 불리한 반면, 사용자에게 보여줄 페이지를 서버에서 완전히 구성한 상태로 가져와 보여주기 때문에 유리하다.

Single Page Application는 말 그대로 하나의 "페이지"로 구성된 애플리케이션이다. SPA로 구성된 웹앱에서는 SSR이 어떤 부분에 필요한 것일까?

웹앱 프론트엔드 개발에 많이 사용되는 프레임워크인 리액트로 우리는 동적 웹앱을 만들 수 있다. 즉 내가 장바구니에서 구매할 상품을 선택할 때마다 페이지 전체가 갱신(렌더)되는 것이 아니라, 선택한 상품들의 총합 가격을 나타내는 UI 딱 그 부분만 업데이트시킬 수 있는 것이다. 리액트로 SPA를 개발하면 네이티브 애플리케이션과 같은 사용자 경험을 줄 수 있고, 결과적으로 앱과 웹이 동일한 서버를 이용하는 셈이 된다.

이렇게 부분부분을 동적으로 그려준다면 그만큼 클라이언트에서 다운 받아야하는 자바스크립트 코드의 양이 많은 셈이므로, 최초 로딩 시에 비교적 시간이 오래 걸릴 수 있다.

SSR 방식은 화면에 그려지는 초기 로딩 속도가 빨라 UX측면에서 좋을 수 있기 때문에 메인 홈페이지 등 첫 화면만 SSR로 적용해볼 수도 있다. (그러나 그려주기만 할 뿐, 자바스크립트 파일이 다운받아져야 클릭 등의 기능이 실제 동작할 것이다.)

Next.js 프로젝트에서 yarn start 스크립트를 실행했을 때 실행되는 코드

리액트로 웹앱 프로젝트를 시작하기는 보다 수월하다. node를 설치하고 노드 버전 관리 툴 nvm, 자바스크립트 패키지나 모듈, 외부 라이브러리 관리 툴인 npm만 인스톨하고 나면 npx create-react-app과 같은 간단한 cli로 웹앱 프로젝트를 시작할 수 있다. (구체적인 설정은 매뉴얼하게 세팅하더라도)

yarn도 npm 과 같이 패키지 매니저 툴이다.
우리가 node(javascript)로 프로젝트를 개발하면 package.json을 세팅해야 한다. (npx create-react-app 을 하면 자동으로 기본 구성이 세팅된 파일이 생성되는데,)

이 package.json 파일은 무엇인가? 파일 코드를 보면 역시 json 형식의 파일이다.
노드로 만든 프로젝트에 관련된 메타데이터를 지닌 파일이다.

개발자나 (또는 사용자가) 패키지 매니저인 npm을 통해 프로젝트에 필요한 라이브러리, 모듈 등을 설치하고 또 앱을 띄워 실행하는 등의 작업을 허용하기 위한 설명서 같은 것이다.
(일부 대략적으로 설명하자면 개발할 때 필요한 라이브러리, 이 프로젝트(웹앱)을 사용하기 위해 필요한 라이브러리 등이 devDependencies, dependencies 로 구분된다고 보면 된다)

노드 공식문서: https://nodejs.org/en/knowledge/getting-started/npm/what-is-the-file-package-json/

대개 단위별 프로젝트 최상위(root) 경로에 생성되는데, (MSA 형식인 경우 특정 디렉토리에서 BFF나 API서비스를 띄우는 구조를 가질 수도 있다.)

Next.js 공식 문서에서 next 프로젝트를 구성할 때 수동으로 package.json을 작성하는 예시를 보여주고 있다. >

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

헷갈릴 지점에서 질문의 핵심을 살펴보면, 우리가 Next.js로 만든 프로젝트에서 yarn start 명령어를 입력하면 "next start" 코드의 실행 내용, 자세한 내막이 과연 무엇인지 궁금한 것이다

nextjs 깃헙저장소: https://github.com/vercel/next.js/tree/canary/packages/next/cli

nextjs의 깃헙이다. 이해 편의를 위해 단순한 관점으로 바라보자면 이 nextjs는 우리의 작고 소중한 프로젝트와 다른, (리액트)프레임워크이다.

따라서 최상위 package.json가 우리의 프로젝트의 그것과 다를 수 있는데,

next로 만든 다양한 프로젝트의 package.json 을 책임지는 코드는 packages 에 정의되었지 않을까 예상해볼 수 있다.

#!/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 { cliCommand } from '../lib/commands'

const nextStart: cliCommand = (argv) => {
  const validArgs: arg.Spec = {
    // Types
    '--help': Boolean,
    '--port': Number,
    '--hostname': String,
    '--keepAliveTimeout': Number,
	// ..
  }
  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)
    }
    // ..
  }

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

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

--help 등 cli로 입력하는 옵션들에 대한 실행 내용이 정의되어 있다. 또 host, port 등을 받아 startServer를 실행하는 코드도 보인다.

프로젝트를 하면서 Socket 등 실시간으로 서버와 소통하는 서비스를 만들어 본 경험이 있다면 createServer 메소드를 본 적이 있을 것이다. 위 "startServer" 함수가 선언된 곳을 보면 http모듈의 createServer를 사용하여 정의되어 있다.

 let requestHandler: RequestHandler

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

나는 리액트로 프론트엔드 개발을 시작했고 지금도 계속 리액트로 개발하고 있는데, 보다 개발이 편리한 것은 맞지만 웹팩 등을 포함해 프로젝트 단위에서 매뉴얼하게 구축해보는 것이 필요하겠다고 생각이 드는 시점이었다.

우리의 콘솔(터미널)에 'started server on localhost:3000, url...' 과 같은 로그를 남겨주는 코드도 이 안에 있는 것이었다!

근본에 대한 탐구가 중요하다는 것을 이 포스트를 마치며 새기게 된다.

profile
developer; not kim but Young

0개의 댓글