모던 리액트 Deep Dive 스터디

11장 - Next.js 13과 리액트 18

려낭 2024. 12. 13. 22:56

11.1 app 디렉터리의 등장

 

Next.js의 아쉬운 점으로 평가받던 것 중 하나가 바로 레이아웃의 존재다.

13버전 이전까지 모든 페이지는 각각의 물리적으로 구별된 파일로 독립돼 있었다.

 

페이지 공동으로 무언가를 집어 넣을 수 있는 곳은  _document 와 _app이 유일하다.

그나마도 이 파일들은 서로 다른 목적을 지니고 있다.

 

이전의 Next.js 12 버전까지는 무언가 페이지 공통 레이아웃을 유지할 수 있는 방법은 _app이 유일했다.

 

그러나 이 방식은 _app에서밖에 할 수 없어 제한적이고, 각 페이지별로 서로 다른 레이아웃을 유지할 수 있는 여지도 부족하다.

 

이러한 레이아웃의 한계를 극복하기 위해 나온 것이 Next.js의 app 레이아웃이다.

 

11.1.1 라우팅

 

기존 /pages로 정의하던 라우팅 방식이 /app 디렉터리로 이동했다는 변화가 있다.

그리고 파일명으로 라우팅하는 것이 불가능해졌다.

Next.js 13에서 라우팅과 페이지를 정의하는 방식이 어떻게 바뀌었는지 살펴보자.

 

라우팅을 정의하는 법

 

Next.js 라우팅은 파일 시스템을 기반으로 하고있다.

새로운 app 기반 라우팅 시스템은 기존 /pages를 사용했던 것과 비슷하지만 약간의 차이가 있다.

  • Next.js 12 이하: /pages/a/b.tsx 또는 /pages/a/b/index.tsx는 모두 동일한 주소로 변환된다.
  • Next.js 13 app: /app/a/b는 /a/b로 변환되며, 파일명은 무시된다. (폴더명까지만 주소로 변환된다.)

즉, Next.js 13의 app 디렉터리 내부의 파일명은 라우팅 명칭에 아무런 영향을 미치지 못한다.

 

layout.js

 

Next.js 13부터는 app 디렉터리 내부의 폴더명이 라우팅 되며, 이 폴더에 포함될 수 있는 파일명은 몇가지로 제한돼 있다.

그 중 하나가 layout.js이다.

 

layout.js는 페이지의 기본적인 레이아웃을 구성하는 요소다.

해당 폴더에 layout이 있다면 그 하위 폴더 및 주소에 모두 영향을 미친다.

 

루트에는 단 하나의 layout을 만들어 둘 수 있고, 모든 페이지에 영향을 미치는 공통 레이아웃이다.

 

페이지 하위에 추가되는 layout은 해당 주소 하위에만 적용된다.

이 layout은 주소별 공통 UI를 포함할 수 있고, 웹페이지를 시작하는 데 필요한 공통 코드를 삽입할 수 있다.

이 공통 코드는 모든 애플리케이션에 영향을 미치지 않고 자신과 자식 라우팅에만 미치게 된다

>>하나의 애플리케이션에서 레이아웃을 더 유연하게 구성할 수 있게 됐다.

 

_document.jsx에서만 처리할 수 있었던 부자연스러움이 사라졌다.

HTML에서 기본으로 제공하는 태그를 추가하고 수정함으로써 별도로 Import하는 번거로움이 사라지고 자연스럽게 코드를 작성할 수 있게 됐다.

 

layout에서 주의해야 할 점

  • layout 은 app 디렉터리 내부에서는 예약어다. 무조건 layout/{js,jsx,ts,tsx}로 사용해야 하며, 레이아웃 이외의 다른 목적으로는 사용할 수 없다.
  • layout은 children을 props로 받아서 렌더링해야 한다. 
  • layout 내부에는 반드시 export default로 내보내는 컴포넌트가 있어야 한다.
  • layout 내부에서도 API 요청과 같은 비동기 작업을 수행할 수 있다.

 

page.js

 

page도 예약어이며, 이전까지 Next.js에서 일반적으로 다뤘던 페이지를 의미한다.

 

page도 규칙을 가지고 있다.

  • page도 역시 app 디렉터리 내부의 예약어다. 무조건 page.{js,jsx,ts,tsx}로 사용해야 하며, 레이아웃 이외의 다른 목적으로는 사용할 수 없다.
  • page도 역시 내부에서 반드시 export default로 내보내는 컴포넌트가 있어야 한다.

 

error.js

 

해당 라우팅 영역에서 사용하는 공통 에러 컴포넌트다.

특정 라우팅별로 서로 다른 에러 UI를 렌더링 할 수 있다.

 

  • 에러 정보를 담고 있는 error:Error 객체와 에러 바운더리를 초기화할 reset:() => void를 props로 받는다.
  • 에러 바운더리는 클라이언트에서만 작동하므로 error 컴포넌트도 클라이언트 컴포넌트여야 한다.
  • 같은 수준의 layout에서 에러가 발생할 경우 해당 error컴포넌트로 이동하지 않는다.

 

not-found.js

 

특정 라우팅 하위의 주소를 찾을 수 없는 404 페이지를 렌더링할 때 사용된다.

 

전체 애플리케이션에서 404를 노출하고 싶다면 app/not-found.js 를 생성해 사용하면 된다. 

이 컴포넌트는 서버 컴포넌트로 구성하면 된다.

 

loading.js

 

리액트 Suspense를 기반으로 해당 컴포넌트가 불러오는 중임을 나타낼 때 사용할 수 있다.

 

"use client" 지시자를 사용해 클라이언트에서 렌더링되게 할 수도 있다.

 

route.js

 

/pages/api 와 동일하게 /app/api를 기준으로 디렉터리 라우팅을 지원하며, 

파일명에 대한 라우팅이 없어진 것과 마찬가지로 /api 에 대해서도 파일명 라우팅이 없어졌다.

 

그 대신 디렉터리가 라우팅 주소를 담당하며 파일명은 route.js로 통일됐다.

 

route.ts가 존재하는 폴더 내부에는 page.tsx가 존재할 수 없다.

두 파일이 공존한다면 경고 메세지를 보게 된다.

 

route의 함수들이 받을 수 있는 파라미터는 다음과 같다.

  • request: NextRequest 객체이며, fetch의 Request를 확장한 Next.js 만의 Request라고 보면 된다. 이 객체에서는 API요청과 관련된 cookie,headers 뿐만이 아니라 nextUrl같은 주소 객체도 확인할 수 있다.
  • context: params만을 가지고 있는 객체이며, 이 객체는 앞서 파일 기반 라우팅에서 언급한 것과 동일한 동적 라우팅 파라미터 객체가 포함돼 있다. 이 객체는 Next.js에서 별도 인터페이스를 제공하지 않으므로 주소의 필요에 따라 원하는 형식으로 선언하면 된다.

 

11.2 리액트 서버 컴포넌트

 

리액트 서버 컴포넌트에 대한 이해와 기존 구조의 한계를 짚어보자

11.2.1 기존 리액트 컴포넌트와 서버 사이드 렌더링의 한계

 

클라이언트 사이드 렌더링 (CSR)

 

기존 리액트의 모든 컴포넌트는 클라이언트에서 작동하며, 브라우저에서 자바스크립트 코드 처리가 이뤄진다.

동작 과정:

  1. 코드 다운로드
  2. 리액트 컴포넌트 트리 생성
  3. DOM에 렌더링

서버 사이드 렌더링 (SSR)

 

서버에서 미리 DOM을 만들어 오고, 이 DOM을 기준으로 클라이언트에서 하이드레이션이 진행된다.


기존 SSR의 단점

  1. 자바스크립트 번들 크기가 0인 컴포넌트를 만들 수 없다.
  2. 백엔드 리소스에 대한 직접 접근이 불가능하다.
  3. 자동 코드 분할이 어렵다.
  4. 연쇄적으로 발생하는 클라이언트와 서버 요청에 대응하기 어렵다.
  5. 추상화에 드는 비용이 증가한다.

이 모든 문제는 리액트가 클라이언트 중심으로 돌아가기 때문에 발생한다.

 

리액트 서버 컴포넌트서버 사이드 렌더링클라이언트 사이드 렌더링의 장점을 모두 취하고자 등장했다.

 


11.2.2 서버 컴포넌트란?

 

하나의 언어, 프레임워크, 그리고 API와 개념을 사용하면서 서버와 클라이언트 모두에서 컴포넌트를 렌더링할 수 있는 기법이다.

 

서버에서 할 수 있는 일은 서버가 처리하고, 나머지 작업은 클라이언트에서 수행한다.


리액트 컴포넌트 트리 구조

 

서버 컴포넌트

  • 요청이 올 때 서버에서 단 한 번만 실행되므로 상태를 가질 수 없다.
  • 한 번 렌더링되면 끝이므로 렌더링 생명주기를 사용할 수 없다.
  • DOM API, window, document에 접근할 수 없다.
  • 서버 컴포넌트, 클라이언트 컴포넌트, HTML 요소 렌더링 가능하다.

클라이언트 컴포넌트

  • 브라우저 환경에서만 실행되므로 서버 컴포넌트를 가져올 수 없다.
  • 하지만 클라이언트 컴포넌트의 자식으로 서버 컴포넌트를 넣는 구조는 가능하다.
  • 일반적인 리액트 컴포넌트와 같다.

공통 컴포넌트

  • 서버와 클라이언트 모두에서 사용할 수 있다.
  • 서버 컴포넌트와 클라이언트 컴포넌트의 모든 제약을 받는다.

리액트는 모든 컴포넌트를 공통 컴포넌트로 판단하며, 클라이언트 컴포넌트를 명시하려면 파일 상단에 'use client'를 선언해야 한다.

 

 

11.2.3 서버 사이드 렌더링과 서버 컴포넌트의 차이
  • 서버 사이드 렌더링 (SSR):
    서버에서 페이지 전체를 HTML로 렌더링하고 클라이언트에 내려준다.
    이후 클라이언트에서 하이드레이션을 통해 이벤트를 붙이는 등의 작업을 수행한다.
  • 서버 컴포넌트:
    서버에서 렌더링 가능한 컴포넌트는 완성된 형태로 제공되며, 클라이언트 컴포넌트는 하이드레이션을 통해 동작한다.

결국, 서버 컴포넌트와 서버 사이드 렌더링을 결합하면 초기 로딩 속도는 물론 브라우저에서 내려받아야 하는 자바스크립트 양도 줄어든다.

 

11.3 Next.js에서의 리액트 서버 컴포넌트

/app 디렉터리

Next.js 13버전부터 서버 컴포넌트를 도입했으며, 서버 컴포넌트는 /app 디렉터리에 구현되어 있다.

  • page.js와 layout.js: 기본적으로 서버 컴포넌트로 동작한다.
11.3.1 새로운 fetch 도입 과 getServerSideProps, getStaticProps, getInitialProps의 삭제
  • getServerSideProps, getStaticProps, getInitialProps는 삭제되었다.
  • 모든 데이터 요청은 웹 표준 API인 fetch를 기반으로 처리된다. 

 

11.3.2 정적 렌더링과 동적 렌더링
  1. 정적 라우팅: 빌드 타임에 렌더링을 미리 해두고 캐싱해 재사용할 수 있다.
  2. 동적 라우팅: 요청이 올 때마다 서버에서 렌더링이 진행된다.

 

11.3.3 캐시, Mutating, Revalidating

 

fetch를 통해 데이터의 유효 시간을 설정하면 **재검증(revalidate)**이 가능하다.

 

11.3.4 스트리밍을 활용한 점진적 페이지 불러오기

 

과거 SSR은 페이지가 모두 완성될 때까지 빈 화면을 보여주는 문제가 있었다.
이를 해결하기 위해 HTML을 작은 단위로 쪼개어 클라이언트로 점진적으로 전송하는 스트리밍이 도입되었다.

 

결과: 데이터가 로드되는 컴포넌트를 먼저 보여줘 사용자의 경험을 개선한다.

 

 

리액트 서버 컴포넌트의 등장으로 클라이언트와 서버의 역할을 명확하게 나누어, 리소스를 최적화하고 개발자 경험과 사용자 경험 모두를 개선할 수 있게 되었다.