๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Programming/12. Wanted PreOnboarding Challenge

์›ํ‹ฐ๋“œ ํ”„๋ก ํŠธ์—”๋“œ ํ”„๋ฆฌ์˜จ๋ณด๋”ฉ ์ฑŒ๋ฆฐ์ง€ 10์›” ์‚ฌ์ „๊ณผ์ œ with Next.js

by @sangseophwang 2022. 9. 28.

 

๐Ÿ’ก CSR(Client-Side Rendering)์ด๋ž€ ๋ฌด์—‡์ด๋ฉฐ, ๊ทธ๊ฒƒ์˜ ์žฅ๋‹จ์ ์— ๋Œ€ํ•˜์—ฌ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”.

 CSR( ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง )์€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฌ์šฉํ•ด ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ง์ ‘ ํŽ˜์ด์ง€๋ฅผ ๋ Œ๋”๋งํ•˜๋Š” ๊ฒƒ์„ ๋œปํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ๋กœ์ง, ๋ฐ์ดํ„ฐ ํŽ˜์นญ, ๋ผ์šฐํŒ…์€ ์„œ๋ฒ„๊ฐ€ ์•„๋‹Œ ํด๋ผ์ด์–ธํŠธ์—์„œ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค. 

 ์žฅ์ ์œผ๋กœ๋Š” ์ดˆ๊ธฐ ๋กœ๋”ฉ ์ดํ›„์—๋Š” ๋น ๋ฅธ ์†๋„๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ์ดˆ๊ธฐ ๋กœ๋“œ๊ฐ€ ๋‹ฌ์„ฑ๋˜๊ณ  ๋ธŒ๋ผ์šฐ์ €์—์„œ ์บ์‹œ ๋ฐ ๊ตฌ๋ฌธ ๋ถ„์„ํ•  ์ˆ˜ ์žˆ๋Š” ํ•ญ๋ชฉ์ด ์™„๋ฃŒ๋˜๋ฉด ์ดํ›„ ํŽ˜์ด์ง€ ๋กœ๋“œ๋Š” ๋งค๋ฒˆ ์„œ๋ฒ„์—์„œ ๋ชจ๋“  ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์™€์•ผ ํ•˜๋Š” SSR๋ณด๋‹ค ๋น ๋ฅด๊ณ  ์›ํ™œํ•ฉ๋‹ˆ๋‹ค.

๋‘๋ฒˆ์งธ๋กœ๋Š” SSR์— ๋น„ํ•ด ์„œ๋ฒ„์— ๋ถ€๋‹ด์ด ์ ์Šต๋‹ˆ๋‹ค. ์ดˆ๊ธฐ ๋ Œ๋”๋ง ์ดํ›„ ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ ๊ณ ์น˜๊ฑฐ๋‚˜ ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค๋ฅธ ํŽ˜์ด์ง€๋กœ ํด๋ฆญํ–ˆ์„ ๋•Œ ๋ธŒ๋ผ์šฐ์ €๋Š” ์ „์ฒด ํŽ˜์ด์ง€๊ฐ€ ์•„๋‹ˆ๋ผ ๋™์ ์œผ๋กœ ํ•„์š”ํ•œ ์š”์†Œ๋งŒ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋ผ์šฐํŒ… ์‹œ ๋งค๋ฒˆ ์ƒˆ๋กœ์šด ํŽ˜์ด์ง€๋ฅผ ๋ Œ๋”๋งํ•ด ๋ฐ˜ํ™˜ํ•ด์•ผํ•˜๋Š” SSR์— ๋น„ํ•ด ์„œ๋ฒ„์— ๊ฐ€ํ•ด์ง€๋Š” ๋ถ€๋‹ด์„ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹จ์ ์œผ๋กœ๋Š” ์ฒซ์งธ๋กœ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋‹ค์šด๋กœ๋“œํ•˜๊ณ  ๋ถ„์„ํ•˜๋Š” ๊ณผ์ •์—์„œ ์ดˆ๊ธฐ ํŽ˜์ด์ง€ ๋กœ๋”ฉ์— ์‹œ๊ฐ„์ด ๋‹ค์†Œ ๊ฑธ๋ฆด ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค. ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋งํ•œ ํŽ˜์ด์ง€๋ฅผ ์ „๋‹ฌํ•ด์ฃผ๋Š” SSR๊ณผ ๋‹ฌ๋ฆฌ ๋นˆ HTML์—์„œ ๋ชจ๋“  ๋กœ์ง์ด ๋‹ด๊ฒจ์žˆ๋Š” JS๋ฅผ ๋ฐ›์•„ ๊ตฌ๋ฌธ์„ ๋ถ„์„ํ•˜๋ฉฐ ๋ Œ๋”๋ง์„ ์ง„ํ–‰ํ•˜๊ธฐ์— ์ฒซ ์ง„์ž… ์‹œ ๋กœ๋”ฉ ์†๋„(FP)๊ฐ€ ๊ธธ๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. 

๋‘๋ฒˆ์งธ ๋‹จ์ ์€ SEO์— ๋‹ค์†Œ ์ทจ์•ฝํ•˜๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค. ํ•˜๋‚˜์˜ html, ๊ทธ๊ฒƒ๋„ ๋นˆ html์„ ๊ฐ€์ ธ์™€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๊ตฌ๋ฌธ ๋ถ„์„์„ ํ†ตํ•ด ํŽ˜์ด์ง€๊ฐ€ ๋ Œ๋”๋ง๋˜๋Š” CSR์€ SEO๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ํฌ๋กค๋Ÿฌ ์ž…์žฅ์—์„œ ํ•ด๋‹น ์„œ๋น„์Šค์˜ ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€์—์„œ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ ๋ถˆ๋ฆฌํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋Š” ๊ฒ€์ƒ‰ ์—”์ง„ ์ตœ์ ํ™”์—๋„ ์ข‹์ง€ ์•Š์€ ์˜ํ–ฅ์„ ๋ฏธ์น  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฌผ๋ก  ์ตœ๊ทผ ๊ตฌ๊ธ€ ๋ด‡ ๋“ฑ ํฌ๋กค๋Ÿฌ๋“ค์€ CSR์—์„œ๋„ SEO๋ฅผ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํ•˜์ง€๋งŒ, ๊ทธ๋ž˜๋„ ์„ ํ˜ธ๋„ ์ธก๋ฉด์—์„œ๋Š” MPA( Multi Page Application )๊ฐ€ ๋” ์šฐ์„ธํ•˜๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

 

๐Ÿ’ก SPA(Single Page Application)๋กœ ๊ตฌ์„ฑ๋œ ์›น ์•ฑ์—์„œ SSR(Server-side Rendering)์ด ํ•„์š”ํ•œ ์ด์œ ์— ๋Œ€ํ•˜์—ฌ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”.

 ์ฒซ์งธ๋กœ CSR์— ๋น„ํ•ด ๋น ๋ฅธ FP(First Paint)์™€ FCP(First Contentful Paint)๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, ์ดˆ๊ธฐ ํŽ˜์ด์ง€ ๋กœ๋“œ๊ฐ€ ๋” ๋น ๋ฅด๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. SSR์€ ์„œ๋ฒ„์—์„œ ํŽ˜์ด์ง€๋ฅผ ๋ Œ๋”๋ง(pre-rendering)ํ•œ ํ›„ ์œ ์ €์—๊ฒŒ ๋จผ์ € ์ธํ„ฐ๋ž™์…˜์ด ๋ถˆ๊ฐ€๋Šฅํ•œ ํŽ˜์ด์ง€๋ฅผ ์ „๋‹ฌํ•ด์ฃผ๊ณ (TTV) ์ดํ›„ ๋ธŒ๋ผ์šฐ์ €์—์„œ JS ๊ตฌ๋ฌธ์„ ์ฝ์œผ๋ฉฐ ์ˆ˜ํ™”(hydration) ๊ณผ์ •์„ ๊ฑฐ์ณ ์ธํ„ฐ๋ž™์…˜์ด ๊ฐ€๋Šฅํ•ด์ง€๋Š”(TTI) ์ˆœ์œผ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์ด ๊ณผ์ •์„ ํ†ตํ•ด ์œ ์ €๋Š” CSR์— ๋น„ํ•ด ๋น ๋ฅด๊ฒŒ ์ดˆ๊ธฐ ํ™”๋ฉด์„ ๋ณด๊ฒŒ ๋˜๊ณ , ์ด๋Š” '์„œ๋น„์Šค๊ฐ€ ๋น ๋ฅด๋‹ค'๋Š” ์ธ์‹์„ ์ค„ ์ˆ˜ ์žˆ๊ธฐ์— ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ์ข‹์•„์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 ๋‘˜์งธ๋กœ ๋” ๋‚˜์€ SEO๋ฅผ ์œ„ํ•ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. SSR์„ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋ฉด ํฌ๋กค๋Ÿฌ๊ฐ€ ์‰ฝ๊ฒŒ ํ•ด์„ํ•  ์ˆ˜ ์žˆ๋Š” ํ˜•ํƒœ๋กœ์จ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ํฌ๋กค๋Ÿฌ๋Š” ํ•ด๋‹น ์›น ์‚ฌ์ดํŠธ์— ์žˆ๋Š” ๋งํฌ๋“ค์„ ํƒ€๊ณ  ๋‹ค๋‹ˆ๋ฉฐ ์ƒˆ๋กœ์šด ํŽ˜์ด์ง€๋ฅผ ์ฐพ๋Š” ์ผ์„ ๊ณ„์† ์ˆ˜ํ–‰ํ•˜๊ณ , ์ด๋ ‡๊ฒŒ ๋“ค์–ด๊ฐ„ ์›นํŽ˜์ด์ง€์˜ ์ •๋ณด๋“ค์„ ๊ตฌ๊ธ€ ๊ฒ€์ƒ‰ ์ƒ‰์ธ์— ์ €์žฅํ•˜๋Š” ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ SPA์—์„œ CSR๋กœ ์ œ์ž‘๋œ ์„œ๋น„์Šค์—์„œ๋Š” ๊ทธ ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์–ด๋ ค์šด๋ฐ, ๊ทธ ์˜ˆ๋กœ React ํŒŒ์ผ ๊ตฌ์กฐ๋ฅผ ๋ณด๋ฉด 1๊ฐœ์˜ html ํŒŒ์ผ ์•ˆ์— ๋นˆ <div>๋งŒ ์กด์žฌํ•˜๋Š” ๊ฒƒ์„ ๋“ค ์ˆ˜ ์žˆ๋‹ค. ๋นˆ div ํƒœ๊ทธ ์•ˆ์—๋Š” ๋ Œ๋”๋ง ์ดํ›„ ๋‚ด์šฉ๋“ค์ด ์ฑ„์›Œ์ง€๊ฒŒ ๋˜์ง€๋งŒ ๊ทธ ์ „๊นŒ์ง€๋Š” ๋ง ๊ทธ๋Œ€๋กœ '๋น„์–ด ์žˆ๋Š”' ์ƒํƒœ์ด๊ธฐ์— ํฌ๋กค๋Ÿฌ๊ฐ€ ์›ํ•˜๋Š” ์ •๋ณด๋ฅผ ์ฐพ๊ธฐ ์–ด๋ ต๊ณ  ์ด๊ฒƒ์ด ๊ฒ€์ƒ‰ ์—”์ง„ ์ตœ์ ํ™”์— ์˜ํ–ฅ์„ ์ฃผ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด์— ๋ฏธ๋ฆฌ ๊ทธ๋ฆฐ ํŽ˜์ด์ง€๋ฅผ ์ „๋‹ฌํ•˜๋Š” SSR์€ ํฌ๋กค๋Ÿฌ๊ฐ€ ์ •๋ณด๋ฅผ ์ฐพ๊ณ  ์ƒ‰์ธํ•˜๊ธฐ ์ ํ•ฉํ•˜๊ธฐ์— ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

 

๐Ÿ’ก Next.js ํ”„๋กœ์ ํŠธ๋ฅผ ์„ธํŒ…ํ•œ ๋’ค yarn start ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ–ˆ์„ ๋•Œ ์‹คํ–‰๋˜๋Š” ์ฝ”๋“œ๋ฅผ next.js github ๋ ˆํฌ์ง€ํ† ๋ฆฌ์—์„œ ์ฐพ์€ ๋’ค, ํ•ด๋‹น ํŒŒ์ผ์— ๋Œ€ํ•œ ๊ฐ„๋‹จํ•œ ์„ค๋ช…์„ ์ฒจ๋ถ€ํ•ด์ฃผ์„ธ์š”.

 

์šฐ์„  next.js github ๋ ˆํฌ์ง€ํ† ๋ฆฌ์—์„œ ์ฐพ์€ ์‹คํ–‰๋˜๋Š” ์ฝ”๋“œ์˜ ๊ฒฝ๋กœ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

next.js/packages/next/cli/next-start.ts

์šฐ์„  ์ž˜๋ชป๋œ ํ‚ค์›Œ๋“œ๋กœ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ–ˆ์„ ๋•Œ์— ๋Œ€ํ•œ ์•ˆ๋‚ด ํ˜น์€ build๋ฅผ ์ง„ํ–‰ํ•˜์ง€ ์•Š๊ณ  start๋ฅผ ์ง„ํ–‰ํ–ˆ์„ ๋•Œ์— ๋Œ€ํ•œ ์•ˆ๋‚ด์— ๋Œ€ํ•œ ๋‚ด์šฉ์ด ์ดˆ๋ฐ˜์— ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. --help, --port ๋“ฑ์˜ cliCommand๋‚˜ --h์™€ ๊ฐ™์€ alias์— ๋Œ€ํ•œ ์•ˆ๋‚ด๋„ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. 

์ดํ›„ ๋ฌธ์ œ ์—†์ด ์ง„ํ–‰๋˜๋ฉด 0.0.0.0 ๋˜๋Š” --hostname์œผ๋กœ ์ง€์ •ํ•œ ๊ฐ’๊ณผ ํ•จ๊ป˜ `:ํฌํŠธ ๋„˜๋ฒ„` ๊ฐ€ ๋ถ™์€ ๋งํฌ๋ฅผ ํ„ฐ๋ฏธ๋„๋กœ ๋ณด์—ฌ์ฃผ๋ฉฐ ์•ฑ์ด ์ค€๋น„๋˜์—ˆ์Œ์„ ์•Œ๋ ค์ค๋‹ˆ๋‹ค. ์•ฑ์€ `_app.js` (ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋ผ๋ฉด _app.tsx), `_document.js` ํŒŒ์ผ์ด ์ฐจ๋ก€๋กœ ์‹คํ–‰๋˜๋ฉฐ ์ œ์ž‘ํ•œ ํŽ˜์ด์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

๋งˆ์ง€๋ง‰์œผ๋กœ ๋ ˆํฌ์ง€ํ† ๋ฆฌ์—์„œ ์ฐพ์€ ์ฝ”๋“œ๋ฅผ ์ฒจ๋ถ€ํ•ฉ๋‹ˆ๋‹ค.

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,
    '--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'] || '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

  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 }

 

๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€