[Foundation] How Next.js Works

2023. 4. 18. 23:16Next.js/Foundations

본격적으로 Next.js 기능들을 배우기 전에 Next.js가 어떻게 돌아가는지 이해한다면 보다 도움이 될 것이다. 

앞서 리액트는 라이브러리라는 특성상 애플리케이션 빌드 및 구조화 방법이 자유롭고, 때문에 여러 방법이 있다고 언급한 바 있다. 반면 Next.js는 프레임워크로써 더 빠른 앱 개발과 최적화에 필요한 도구들을 제공한다.

여하튼 Next.js의 코드가 돌아가면서 각 단계별로 어떤 일이 벌어지는지 한번 뜯어보자. 아래 개념과 같은 사항들에 대해 알아보고, Next.js가 코드의 백그라운드에서 어떤 프로세스를 거치는지 논의해볼 예정이다.

  • 코드가 실행되는 환경 : Development vs Production
  • 코드가 실행되는 단계 : 빌드 타임 vs 런타임
  • 렌더링이 일어나는 곳 : 클라이언트 vs 서버

 

 

1. 개발환경에서 프로덕션으로

'환경'이란 말은 커드가 실행되는 컨텍스트라고 생각할 수 있다.

크게 분류하자면 개발 환경과 배포 환경이 있는데, 개발 단계에선 로컬 PC에서 애플리케이션을 빌드하고 실행한다. 한편, 프로덕션은 애플리케이션을 배포하고 사용자들이 실제로 사용할 수 있게끔 만드는 프로세스이다.

1.1 Next.js의 개발/배포환경
Next.js에선 개발과 프로덕션 단게에서 필요한 각 기능들을 제공한다.

  • 개발 단계에선 보다 나은 개발자 경험을 위한 최적화된 기능들을 제공한다. 예를 들어, TypeScript 지원이나 ESLint 통합, Fast Refresh 기능 등등이 있다.
  • 프로덕션 단계에선 좋은 유저 경험을 위해 Next.js가 알아서 최적화 작업을 수행한다. 앱의 성능과 접근성을 높이는 방향으로 코드를 변환하는 것이 목표이다.


환경마다 고려 사항과 목표가 워낙 다양하다보니 그만큼 애플리케이션을 개발 → 프로덕션 단계로 옮기는 과정에서 수행해야 할 작업이 많다. 컴파일, 번들링, 최소화, 코드 스플리팅 등등이 모두 그 예이다.

 

1.2 Next.js 컴파일러

Next.js는 코드 변환을 위한 수많은 작업들을 핸들링하고, 이를 위해 여러 인프라가 내장되어 있다. 결국 이 덕분에 개발자가 만든 애플리케이션을 쉽게 배포 환경까지 끌고갈 수 있는 것이다.

그렇다면 Next.js는 무슨짓을 해놨길레 이런 핸들링이 가능한 것일까? 정리해서 말하자면

  • Next.js가 Rust로 작성된 컴파일러를 갖고 있고,
  • SWC라는 플랫폼을 사용하기에 가능한 것이다.


여기서 Rsut는 로우레벨 개발 언어중 하나이고, SWC는 컴파일, 최소화, 번들링 등에 사용할 수 있는 플랫폼이다. 여하튼 바로 다음 단원에서 이 내용들에 대해 다뤄보자.

 

 

2. 컴파일(Compiling)

컴파일이란 한 언어로 작성된 코드를 다른 언어 혹은 동일 언어의 다른 버전으로 변환하는 과정을 말한다.

요새는 개발자는 보통 개발자 친화적인 JSX나 Typesript, 모던JS 같은 언어로 코드를 작성한다. 이러한 언어들은 분명 개발 효율성에 도움을 주지만, 정작 브라우저는 이를 이해할 수 없는 코드로 받아들이게 된다. 때문에 결국 브라우저가 이해할 수 있게 코드를 변환해줘야하고, 이는 즉 자바스크립트로의 컴파일이 필요하다는 의미가 된다.

 

Next.js에서 컴파일은 개발자가 코드를 작성하고 수정하는 개발 단계와 애플리케이션을 프로덕션에 사용할 수 있도록 빌드하는 단게 모두에서 일어난다.

 

 

3. 최소화(Minifying)

개발자는 코드를 짤때 가독성을 중시해서 작성한다. 이 과정에서 주석, 공백, 들여쓰기, line분할 등이 추가되는데, 사실 코드를 실행한다는 관점에선 필요하지 않은 정보들이다.

이처럼 불필요한 코드 서식 및 주석을 제거하는 프로세스를 최소화Minificationp이라고 한다. 결국 최소화란 파일 크기의 감소와 성능 향상을 얻어내기 위한 작업이라고 볼 수 있다.


Next.js에선 프로덕션을 위해 JS와 CSS파일이 자동으로 최소화된다.

 

 

4. 번들링(Bundling)

개발자는 하나의 애플리케이션을 만들기 위해 모듈, 컴포넌트, 함수 등의 더 작은 단위로 나누어 개발하고, 이들을 조립해 완전한 하나의 애플리케이션을 완성한다. 이러한 내부 모듈들과 서드파티 패키지들을 export/import 하다보면 자연스럽게 복잡한 종속성 덩어리의 웹이 되고만다.


번들링은 이처럼 복잡한 종속성을 해결하고, 파일이나 모듈들을 병합(패키징)해 브라우저에 최적화된 번들로 만드는 프로세스를 말한다. 번들링을 하면 유저가 웹 페이지를 방문했을 때, 파일 요청 횟수를 줄일 수 있다.

 

 

5. 코드 스플리팅

보통 한 애플리케이션을 여러 페이지로 쪼개 서로다른 URL로 접근할 수 있게 만든다. 이러한 각 페이지는 고유한 앤트리 포인트entry point가 된다.

코드 스플리팅Code-splitting이란 애플리케이션의 번들을 각 엔트리포인트에 맞는 작은 조각chunk로 쪼개는 프로세스이다. 굳이 분할을 하는 이유는 페이지 구동시 필요한 부분만 로드해 애플리케이션의 초기 로딩 시간을 줄이기 위함이다.


Next.js는 코드 스플리팅을 위한 기능이 내장되어 있는데, pages/ 디렉토리에 각 파일들을 넣어두면 빌드하는 단계에서 각 JS파일로 자동 분할된다. 추가로 아래 기능들을 지원한다.

  • 페이지간 공유되는 코드들은 별도의 다른 번들로 분할되는데, 이렇게 하는 이유는 동일 코드가 재다운로드되지 않도록 하기 위함이다.
  • 초기 페이지 로드 후 Next.js는 유저가 둘러볼법한 페이지의 코드를 미리 로드pre-loading할 수 있다
  • 다이나믹 임포트로 초기 로드되는 코드를 직접 분할해줄 수도 있긴하다



6. 빌드 타임 vs 런타임

빌드 타임(or build step)이란 애플리케이션 코드를 프로덕션에 사용할 수 있도록 준비하는 일련의 단계들을 총칭하는 말이다.

애플리케이션을 빌드할 때 Next.js는 코드들을 프로덕션에 최적화된 파일로 변환하는데, 이렇게 함으로써 코드를 서버에 배포하고 유저들이 사용할 수 있게끔 해준다. 이렇게 변환된 파일에는 다음 사항들이 포함된다

  • 정적 페이지용 HTML 파일들
  • 서버에서 페이지를 렌더링하기 위한 JS코드
  • 클라이언트 단에서 페이지를 동적으로 만들기 위한 JS코드
  • CSS 파일들


런타임(or request time)이란, 애플리케이션이 빌드 및 배포된 후 유저의 리퀘스트에 따라 response를 보내는데 소요되는 시간을 말한다. 

 

 

7. 클라이언트와 서버

웹 애플리케이션에서 클라이언트란 유저의 디바이스에서 구동되는 브라우저를 말한다. 이 브라우저는 서버로 리퀘스트를 보내고, 서버로부터 응답을 받으면 유저가 상호작용할 수 있는 인터페이스로 변환한다.

서버는 애플리케이션 코드를 저장하고, 클라이언트로부터 리퀘스트를 받고, 일부 연산을 거쳐 적절한 응답을 보내는 컴퓨터를 말한다.

 

 

8. 렌더링

렌더링이란 리액트 코드를 HTML의 UI로 변환하는 작업을 말한다. 렌더링은 서버나 클라이언트단에서 수행될 수 있으며, 빌드 타임 이전에 수행되거나 런타임의 모든 요청에 대해 일어날 수 있다.

 

Next.js에선 세 가지 타입의 렌더링 메서드를 쓸 수 있는데 각각 아래와 같다.

  • 서버 사이드 렌더링(SSR; Server-Side Rendering)
  • 클라이언트 사이드 렌더링(CSR; Client-Side Rendering)
  • 정적 사이트 생성(SSG; Static Site Generation)

 

8.1 Pre-Rendering

SSR과 SSG는 Pre-Rendering이라고도 불리는데, 그 이유는 데이터 요청과 리액트 컴포넌트의 HTML 변환이 사전에 수행되어 클라이언트 단에 보내지기 때문이다.

 

8.2 CSR vs Pre-Rendering

표준 리액트 앱의 경우 브라우저는 서버로부터 텅 빈 HTML과 함께 UI 생성을 위한 JS덩어리를 받게 된다. 이러한 방식을 CSR이라고 부르는데, 말 그대로 유저의 디바이스에서 초기 렌더링이 일어나기 때문에 붙여진 이름이다.

 

만약 완전히 CSR로 작성된 페이지가 있다면, 유저 입장에선 렌더링이 완료되는 시점까지 빈 화면만 보게 된다. 반면, pre-rendered 앱에선 서버측에서 미리 렌더링이 수행된 후 그 결과물을 유저가 받아보기 때문에 온전한 페이지를 볼 수 있게된다. 

Next.js는 기본적으로 모든 페이지에 대해 pre-render를 수행함

 

NOTE
Next.js 앱에서도 data fetching과 관련하여 useEffect 훅이나 useSWR같은 훅 사용시 CSR방식을 사용할 수 있긴하다.

 

8.3 SSR

SSR에서 페이지의 HTML은 각 요청마다 서버에서 생성된다. 그 후 생성된 HTML과 JSON데이터, 페이지를 동적으로 만들어줄 JS명령어들을 클라이언트 측으로 보내는데, 이 과정에서 클라이언트는 다음 과정을 겪게된다.

  1. 동적인 상태는 아니지만 어쨌든 뭔가 내용이 보이는 HTML을 보게된다. 현 시점에선 버튼같은걸 눌러봤자 아무 반응도 없다.
  2. 그 사이 리액트는 JSON데이터와 JS명령어들을 토대로 컴포넌트를 동적인 상태로 만드는 연산을 수행한다(버튼에 이벤트 핸들러 달기같은) 이 과정을 hydration이라고 한다.
  3. hydration이 끝나면 비로소 동적인 페이지가 완성되고, 버튼같은걸 누르면 뭔가 반응하는 모습을 볼 수 있게된다

 

Next.js에선 getServerSideProps를 통해 SSR페이지를 선택할 수 있다.

 

NOTE
React18과  Next.js 12버전에서 리액트 서버 컴포넌트의 알파 버전이 도입되었다. 서버 컴포넌트는 서버 측에서 완전히 렌더링되므로 렌더링을 위한 클라이언트측 JS가 필요없게 된다.

또한 서버 컴포넌트를 쓰면 개발자는 원하는 로직들을 서버에서만 보관하고, 그 결과만 클라이언트측에 보낼 수 있다. 이렇게 하면 클라이언트로 보내는 번들 사이즈를 줄일 수 있고, CSR 성능을 높일 수 있다. 자세한건 여기에서 확인가능.

 

8.4 SSG

SSG를 쓰면 SSR과 동일하게 HTML이 서버측에서 생성되긴 하지만, 별도의 런타임이 없는 것이 특징이다. 대신, 앱이 배포될 때 빌드 시점에 HTML을 단 한번 생성한 후 CDN에 저장해두는데, 이렇게 함으로써 각 요청에 대해 동일한 HTML을 재사용할 수 있게 된다.

 

Next.js에선 getStaticProps를 사용해서 페이지를 정적으로 생성하도록 선택할 수 있다.

 

여하튼 Next.js의 장점은 본인의 요구사항에 맞춰 페이지 단위로 렌더링 메서드를 선택할 수 있다는 점이다. 즉, 페이지 바이 페이지별로 SSG, SSR, CSR을 선택할 수 있다는 뜻.

NOTE
웹사이트 빌드 후 정적 페이지를 업데이트하거나 생성할 수도 있는데, Incremetal Static Regeneration라는걸 쓰면 된다. 이 말인 즉슨, 데이터가 변경된 후에도 전체 사이트를 rebuild할 필요가 없다는 뜻.

 

 

9. CDN과 Edge

애플리케이션 코드는 네트워크에 배포된 후 어디에서 저장되고 구동되는 것일까?

 

네트워크란 리소스를 공유할 수 있는 컴퓨터(혹은 서버)들이 상호 연결된 것이라고 볼 수 있는데, Next.js 앱의 경우 애플리케이션 코드는 origin server나 CDNsContent Delivery Networks 혹은 the Edge에 배포될 수 있다. 뭔 소린가 싶을텐데 각각이 무엇을 의미하는지 살펴보자.

 

9.1 Origin Servers

이전에 언급한 바 있는데, 서버란 오리지널 버전의 애플리케이션 코드를 저장하고 구동하는 메인 컴퓨터를 일컫는 말이다. 여기서 오리지널 버전이란 말이 중요한데, CDN 서버나 Edge 서버같은 네트워크와 구분하여 오리지널 버전의 코드를 구동하는 서버를 일컫고자 origin server란 용어를 사용한다.

 

origin server는 request를 받으면 특정 연산 후 response를 보내는데, 이 연산된 결과물은 CDN같은 곳으로도 전달될 수 있다.

 

9.2 CDN; Content Delivery Network

CDNs은 오리진 서버와 클라이언트 사이에 위치하는 네트워크로, HTML이나 이미지 파일같은 정적인 컨텐츠를 세계 각지의 장소에 보관한다. 만약 어떤 새로운 요청이 들어오면 유저와 가장 가까운 곳에 위치한 CDN 서버에서 캐시된 결과물을 response로 보내게 된다.

 

이렇게 하면 오리진 서버 입장에선 매 요청마다 새로운 연산을 수행할 필요가 없게되고, 유저 입장에선 지리학적으로 가까운 곳에서 응답을 받게 되므로 빠르게 결과물을 볼 수 있게 된다.

 

Next.js는 pre-rendering방식을 사용하므로 CDN을 사용해 정적인 결과물을 저장하기에 용이하고, 결과적으론 유저에게 빠른 응답이 가능하다.

 

9.3 The Edge

Edge란 유저와 가장 가까운 네트워크의 끝 단(혹은 가장자리)를 일컫는 일반화된 개념이다. 말이 굉장히 두루뭉술한데, CDN도 Edge의 한 종류로 볼 수 있으며 그 이유는 정적 컨텐츠를 네트워크의 엣지(fringe)에 저장하기 때문이다.

 

CDN과 마찬가지로 Edge 서버도 세계 각지에 분산되어 있으나, 정적 컨텐츠만을 저장하는 CDN과 달리 Edge 서버는 작은 코드 조각도 돌릴 수 있다. 즉, 캐싱도 가능하고 코드 구동도 가능하다는 소리.

 

기존에 클라이언트나 서버측에서 수행되던 일부 작업들을 Edge로 옮기면

1) 클라이언트 측으로 보내는 코드량을 줄일 수 있고,

2) 유저의 request중 일부는 오리진 서버까지 돌아갈 필요가 없게 되어 레이턴시(지연시간)를 줄일 수 있게된다.

3) 결과적으로 애플리케이션의 성능 향상이 가능하다

 

Next.js에선 미들웨어를 써서 Edge에서 코드를 구동시킬 수 있으며, 곧 리액트 서버 컴포넌트를 사용해 코드를 실행할 수 있다. Next.js에서의 Edge사용 예제는 여기서 확인 가능.

 

'Next.js > Foundations' 카테고리의 다른 글

[Foundation] From React to Next.js  (0) 2023.04.16
[Foundation] From JS to React  (0) 2023.04.15
[Foundation] Next.js란?  (0) 2023.04.15