1. CSR(Client-side Rendering)이란 무엇이며, 그것의 장단점에 대하여 설명해주세요.
CSR는 클라이언트가 모든 것을 처리하는 방식입니다. app.js를 서버로부터 다운받아 이것들을 기반으로 동적으로 HTML을 생성해 사용자에게 최종적인 애플리케이션을 보여줍니다. 아래 코드와 같이 서버에서는 body에 app과 script 태그를 보내주고 클라이언트에서 이것들을 동작시켜 완성된 페이지를 보여주는 방식입니다.
장점은 첫 번째 이후 페이지 렌더링이 빠르고 데이터 비용을 아낄 수 있는 여지가 높습니다. 하나의 동작이 이루어질 때마다 필요없는 부분까지 한번에 불러와야하는 SSR과 달리 CSR는 변화된 부분만을 로딩해올 수 있어 빠르고 효율적입니다.
단점은 사용자가 첫 화면을 보기까지 로딩 시간이 오래 걸리고 페이지 캐싱이 잘되지 않습니다. 또한 SEO가 좋지 않아 빠른 검색이 어렵습니다. 빈 html 파일이 오는 방식이기 때문에 검색 엔진 최적화나 페이지 캐싱에 어려움이 있습니다.
2. SPA(Single Page Application)로 구성된 웹 앱에서 SSR(Server-side Rendering)이 필요한 이유에 대하여 설명해주세요.
SPA는 하나의 페이지로 구성되어 있고 사용자가 한 페이지 내에 머무르면서 서버에서 필요한 데이터만 받아와 부분적으로 업데이트합니다. SPA에서 CSR을 사용할 경우 검색 엔진 최적화가 어렵다는 단점으로 인해 SSR이 필요합니다. SSR은 서버에서 필요한 데이터를 모두 가져와서 html 파일을 만들고 이 html 파일을 동적으로 제어할 수 있는 소스코드까지 클라이언트에 전달하는 방식입니다. 따라서 모든 컨텐츠가 html에 담겨있기 때문에 효율적인 SEO가 가능하기 때문에 SSR이 필요합니다.
3. Next.js 프로젝트에서 yarn start(or npm run start) 스크립트를 실행했을 때 실행되는 코드를 Next.js Github 레포지토리에서 찾은 뒤, 해당 파일에 대한 간단한 설명을 첨부해주세요.
npm run start 스크립트 실행 코드는 packages/next/src/cli/next-start.ts에서 실행됩니다.
next-start.ts 파일을 보면 서버 시작을 위해 import로 startServer 함수를 가져옵니다.
// next-start.ts
import arg from 'next/dist/compiled/arg/index.js' // 명령줄 인수를 구문 분석하기 위한 arg 모듈 가져오기
import { startServer } from '../server/lib/start-server' // 서버 시작을 위한 startServer 함수 가져오기
import { getPort, printAndExit } from '../server/lib/utils' // 포트 번호 및 유틸리티 함수 가져오기
import isError from '../lib/is-error' // 에러 객체 확인을 위한 isError 함수 가져오기
import { getProjectDir } from '../lib/get-project-dir' // 프로젝트 디렉토리 가져오기
import { CliCommand } from '../lib/commands' // CLI 명령어 객체를 정의하는 인터페이스 가져오기
import { resolve } from 'path' // 파일 경로 처리를 위한 path 모듈의 resolve 함수 가져오기
import { PHASE_PRODUCTION_SERVER } from '../shared/lib/constants' // 상수 값 가져오기
import loadConfig from '../server/config' // Next.js 설정 로드를 위한 loadConfig 함수 가져오기
const nextStart: CliCommand = async (argv) => { // nextStart 함수 정의, CliCommand 타입의 CLI 명령어 함수
const validArgs: arg.Spec = { // 유효한 인수 명세 객체 정의
// Types
'--help': Boolean, // --help 플래그는 불리언 타입
'--port': Number, // --port 옵션은 숫자 타입
'--hostname': String, // --hostname 옵션은 문자열 타입
'--keepAliveTimeout': Number, // --keepAliveTimeout 옵션은 숫자 타입
// Aliases
'-h': '--help', // -h 옵션은 --help 옵션의 별칭
'-p': '--port', // -p 옵션은 --port 옵션의 별칭
'-H': '--hostname', // -H 옵션은 --hostname 옵션의 별칭
}
let args: arg.Result<arg.Spec> // 인수 구문 분석 결과를 저장할 변수
try {
args = arg(validArgs, { argv }) // 명령줄 인수를 구문 분석하여 결과를 args에 저장
} catch (error) {
if (isError(error) && error.code === 'ARG_UNKNOWN_OPTION') { // 알 수 없는 옵션 에러인 경우
return printAndExit(error.message, 1) // 에러 메시지를 출력하고 프로그램을 종료
}
throw error // 그 외의 에러는 throw하여 처리
}
if (args['--help']) { // --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'] // keep-alive 타임아웃 옵션 값 가져오기
if (
typeof keepAliveTimeoutArg !== 'undefined' && // 값이 정의되었고
(Number.isNaN(keepAliveTimeoutArg) || // NaN이거나
!Number.isFinite(keepAliveTimeoutArg) || // 유한한 숫자가 아니거나
keepAliveTimeoutArg < 0) // 0보다 작은 경우
) {
printAndExit(
`Invalid --keepAliveTimeout, expected a non negative number but received "${keepAliveTimeoutArg}"`,
1
) // 잘못된 keep-alive 타임아웃 값이면 에러 메시지를 출력하고 프로그램을 종료
}
const keepAliveTimeout = keepAliveTimeoutArg
? Math.ceil(keepAliveTimeoutArg) // 값이 존재하면 올림하여 할당
: undefined // 값이 존재하지 않으면 undefined 할당
const config = await loadConfig(
PHASE_PRODUCTION_SERVER, // 프로덕션 서버 모드로 설정 로드
resolve(dir || '.'), // 디렉토리 경로를 절대 경로로 변환
undefined,
undefined,
true
)
await startServer({
dir, // 디렉토리
isDev: false, // 개발 모드 여부
hostname: host, // 호스트
port, // 포트
keepAliveTimeout, // keep-alive 타임아웃
useWorkers: !!config.experimental.appDir, // 워커 사용 여부
})
}
export { nextStart } // nextStart 함수를 내보냄
import된 startServer 함수가 있는 start-server.ts 파일의 코드입니다.
여기에서 startServer 함수를 정의하고 있고 gpt의 도움을 받아 이 코드를 요약해봤습니다.
- 서버와 관련된 변수와 핸들러를 초기화합니다.
- HTTP 서버를 생성하고 설정합니다.
- 서버를 시작하고 요청 및 업그레이드 핸들러를 등록합니다.
- 옵션에 따라 워커를 사용하여 라우터를 실행하거나 메인 프로세스에서 Next.js 애플리케이션을 실행합니다.
- 서버가 시작되었음을 로그로 출력합니다.
- 서버 종료시 워커를 종료하고 소켓을 닫습니다.
import type { Duplex } from 'stream'
import type { IncomingMessage, ServerResponse } from 'http'
import type { ChildProcess } from 'child_process'
import http from 'http'
import { isIPv6 } from 'net'
import * as Log from '../../build/output/log'
import { normalizeRepeatedSlashes } from '../../shared/lib/utils'
import { initialEnv } from '@next/env'
import {
genRouterWorkerExecArgv,
getDebugPort,
getNodeOptionsWithoutInspect,
} from './utils'
export interface StartServerOptions {
dir: string
prevDir?: string
port: number
isDev: boolean
hostname: string
useWorkers: boolean
allowRetry?: boolean
isTurbopack?: boolean
isExperimentalTurbo?: boolean
keepAliveTimeout?: number
onStdout?: (data: any) => void
onStderr?: (data: any) => void
}
type TeardownServer = () => Promise<void>
export async function startServer({
dir,
prevDir,
port,
isDev,
hostname,
useWorkers,
allowRetry,
keepAliveTimeout,
onStdout,
onStderr,
}: StartServerOptions): Promise<TeardownServer> {
const sockets = new Set<ServerResponse | Duplex>()
let worker: import('next/dist/compiled/jest-worker').Worker | undefined
let handlersReady = () => {}
let handlersError = () => {}
let isNodeDebugging: 'brk' | boolean = !!(
process.execArgv.some((localArg) => localArg.startsWith('--inspect')) ||
process.env.NODE_OPTIONS?.match?.(/--inspect(=\S+)?( |$)/)
)
if (
process.execArgv.some((localArg) => localArg.startsWith('--inspect-brk')) ||
process.env.NODE_OPTIONS?.match?.(/--inspect-brk(=\S+)?( |$)/)
) {
isNodeDebugging = 'brk'
}
let handlersPromise: Promise<void> | undefined = new Promise<void>(
(resolve, reject) => {
handlersReady = resolve
handlersError = reject
}
)
let requestHandler = async (
_req: IncomingMessage,
_res: ServerResponse
): Promise<void> => {
if (handlersPromise) {
await handlersPromise
return requestHandler(_req, _res)
}
throw new Error('Invariant request handler was not setup')
}
let upgradeHandler = async (
_req: IncomingMessage,
_socket: ServerResponse | Duplex,
_head: Buffer
): Promise<void> => {
if (handlersPromise) {
await handlersPromise
return upgradeHandler(_req, _socket, _head)
}
throw new Error('Invariant upgrade handler was not setup')
}
// setup server listener as fast as possible
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)
})
try {
if (useWorkers) {
const httpProxy =
require('next/dist/compiled/http-proxy') as typeof import('next/dist/compiled/http-proxy')
let renderServerPath = require.resolve('./render-server')
let jestWorkerPath = require.resolve('next/dist/compiled/jest-worker')
if (prevDir) {
jestWorkerPath = jestWorkerPath.replace(prevDir, dir)
renderServerPath = renderServerPath.replace(prevDir, dir)
}
const { Worker } =
require(jestWorkerPath) as typeof import('next/dist/compiled/jest-worker')
const routerWorker = new Worker(renderServerPath, {
numWorkers: 1,
// TODO: do we want to allow more than 10 OOM restarts?
maxRetries: 10,
forkOptions: {
execArgv: await genRouterWorkerExecArgv(
isNodeDebugging === undefined ? false : isNodeDebugging
),
env: {
FORCE_COLOR: '1',
...((initialEnv || process.env) as typeof process.env),
PORT: port + '',
NODE_OPTIONS: getNodeOptionsWithoutInspect(),
...(process.env.NEXT_CPU_PROF
? { __NEXT_PRIVATE_CPU_PROFILE: `CPU.router` }
: {}),
WATCHPACK_WATCHER_LIMIT: '20',
},
},
exposedMethods: ['initialize'],
}) as any as InstanceType<typeof Worker> & {
initialize: typeof import('./render-server').initialize
}
let didInitialize = false
for (const _worker of ((routerWorker as any)._workerPool?._workers ||
[]) as {
_child: ChildProcess
}[]) {
// eslint-disable-next-line no-loop-func
_worker._child.on('exit', (code, signal) => {
// catch failed initializing without retry
if ((code || signal) && !didInitialize) {
routerWorker?.end()
process.exit(1)
}
})
}
const workerStdout = routerWorker.getStdout()
const workerStderr = routerWorker.getStderr()
workerStdout.on('data', (data) => {
if (typeof onStdout === 'function') {
onStdout(data)
} else {
process.stdout.write(data)
}
})
workerStderr.on('data', (data) => {
if (typeof onStderr === 'function') {
onStderr(data)
} else {
process.stderr.write(data)
}
})
const { port: routerPort } = await routerWorker.initialize({
dir,
port,
hostname,
dev: !!isDev,
workerType: 'router',
isNodeDebugging: !!isNodeDebugging,
keepAliveTimeout,
})
didInitialize = true
const getProxyServer = (pathname: string) => {
const targetUrl = `http://${
targetHost === 'localhost' ? '127.0.0.1' : targetHost
}:${routerPort}${pathname}`
const proxyServer = httpProxy.createProxy({
target: targetUrl,
changeOrigin: false,
ignorePath: true,
xfwd: true,
ws: true,
followRedirects: false,
})
proxyServer.on('error', (_err) => {
// TODO?: enable verbose error logs with --debug flag?
})
return proxyServer
}
// proxy to router worker
requestHandler = async (req, res) => {
const urlParts = (req.url || '').split('?')
const urlNoQuery = urlParts[0]
// this normalizes repeated slashes in the path e.g. hello//world ->
// hello/world or backslashes to forward slashes, this does not
// handle trailing slash as that is handled the same as a next.config.js
// redirect
if (urlNoQuery?.match(/(\\|\/\/)/)) {
const cleanUrl = normalizeRepeatedSlashes(req.url!)
res.statusCode = 308
res.setHeader('Location', cleanUrl)
res.end(cleanUrl)
return
}
const proxyServer = getProxyServer(req.url || '/')
// http-proxy does not properly detect a client disconnect in newer
// versions of Node.js. This is caused because it only listens for the
// `aborted` event on the our request object, but it also fully reads
// and closes the request object. Node **will not** fire `aborted` when
// the request is already closed. Listening for `close` on our response
// object will detect the disconnect, and we can abort the proxy's
// connection.
proxyServer.on('proxyReq', (proxyReq) => {
res.on('close', () => proxyReq.destroy())
})
proxyServer.on('proxyRes', (proxyRes) => {
res.on('close', () => proxyRes.destroy())
})
proxyServer.web(req, res)
}
upgradeHandler = async (req, socket, head) => {
const proxyServer = getProxyServer(req.url || '/')
proxyServer.ws(req, socket, head)
}
handlersReady()
} else {
// when not using a worker start next in main process
const next = require('../next') as typeof import('../next').default
const addr = server.address()
const app = next({
dir,
hostname,
dev: isDev,
isNodeDebugging,
httpServer: server,
customServer: false,
port: addr && typeof addr === 'object' ? addr.port : port,
})
// handle in process
requestHandler = app.getRequestHandler()
upgradeHandler = app.getUpgradeHandler()
await app.prepare()
handlersReady()
}
} catch (err) {
// fatal error if we can't setup
handlersError()
console.error(err)
process.exit(1)
}
// return teardown function for destroying the server
async function teardown() {
server.close()
sockets.forEach((socket) => {
sockets.delete(socket)
socket.destroy()
})
if (worker) {
await worker.end()
}
}
return teardown
}
'Learning Log > Next.js' 카테고리의 다른 글
[next.js] 생활코딩 Next.js 13 3일차 (0) | 2023.08.08 |
---|---|
[next.js] 생활코딩 Next.js 13 2일차 (0) | 2023.08.04 |
[next.js] 생활코딩 Next.js 13 1일차 (0) | 2023.08.03 |