이번 포스팅은...
저번 프리온보딩은 제대로 참여를 못했다.
주말이 껴 있는 부분도 있었고, 개인적인 사유로 좀 많이 바빴다.
이번 7월 챌린지는 예전에 1월인가 프리온보딩을 진행했던 강사님이 다시 진행하신다고 하셨다.
디스코드를 안나가고 있었는데 다행이었다(?)
사전과제가 있었는데 이를 정리해서 개인 블로그에 포스팅하고 링크 제출이 있었다.
매번 이론은 잘 안보고 기능 구현만 했는데...
이번에 사전 과제를 하면서 한번 정리를 해봐야겠다.
근데 어렵게 설명하는건 좋아하지 않아서 나름 쉽게 풀고 예시를 들어 설명할 예정이다.
1. CSR(Client-side Rendering)이란 무엇이며, 그것의 장단점에 대하여 설명해주세요.
CSR(Client-side Rendering)이란 무엇인가?
영어에서 표현한바와 같이 랜더링 작업을 클라이언트(브라우저단)에서 해주는 것을 의미한다.
다른 블로그나 문서를 보면 뭔가 어렵고 복잡하게 설명을 하는데 난 이 부분을 쉽게 설명해보겠다.
웹 브라우저에서 뭔가를 보여주려면 세 가지 요소가 필요하다.
페이지를 구성하는 HTML,
페이지를 이쁘게 꾸미기 위한 CSS,
그리고 사용자와 상호 작용을 위한 JS
이렇게 세 가지 요소가 필요하다.
이게 있어야 웹 브라우저에서 뭔가를 표현하거나, 사용자와 상호 작용을 할 수 있다.
일단 웹 페이지는 저 세 가지 요소가 삼위일체(트포?) 마냥 잘 조합되어야 사용자가 비로소 사용할 수 있게 된다.
조합을 해주는 주체가 서버이면 SSR이고, 클라이언트(사용자측)이라면 CSR이다.
CSR은 저 요소들 중 HTML은 최소한의 태그만 포함한 가벼운 형태(body가 비어있는)로 서버에게 받아온다.
그리고 JS를 받아오고, 그 JS를 가지고 리엑트나 뷰를 통해서 HTML을 랜더링 해준다.
게임으로 설명해보면...
스타크래프트의 저그 드론이라 생각하면 된다.
이 드론은 스토리상 저그 건물의 유전정보를 모두 가지고 있다고 한다.
그래서 오버로드의 명령에 따라 필요한 건물로 변태를 한다.
CSR도 얘랑 비슷하다.
맨 처음 서버에서 저 드론을 하나 받았다고 생각해보자.
그럼 저 드론은 아래와 같이 다양한 형태로 변태가 가능하다.
그렇다...
서버로부터 드론(HTML과 JS를 내포(內包)한 것)을 내려받고,
드론은 내포된 JS를 통해 HTML을 랜더링한다.
이 정도 설명하면 CSR는 이해가 되었을 것이다.
번외로...
**SSR(Server-Side Rendering)**은 프로토스의 프로브가 소환하는 건물이라 보면 된다.
스토리상...
프로토스 건물은 아이어에서 이미 완성된 건물을 포탈을 통해 이동시키는거다.
즉 프로브(클라이언트)가 서버측(아이어)에게 요청을 하면,
서버측에서 랜더링을 다한 HTML과 JS를 클라이언트에게 주는 것이다.
급조한 거라서 설명이 뭔가 매끄럽지 못한데 -_-;;
대충 이 정도면 감을 잡았으리라 생각한다.
CSR(Client-side Rendering)의 장단점
CRS의 장단점은 아래와 같다.
장점
- 다른 페이지로 이동하거나 추가 페이지 로드 시간이 빠르다.
- 이미 서버로부터 모든 자원(JS,CSS)을 내려받았기 때문에 추가적인 자원을 로드하는데 시간이 필요하지 않다.
- UI를 전체적으로 다시 로드할 필요가 없다.
- 변경되는 부분만 업데이트가 가능하다.
단점
- HTML 파일만 받고, 클라이언트에서 랜더링을 하기에 SEO(search engine optimization)에 불리하다.
- 크롤링 봇은 HTML 파일을 보고 판단하는데 내용이 비어있다.
- 첫 로드 시 모든 로직을 담은 파일(JS)를 받기에 첫 진입 속도가 느리다.
- 이와 연계되는게 클라이언트의 하드웨어 및 소프트웨어에 의존도가 높아져서, 사용자마다 페이지 로드 시간이 다를 수 있다.
- JS가 동작하지 않는 브라우저 환경에서의 문제
- 물론 JS가 돌아가지 않는 브라우저가 어디있겠냐지만, 보안이나 어떤 문제로 인해 동작할 수 없는 환경이 있을 수 있다.
2. SPA(Single Page Application)로 구성된 웹 앱에서 SSR(Server-side Rendering)이 필요한 이유에 대하여 설명해주세요.
SPA는 가만 보기에 CRS와 유사하다.
어찌보면 SPA의 구현 방식중 CRS나 SSR이 있다고 생각하면 편하다.
근데 SPA에서 SSR이 필요한 이유는 CRS의 단점을 극복하기 위함이라 보면 된다.
자주 언급되는 SEO의 경우 CRS에서 극복이 어느정도 가능하지만, 손쉽게 처리하기 어렵다.
브라우저의 호환성도 이슈가 될 수 있다.
SPA의 경우 클라이언트의 브라우저에 100% 의존하기에 몇몇 자바스크립트 문법의 경우 브라우저마다 다르게 동작한다.
SSR을 사용할 경우 서버에서 초기 페이지에 대해 렌더링을 해주기에 이 문제를 어느정도 완화시킬 수 있다.
마지막으로 보안 문제가 있을 수 있다.
클라이언트에서 서버에게 Ajax나 Axios 등 요청으로 권한 검사를 해줄 수 있다.
하지만 권한이 있는 사용자만 접근해야 하는 페이지가 있거나, 권한이 있어야 볼 수 있는 항목을 처리해야 할 경우 문제가 생길 수 있다.
보안 처리를 클라이언트에게 맡기는 경우 상당한 리스크가 생긴다.
SSR을 사용할 경우 서버 측에서 초기 렌더링 시 보안 로직을 수행하여 결과를 다르게 줄 수 있는 장점이 있다.
3. Next.js 프로젝트에서 yarn start(or npm run start) 스크립트를 실행했을 때 실행되는 코드를 Next.js Github 레포지토리에서 찾은 뒤, 해당 파일에 대한 간단한 설명을 첨부해주세요.
먼저 이 질문을 이해하기 앞서, Next.JS가 아닌 npm이나 Node의 진입점이 궁금했다.
정확히 이야기하면, **yarn start(or npm run start)**이 명령어를 줬을 때 실행하는 최초 시작점이랄까?
Spring의 경우 Main함수가 있다.
@Slf4j
@SpringBootApplication
@EnableAspectJAutoProxy
@EnableScheduling
@ServletComponentScan
public class OnlydreamApplication implements CommandLineRunner, ApplicationRunner {
public static void main(String[] args) {
ApiContextInitializer.init();
SpringApplication.run(OnlydreamApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
}
@Override
public void run(ApplicationArguments args) throws Exception {
}
}
옛날에 스프링으로 개발했던 흑우집합소 코드인데... 아련하다 ㅎㅎ
무튼 이런 시작점이 어딘지 잘 몰랐다.
Nest.Js의 경우에는 main.ts등으로 아래와 같이 bootstrap함수를 호출하는 것으로 시작한다.
async function bootstrap() {
const app = await NestFactory.create(AppModule, {... });
await app.listen(9911, "0.0.0.0");
}
bootstrap();
하지만 Next.Js의 경우 이 명령어를 줬을 때 실행하는 **Main함수(또는 메서드)**를 찾을 수 없었다.
구글링 한 결과...
node_modules/next/dist/cli/next-start.js 저 파일을 확인해보라 했다.
이제부터 긴 설명에 앞서...
만약 본인의 컴퓨터에 있는 Next.Js 프로젝트가 아닌 깃허브에서 보고 싶다면,
Github의 next.js/packages/next/src를 참고하며 보도록 하자.
결국 개인 프로젝트 폴더의 node_modules/next/dist/ 와 next.js/packages/next/src 는 같은 결과물을 바라본다.
물론 버전에 따라 틀릴 수 있겠지만...
그리고 정답을 먼저 공개하자면...
base-server.ts가 모든 것을 쥐고 있다. (로컬일 경우 base-server.js)
이 부분은 아래 설명을 추적하다보면 이해가 될 것이다.
그럼 위에 나온대로 next-start.js파일부터 하나씩 내려가보자.
//node_modules/next/dist/cli/next-start.js
#!/usr/bin/env node
'use strict'
Object.defineProperty(exports, '__esModule', {
value: true,
})
exports.nextStart = void 0
var _indexJs = _interopRequireDefault(
require('next/dist/compiled/arg/index.js')
)
var _startServer = require('../server/lib/start-server')
var _utils = require('../server/lib/utils')
var Log = _interopRequireWildcard(require('../build/output/log'))
var _isError = _interopRequireDefault(require('../lib/is-error'))
var _getProjectDir = require('../lib/get-project-dir')
function _interopRequireDefault(obj) {
return obj && obj.__esModule
? obj
: {
default: obj,
}
}
//기타 엄청난 코드들...
잠깐 열었다가 닫고 싶어진 코드였다 -_-;;
그래도 학습을 위해선 봐야지...
조금 스크롤을 내리면 const nextStart = (argv)=>{} 익명함수가 보인다.
//node_modules/next/dist/cli/next-start.js
const nextStart = argv => {
const validArgs = {
// Types
"--help": Boolean,
"--port": Number,
"--hostname": String,
"--keepAliveTimeout": Number,
// Aliases
"-h": "--help",
"-p": "--port",
"-H": "--hostname",
}
}
//...
;(0, _startServer)
.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)
})
Cli 전달인자들에 대한 것들이 있고 마지막 쯔음 startServer서버를 실행하는 함수를 전달받는 함수가 있다.
**(0, _startServer).**이 형태는 처음 보는 형태였다.
저게 어떻게 함수를 호출하는지...
기본기가 부족한 나로써는 이해가 되지 않지만...
일단 더 추적해봤다.
저 전달인자인 _startServer를 추적해보니...
node_modules/next/dist/server/start-server.d.ts 저 파일로 연결이 되었다.
//node_modules/next/dist/server/start-server.d.ts
import type { NextServerOptions, NextServer } from "../next"
interface StartServerOptions extends NextServerOptions {
allowRetry?: boolean
keepAliveTimeout?: number
}
export declare function startServer(
opts: StartServerOptions
): Promise<NextServer>
export {}
이것만 보니 뭔가 구현체?
서버를 띄우거나 하는 것이 없었고,
대신 파일 아래를 보니 start-server.js파일이 존재했다.
//node_modules/next/dist/server/lib/start-server.js
"use strict"
Object.defineProperty(exports, "__esModule", {
value: true,
})
exports.startServer = startServer
var _log = require("../../build/output/log")
var _http = _interopRequireDefault(require("http"))
var _next = _interopRequireDefault(require("../next"))
function _interopRequireDefault(obj) {
return obj && obj.__esModule
? obj
: {
default: obj,
}
}
function startServer(opts) {
let requestHandler
const server = _http.default.createServer((req, res) => {
return requestHandler(req, res)
})
//...
}
이 코드의 startServer함수가 서버를 띄우는 것임을 확인했다.
그리고 server변수를 보면 _http를 통해서 서버를 띄운다.
그리고 저 _http는 node 서버임을 확인할 수 있다.
난 Next.JS가 어떤 서버를 가지고 있나 싶었는데...
실질적으로 Next.Js는 Node 서버를 내포하고 있음을 확인할 수 있었다.
그리고 startServer에서 조금 더 내려가서 서버가 구동되면 동작하는 코드가 있다.
//node_modules/next/dist/server/lib/start-server.js
server.on("listening", () => {
const addr = server.address()
const hostname =
!opts.hostname || opts.hostname === "0.0.0.0" ? "localhost" : opts.hostname
const app = (0, _next).default({
...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)
저기 보면 또 이상한 **(0, _next)**문법을 사용하는데...
나중에 알아봐야겠다.
무튼 저기 보면 _next를 전달인자로 잡는데 이걸 추적하면,
node_modules/next/dist/server/next.d.ts 이 파일로 이동한다.
//node_modules/next/dist/server/next.d.ts
//...
export declare class NextServer {
private serverPromise?
private server?
private reqHandlerPromise?
private preparedAssetPrefix?
options: NextServerOptions
constructor(options: NextServerOptions)
get hostname(): string | undefined
get port(): number | undefined
getRequestHandler(): RequestHandler
getUpgradeHandler(): (
req: IncomingMessage,
socket: any,
head: any
) => Promise<void>
setAssetPrefix(assetPrefix: string): void
logError(...args: Parameters<Server["logError"]>): void
render(...args: Parameters<Server["render"]>): Promise<void>
renderToHTML(
...args: Parameters<Server["renderToHTML"]>
): Promise<string | null>
renderError(...args: Parameters<Server["renderError"]>): Promise<void>
renderErrorToHTML(
...args: Parameters<Server["renderErrorToHTML"]>
): Promise<string | null>
render404(...args: Parameters<Server["render404"]>): Promise<void>
serveStatic(...args: Parameters<Server["serveStatic"]>): Promise<void>
prepare(): Promise<void>
close(): Promise<any>
private createServer
private loadConfig
private getServer
private getServerRequestHandler
}
위의 start-server.d.ts처럼 이 파일도 ts와 같은 경로에 next.js파일이 있다.
이는 내 프로젝트에서 배포 시 ts => js로 변경을 해서 그렇지 않을까 싶다.
결국 배포할 때는 ts가 js로 변하기 때문에?
저기에 있는 함수 중 render시리즈를 보면 다음과 같이 구현되어 있다.
//node_modules/next/dist/server/next.js
async render(...args) {
const server = await this.getServer();
return server.render(...args);
}
async renderToHTML(...args) {
const server = await this.getServer();
return server.renderToHTML(...args);
}
async renderError(...args) {
const server = await this.getServer();
return server.renderError(...args);
}
async renderErrorToHTML(...args) {
const server = await this.getServer();
return server.renderErrorToHTML(...args);
}
async render404(...args) {
const server = await this.getServer();
return server.render404(...args);
}
리턴이 **const server = await this.getServer();**의 객체에 있는 함수를 호출한다.
getServer함수는 바로 아래에 있다.
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;
}
결국 자신의 함수 중 createServer함수를 호출한다.
//node_modules/next/dist/server/next.js
async createServer(options) {
if (options.dev) {
const DevServer = require("./dev/next-dev-server").default;
return new DevServer(options);
}
const ServerImplementation = await getServerImpl();
return new ServerImplementation(options);
}
여기서 보면 dev 모드일 경우와 일반(아마 운영과 같이 dev를 사용하지 않는)모드로 나눠서 서버를 반환한다.
물론 결국 둘다 하나의 클래스에서 파생되었겠지만...
그럼 먼저 개발 모드를 보자.
Dev
개발 서버 측으로 와보면 이제 실제 Next.js의 서버 구현체를 지나 각 페이지의 구현 및 라우팅, 기타 컴포넌트를 다루는 것들 등...
다양한 메서드로 알차게(?) 채워져 있다.
뒤늦게 안 사실이지만 상속된 메서드를 구현하는 것이었다.
Java나 Dart와 같은 언어처럼 상속 시 @Override 어노테이션이 없어서 구분이 안되었다.
//
//node_modules/next/dist/server/dev/next-dev-server.d.ts
export interface Options extends ServerOptions {
/**
* Tells of Next.js is running from the `next dev` command
*/
isNextDevCommand?: boolean
}
export default class DevServer extends Server {
///....
protected getAppPathsManifest(): undefined
protected getCustomRoutes(): CustomRoutes
protected getPreviewProps(): __ApiPreviewProps
protected getPagesManifest(): undefined
protected getAppPathsManifest(): undefined
protected getMiddleware(): MiddlewareRoutingItem | undefined
protected getEdgeFunctions(): RoutingItem[]
protected getServerComponentManifest(): undefined
protected getServerCSSManifest(): undefined
protected hasMiddleware(): Promise<boolean>
protected ensureMiddleware(): Promise<void>
protected ensureEdgeFunction({
page,
appPaths,
}: {
page: string
appPaths: string[] | null
}): Promise<void>
generateRoutes(): {
headers: import("../router").Route[]
rewrites: {
beforeFiles: import("../router").Route[]
afterFiles: import("../router").Route[]
fallback: import("../router").Route[]
}
redirects: import("../router").Route[]
catchAllRoute: import("../router").Route
catchAllMiddleware: import("../router").Route[]
pageChecker: import("../router").PageChecker
useFileSystemPublicRoutes: boolean
dynamicRoutes: import("../router").DynamicRoutes | undefined
nextConfig: import("../config-shared").NextConfig
fsRoutes: import("../router").Route[]
}
protected generatePublicRoutes(): never[]
protected getDynamicRoutes(): never[]
protected findPageComponents({
pathname,
query,
params,
isAppPath,
appPaths,
}: {
pathname: string
query: ParsedUrlQuery
params: Params
isAppPath: boolean
appPaths?: string[] | null
}): Promise<FindComponentsResult | null>
}
코드는 너무 길어서 다 확인할 수 없지만...
run함수는 아마 Next API쪽을 다루는게 아닌가 싶다.
//node_modules/next/dist/server/dev/next-dev-server.js
async run(req, res, parsedUrl) {
await this.devReady;
this.setupWebSocketHandler(undefined, req);
const { basePath } = this.nextConfig;
let originalPathname = null;
if (basePath && (0, _pathHasPrefix).pathHasPrefix(parsedUrl.pathname || "/", basePath)) {
// strip basePath before handling dev bundles
// If replace ends up replacing the full url it'll be `undefined`, meaning we have to default it to `/`
originalPathname = parsedUrl.pathname;
parsedUrl.pathname = (0, _removePathPrefix).removePathPrefix(parsedUrl.pathname || "/", basePath);
}
const { pathname } = parsedUrl;
if (pathname.startsWith("/_next")) {
if (await (0, _fileExists).fileExists((0, _path).join(this.publicDir, "_next"))) {
throw new Error(_constants.PUBLIC_DIR_MIDDLEWARE_CONFLICT);
}
}
const { finished =false } = await this.hotReloader.run(req.originalRequest, res.originalResponse, parsedUrl);
if (finished) {
return;
}
if (originalPathname) {
// restore the path before continuing so that custom-routes can accurately determine
// if they should match against the basePath or not
parsedUrl.pathname = originalPathname;
}
try {
return await super.run(req, res, parsedUrl);
} catch (error) {
res.statusCode = 500;
const err = (0, _isError).getProperError(error);
try {
this.logErrorWithOriginalStack(err).catch(()=>{});
return await this.renderError(err, req, res, pathname, {
__NEXT_PAGE: (0, _isError).default(err) && err.page || pathname || ""
});
} catch (internalErr) {
console.error(internalErr);
res.body("Internal Server Error").send();
}
}
}
뭐 여기까지만 보고 이거 다 이해하려하면 너무 어려우니,
대충 어디서 돌아가는지만 파악하면 될 듯 싶다.
그리고 넘어가기 전 이 DevServer의 상속 구조를 기억하고 가자.
//var _nextServer = _interopRequireWildcard(require("../next-server"));
class DevServer extends _nextServer.default {}
저 _nextServer는 운영 서버 클래스를 나타낸다.
링크를 타고 가보면 node_modules/next/dist/server/next-server.d.ts를 나타냄을 알 수 있다.
운영
아까 보던 곳에서 운영은 다음과 같이 호출하게 되어 있다.
//node_modules/next/dist/server/next.js
const getServerImpl = async () => {
if (ServerImpl === undefined)
ServerImpl = (await Promise.resolve(require("./next-server"))).default
return ServerImpl
}
근데 개발 섭도 결국 운영에서 파생되어 진거고, 이 운영섭은 어떤걸 상속받았을까?
export default class NextNodeServer extends BaseServer {}
저 BaseServer가 실제 Next.Js의 서버 중추 역할을 하는 것을 알 수 있다.
우리가 Next.js에서 yarn dev 또는 yarn start를 수행하면 위의 과정을 거친 후 서버가 구동이 되고,
구동된 서버 위에서 route를 호출할 경우 이런 저런 호출을 타고 결국 아래의 함수를 호출한다.
//node_modules/next/dist/server/base-server.js
async renderToResponse(ctx) {
const { res , query , pathname } = ctx;
let page = pathname;
const bubbleNoFallback = !!query._nextBubbleNoFallback;
delete query._nextBubbleNoFallback;
try {
// Ensure a request to the URL /accounts/[id] will be treated as a dynamic
// route correctly and not loaded immediately without parsing params.
if (!(0, _utils1).isDynamicRoute(page)) {
const result = await this.renderPageComponent(ctx, bubbleNoFallback);
if (result !== false) return result;
}
if (this.dynamicRoutes) {
for (const dynamicRoute of this.dynamicRoutes){
const params = dynamicRoute.match(pathname);
if (!params) {
continue;
}
page = dynamicRoute.page;
const result = await this.renderPageComponent({
...ctx,
pathname: page,
renderOpts: {
...ctx.renderOpts,
params
}
}, bubbleNoFallback);
if (result !== false) return result;
}
}
// currently edge functions aren't receiving the x-matched-path
// header so we need to fallback to matching the current page
// when we weren't able to match via dynamic route to handle
// the rewrite case
// @ts-expect-error extended in child class web-server
if (this.serverOptions.webServerConfig) {
// @ts-expect-error extended in child class web-server
ctx.pathname = this.serverOptions.webServerConfig.page;
const result = await this.renderPageComponent(ctx, bubbleNoFallback);
if (result !== false) return result;
}
} catch (error) {
const err = (0, _isError).getProperError(error);
if (error instanceof _utils.MissingStaticPage) {
console.error("Invariant: failed to load static page", JSON.stringify({
page,
url: ctx.req.url,
matchedPath: ctx.req.headers["x-matched-path"],
initUrl: (0, _requestMeta).getRequestMeta(ctx.req, "__NEXT_INIT_URL"),
didRewrite: (0, _requestMeta).getRequestMeta(ctx.req, "_nextDidRewrite"),
rewroteUrl: (0, _requestMeta).getRequestMeta(ctx.req, "_nextRewroteUrl")
}, null, 2));
throw err;
}
if (err instanceof NoFallbackError && bubbleNoFallback) {
throw err;
}
if (err instanceof _utils.DecodeError || err instanceof _utils.NormalizeError) {
res.statusCode = 400;
return await this.renderErrorToResponse(ctx, err);
}
res.statusCode = 500;
// if pages/500 is present we still need to trigger
// /_error `getInitialProps` to allow reporting error
if (await this.hasPage("/500")) {
ctx.query.__nextCustomErrorRender = "1";
await this.renderErrorToResponse(ctx, err);
delete ctx.query.__nextCustomErrorRender;
}
const isWrappedError = err instanceof WrappedBuildError;
if (!isWrappedError) {
if (this.minimalMode && process.env.NEXT_RUNTIME !== "edge" || this.renderOpts.dev) {
if ((0, _isError).default(err)) err.page = page;
throw err;
}
this.logError((0, _isError).getProperError(err));
}
const response = await this.renderErrorToResponse(ctx, isWrappedError ? err.innerError : err);
return response;
}
if (this.router.catchAllMiddleware[0] && !!ctx.req.headers["x-nextjs-data"] && (!res.statusCode || res.statusCode === 200 || res.statusCode === 404)) {
res.setHeader("x-nextjs-matched-path", `${query.__nextLocale ? `/${query.__nextLocale}` : ""}${pathname}`);
res.statusCode = 200;
res.setHeader("content-type", "application/json");
res.body("{}");
res.send();
return null;
}
res.statusCode = 404;
return this.renderErrorToResponse(ctx, null);
}
그리고 결국 우리가 실제로 작성하게 되는 _document.tsx나 _app.tsx를 랜더링 하며,
각 컴포넌트들을 순차적으로 렌더링 하게 된다.
정리
3번 문항의 경우 하나씩 추적하느라 설명이 너무 복잡하고 길었다.
개인적으로 이렇게 딥하게 파는걸 좋아하진 않지만...
동작을 이해하기 위해서는 필요한 과정이라 생각한다.
물론 서비스 구현에 목적이 있고, 다양한 아이템에 대해 아이디어를 녹여서,
결과를 빨리 봐야 하는 사람들에겐 이게 무슨 의미냐 싶겠지만...
어떤식으로든 결국 이 구조를 한번쯤은 까봐야 하는 순간이 올 것이다.
이런 딥한걸 싫어하는 나같은 사람을 위해 최소한의 설명으로 풀어내봤다.
과제도 과제였지만, 덕분에 돌아가는 방식을 조금이나마 알게 되어 유익한 시간이었다.
이제 다시 뚜껑을 덮고 서비스를 구현해봐야겠다.