[Next.js] 서버사이드의 '서버'란 어디일까

Gyuwon Lee·2023년 10월 6일
18
post-thumbnail

프론트 서버 vs 백 서버

  • 내가 알고 있던 서버:

    • 백엔드의 영역
    • 다양한 환경으로 구성할 수 있지만, 공통적으로 DB의 데이터를 읽어오거나 추가/삭제
    • 네트워크를 통한 클라이언트의 API 요청에 응답함
  • SSR, RSC를 접하고 생긴 의문:

    • 서버에서 HTML을 생성하거나 컴포넌트를 해석 (소스 코드 → 직렬화된 데이터) 한다면, 여기서의 ‘서버’ 는 대체 어디인가?
    • API 통신은 주어진 엔드포인트에 HTTP 요청을 보내 이루어지는데, 서버에 HTML 수신을 위한 엔드포인트가 따로 있는 것도 아니고… 애초에 서버가 어떤 환경인지도 모르는데 HTML을 만들 수 있나? 서버가 리액트 코드를 어떻게 이해하지?

  • 서버는 말 그대로 ‘클라이언트의 요청에 응답하는 프로그램’ 을 통칭한다. 따라서 어떤 요청에 응답하는지에 따라 HTML/CSS/JS 등 정적 파일을 보내주는 프론트엔드 서버와, DB 데이터를 보내주고 연산하는 백엔드 서버로 나눌 수 있다.

    • 정적 리소스 (static): 정적 리소스는 변화가 없는 리소스를 뜻한다. 즉, HTML, CSS, Javascript와 같이 미리 서버에 저장해두고 서버가 요청을 받으면 응답만 해주면 되는 것들을 뜻한다. 어느 사용자에게도 동일한 결과값을 보여준다.
    • 동적 리소스 (dynamic): 동적 리소스는 누가, 언제, 어떻게 서버에 요청했는지에 따라 결과값을 다르게 보여주는 리소스를 뜻한다. 사용자에게 맞춤형 콘텐츠를 제공해줄 수 있게 된다. ex) 유투브의 추천 영상
  • 백엔드 서버(API)서버는 DB 조회 및 다양한 로직을 처리하는데만 집중하도록 해야 한다. 웹 서버(프론트 서버)를 통해 단순히 정적 컨텐츠를 응답해주는 역할을 맡기면서 서버의 부하를 방지할 수 있다.

  • 결론적으로, 우리가 create-react-app 에서 dev 명령어를 사용하거나, create-next-app에서 next dev 명령어를 사용해 프로그램을 실행시키면 작성된 소스코드를 바탕으로 개발 서버가 실행되어 각 페이지에 필요한 정적 리소스를 브라우저에 보내주는 것이다.

Next.js Dev Server

  • create-react-app 의 경우 webpack-dev-server 를 사용하는데, next의 경우 자체적인 built-in 서버를 사용하는 듯하다.
  • node_modulesnext 패키지를 확인해 보자.
// package.json
"bin": {
  "next": "./dist/bin/next"
},
  • package.json 의 bin 필드를 확인하면 CLI 명령어 입력 시 어떤 파일이 실행되는지 확인할 수 있다.
// ./dist/bin/next

const _commands = require("../lib/commands");
const foundCommand = Boolean(_commands.commands[args._[0]]);
const command = foundCommand ? args._[0] : defaultCommand;
...
async function main() {
  const currentArgsSpec = _commandargs.commandArgs[command]();
  const validatedArgs = (0, _getvalidatedargs.getValidatedArgs)(currentArgsSpec, forwardedArgs);
  ...
  await _commands.commands[command]().then((exec)=>exec(validatedArgs)).then(()=>{
    if (command === "build" || command === "experimental-compile") {
      // ensure process exits after build completes so open handles/connections
      // don't cause process to hang
      process.exit(0);
    }
  });
}
main();
// ./dist/lib/commands

const commands = {
  build: ()=>Promise.resolve(require("../cli/next-build").nextBuild),
  start: ()=>Promise.resolve(require("../cli/next-start").nextStart),
  export: ()=>Promise.resolve(require("../cli/next-export").nextExport),
  dev: ()=>Promise.resolve(require("../cli/next-dev").nextDev),
  lint: ()=>Promise.resolve(require("../cli/next-lint").nextLint),
  telemetry: ()=>Promise.resolve(require("../cli/next-telemetry").nextTelemetry),
  info: ()=>Promise.resolve(require("../cli/next-info").nextInfo),
  "experimental-compile": ()=>Promise.resolve(require("../cli/next-build").nextBuild),
  "experimental-generate": ()=>Promise.resolve(require("../cli/next-build").nextBuild)
};
  • commands 객체에 각 명령어 입력 시 실행되어야 하는 모듈을 명시해 둔다.
  • main 함수에서는 이 commands 객체를 불러와, 해당 커맨드에 해당하는 모듈을 실행한다. 이때 빌드 또는 컴파일 명령어가 실행되었다면 해당 프로세스를 명시적으로 종료해 준다. 즉, 반대로 생각하면 dev 명령어 실행 시에는 프로세스가 계속 실행 중인 상태가 된다. 즉, 서버를 켜고 있다는 사실을 알 수 있다.
const nextDev = async (args)=>{
  ...
  const port = (0, _utils.getPort)(args);
  const distDir = _path.default.join(dir, config.distDir ?? ".next");
  ...
  const startServerPath = require.resolve("../server/lib/start-server");
  async function startServer(options) {
    return new Promise((resolve)=>{
      var _options_selfSignedCertificate;
      let resolved = false;
      child = (0, _child_process.fork)(startServerPath, {...});
      child.on("message", (msg)=>{...});
      child.on("exit", async (code, signal)=>{...});
  }
  const runDevServer = async (reboot)=>{
    try {
      if (!!args["--experimental-https"]) {
        ...
      } else {
        await startServer(devServerOptions);
      }
      await preflight(reboot);
    } catch (err) {
      console.error(err);
      process.exit(1);
    }
  };
  await (0, _trace.trace)("start-dev-server").traceAsyncFn(async (_)=>{
      await runDevServer(false);
  });
};
  • start-server 모듈을 불러와서, 해당 프로세스를 fork 해서 실행시키는 것을 확인할 수 있다.
// server/lib/start-server

const _http = /*#__PURE__*/ _interop_require_default(require("http"));
const _https = /*#__PURE__*/ _interop_require_default(require("https"));
...
async function startServer({...}) {
	...
	const server = selfSignedCertificate ? _https.default.createServer({
	  key: _fs.default.readFileSync(selfSignedCertificate.key),
	  cert: _fs.default.readFileSync(selfSignedCertificate.cert)
	}, requestListener) : _http.default.createServer(requestListener);
	...
	await new Promise((resolve)=>{
	  server.on("listening", async () => {
			...
	    resolve();
	  });
	  server.listen(port, hostname);
	});
}
  • start-server 모듈은 찐 서버 로직이라 자세히 읽지 않았지만, http 또는 https 서버를 실행시키는 역할을 한다. (next 에는 이외에도 render-server, router-server 등의 파일이 더 있다.)

서버 그리고 SSR, RSC

  • 결국 SSR 또는 RSC 사용 시 작업을 도맡는 ‘서버’ 란 next에 의해 구현되어 있는 웹 서버다. 백엔드 개발자들이 DB 데이터 읽기/쓰기를 위해 구현하는 웹 어플리케이션 서버와 혼동하면 안 된다.

  • 기존에 리액트 소스 코드를 JS 스크립트로 번들에 포함시켜 클라이언트에서 렌더링하던 방식과는 다르게, SSR의 경우 소스코드를 바탕으로 HTML을 생성하는 작업을 이러한 서버에서 모두 마친 뒤 브라우저에 HTML을 전송한다는 것이고, RSC의 경우에는 HTML 이전에 컴포넌트 렌더링 자체를 서버에서 작업한다는 것이다.

    • SSR은 사실 initial page rendering 과 관련이 있는데, interactive 하지 않은 first snapshot 을 HTML 형태로 전송하는 것이므로 hydration이 끝나고 나서부터는 일반적인 client-side rendering 페이지와 차이점이 없게 된다.
    • 그러나 RSC의 경우 컴포넌트 렌더링 자체를 서버에서 하는 것이므로 유저 인터랙션에 의해 페이지의 일부가 변경되는 등 initial-render 이후에 페이지에 변경사항이 생겼을 때도 JS 스크립트에 의해 브라우저에서 처리되는 것이 아니라 서버에서 렌더링되어 페이지에 나타나게 된다.
  • 리액트에서 컴포넌트란 리액트 element를 리턴하는 함수 또는 클래스다. 이 컴포넌트들로 이루어진 React tree를 해석해서 HTML로 만드는 것이 렌더링이다. 기존에는 이 렌더링을 위한 JS 번들에 컴포넌트를 해석하는 로직이 포함되어 있어 컴포넌트를 브라우저에서 그리기 시작했다면, RSC는 이미 서버에서 한 차례 해석되어 직렬화된 JSON 형태로 전달된다.

RSC만의 장점?

  • RSC를 사용하면, 브라우저에서 실행될 JS 번들의 사이즈를 크게 줄일 수 있다. 컴포넌트 소스 코드나, 컴포넌트 내부에서 사용된 외부 라이브러리 코드 등이 포함될 필요가 없다. 어차피 서버에서 전부 실행되고, 클라이언트는 직렬화된 JSON만 받기 때문이다.

  • 또한 기존 Next.js의 SSR에서는 서버 쪽 로직을 작성하기 위해 반드시 getServerSideProps, getStaticProps 등의 함수를 사용해야 했고, 이는 page-level 에서만 export될 수 있었다. 따라서 서버 쪽 로직에서 데이터를 fetch해왔다면 컴포넌트에 넘겨주기 위해서는 props drilling이 불가피했다. 반면 RSC는 컴포넌트 코드 자체가 서버에서 해석(렌더링)되므로 별도의 함수가 필요없고, 컴포넌트 내부에서 직접 서버 리소스를 활용할 수 있게 되었다.

profile
하루가 모여 역사가 된다

0개의 댓글