본 포스팅은 인프런 강의
<한 입 크기로 잘라먹는 Next.js(15 +) - 이정환 Winterlood>
의 내용을 기반으로 작성합니다 ✍
App Router
Page Router만 존재했던 전과 달리 Next.js 13 버전에서 완전히 새롭게 추가된 라우팅 방식 App Router
이 변화로 인해 여러 가지 기능들의 변화가 생기거나 새로운 기능이 많이 추가됐다.
변경되거나 추가되는 사항 | 크게 변경되지 않은 사항 |
- 페이지 라우터 설정 방식 변경 - 레이아웃 설정 방식 변경 - 데이터 패칭 방식 변경 - React 18부터 새롭게 추가된 신규 기능 사용 가능 - React Server Component - Streaming |
- 네비게이팅 (Navigation) - 프리패칭 (Pre-Fetching) - 사전 렌더링 (Pre-Rendering) |
파일 구조 대략적으로 확인하기
Next.js 설치 시, App Router 사용을 선택한 경우 만들어지는 폴더 구조를 가볍게 확인해 보자.
- /app : Page Router의 경우에는 pages 폴더가 생겼지만 이번에는 app 폴더가 생성된다
- page.tsx : 페이지 역할을 하는 파일
- layout.tsx : 페이지의 레이아웃을 설정하는 파일
페이지 라우팅 설정하기
Page Router의 페이지 라우팅
- /pages 기반
- /pages 아래에 생성한 파일들의 이름이 곧 경로가 된다
App Router의 페이지 라우팅
- /app 기반
- 무조건 "page"라는 이름을 갖는 파일만 페이지 파일로 취급된다
- 동적 경로(Dynamic Routes) 만들기 : 대괄호로 파라미터를 감싼 폴더를 만들고 그 안에 page 파일을 생성한다
동적 경로 생성 방식
Catch-All Segment
- 특정 경로의 하위 경로도 모두 처리할 수 있게 해주는 기능
- 대괄호와 파라미터 사이에 ... 를 추가해서 사용
- ../[..id]/page.tsx
Optional Catch-All Segment
- Catch-All Segement와 비슷하지만 경로가 없을 경우에도 처리할 수 있게 해주는 기능
- 대괄호와 파라미터 사이에 ... 추가 + 대괄호를 두개 사용
- ../[[...id]]/page.tsx
예시를 통한 비교)
/book/[id]/page.tsx | /book/[...id]/page.tsx | /book/[[...id]]/page.tsx | |
/book | 404 Not Found | 404 Not Found | id = [] |
/book/34 | id = ['34'] | id = ['34'] | id = ['34'] |
/book/12/3/45/6 | 404 Not Found | id = ['12', '3', '45', '6'] | id = ['12', '3', '45', '6'] |
레이아웃 설정하기
페이지의 경우 "page"란 이름을 가진 파일만 페이지 역할을 할 수 있는 것과 동일하게
레이아웃 역시 "layout"이란 이름을 가진 파일만 레이아웃 역할을 할 수 있다.
그리고 레이아웃은 하위 경로에 위치한 모든 페이지 컴포넌트에 적용된다.
만일 하위 경로에 새로운 레이아웃 파일을 생성하면 레이아웃이 중첩 적용된다.
Route Group
특정 페이지에만 공통 레이아웃을 적용하고 싶을 때 사용하기 위한 수단이 되는 Route Group
소괄호로 감싸서 이름을 지정한 폴더로, 경로 상에는 아무런 영향을 미치지 않는다.
때문에 Route Group 안에 layout 파일을 작성하면 해당 페이지들에 레이아웃을 동일하게 적용할 수 있다.
예시) /(with-searchbar)/layout.tsx가 적용되는 범위는?
/ | /search | /book/300 |
O | O | X |
React Server Component
React v18 부터 새롭게 추가된 새로운 유형의 컴포넌트
기존 컴포넌트들과는 달리 오직 서버 측에서만 실행되는 컴포넌트로 브라우저 측에서는 아예 실행되지 않는다.
서버 컴포넌트를 이해하기에 앞서 서버 컴포넌트가 왜 필요하게 됐는지부터 하나씩 짚어보자.
React.js vs Next.js 데이터 패칭 방식 비교
React App에서의 데이터 패칭 과정은 다음과 같다.
1. 페이지 역할을 하는 컴포넌트에 데이터를 보관할 state를 생성
2. 데이터 패칭 함수 생성
3. useEffect를 활용하여 컴포넌트 마운트 시점에 데이터 패칭 함수 호출
4. 데이터 로딩 중일 때의 예외 처리
이는 초기 로딩이 느리다는 단점을 갖는데, 백엔드 서버로 보내는 데이터 패칭 함수 호출을 마운트 이후에 시작하기 때문에 FCP(First Contentful Paint) 자체도 늦으며, 이후에 로딩까지 기다리기 때문이다.
이 단점을 보완한 것이 Next App의 데이터 패칭 방식이다.
데이터 패칭을 사전 렌더링 중에 발생 시킬 수 있기 때문에, 데이터 요청 시점이 매우 빨라진다는 장점을 갖는다.
사전 렌더링 과정 중 JS Bundle
위 Next App의 데이터 패칭 과정 중 JS Bundle은
사전 렌더링 과정 중 화면에 상호작용을 추가하기 위해서(= 하이드레이션을 하기 위해서)
작성한 모든 컴포넌트를 자바스크립트 번들로 묶어서 브라우저에게 후속으로 전달하는 것이다.
이 자바스크립트 번들에 포함된 컴포넌트들은 결국 브라우저 측에서 하이드레이션을 위해 한 번 더 실행된다.
그런데 여기에서 JS Bundle에 모든 컴포넌트를 포함할 필요가 있는가?
useEffect나 useState 등의 Hook이 있거나 이벤트 핸들러가 있어 하이드레이션을 꼭 해야하는 컴포넌트가 아니라면
즉, 상호작용이 없는 정적인 컴포넌트라면 굳이 브라우저 측에서 한 번 더 실행할 이유가 없다.
기존 Page Router 버전에서는 페이지에 포함되어 있기만 하면 그대로 번들로 묶어 브라우저에 전달했기 때문에 어쩔 수 없었고, 때문에 번들의 용량이 불필요하게 커지며 번들을 불러오는 시간, 하이드레이션을 진행하는 시간까지 덩달아 늘어나버렸고 최종적으로 TTI(Time to Interact)까지 늦어져 버린다는 문제를 가졌다.
이러한 문제 해결을 위해
JS Bundle을 구성할 때 상호작용이 있는 컴포넌트만 포함하도록
클라이언트에 상호작용이 없는 컴포넌트가 전달되지 않도록
서버에서만 실행되는 컴포넌트, 서버 컴포넌트라는 유형을 따로 분리하게 됐다.
클라이언트 컴포넌트 vs 서버 컴포넌트
클라이언트 컴포넌트 | 서버 컴포넌트 |
- 기존부터 지금까지 써오고 있던 클래식한 컴포넌트 - 서버 컴포넌트의 등장부터는 서버 컴포넌트로 분류되지 못한 나머지 일반적인 컴포넌트를 칭한다. |
|
- 서버와 브라우저 모두에서 한 번씩 실행되어야 하는 컴포넌트 - 상호작용이 있는, 하이드레이션이 필요한 컴포넌트 |
- 서버 측에서만 실행되는 컴포넌트 - 상호작용이 없는, 하이드레이션이 필요 없는 컴포넌트 |
서버 측에서 사전 렌더링을 진행할 때 한 번, 자바스크립트 번들로써 하이드레이션을 진행할 때 한 번, 총 2 번 실행 |
서버 측에서 사전 렌더링을 진행할 때 딱 1 번만 실행 |
"use client"; | 기본 값 |
💡 Next.js의 공식문서는 페이지의 대부분을 서버 컴포넌트로 구성할 것을 권장한다.
클라이언트 컴포넌트는 꼭 필요한 경우에만 사용하도록 하는 것이 좋다.
서버 컴포넌트에서 가능한 작업
서버 컴포넌트 내에서 작성하는 코드들은 브라우저 측에서 실행되지 않는다는 점을 생각해 다음과 같은 작업을 할 수 있다.
- 보안에 민감한 작업 : OK
- 데이터 패칭과 같은 기능 추가 작업 : OK
- 브라우저에서만 할 수 있는 작업 (ex. React Hooks 호출) : NOPE 😑
서버 / 클라이언트 컴포넌트 사용 시 주의 사항
(1) 서버 컴포넌트에서는 브라우저에서 실행된 코드를 포함하면 안된다.
브라우저 측에서 동작하는 기능들을 사용하지 않도록 해야 한다.
- React Hooks (ex. useState, useEffect)
- 이벤트 핸들러 (ex. onClick)
- 브라우저에서 실행되는 기능을 담고 있는 라이브러리
(2) 클라이언트 컴포넌트는 클라이언트에서만 실행되지 않는다.
클라이언트와 서버, 두 곳 모두에서 실행됨을 유의해야 한다.
(3) 클라이언트 컴포넌트에서 서버 컴포넌트를 import 할 수 없다.
서버 컴포넌트들은 JS 번들로부터 제외됐기 때문에 애초에 브라우저에 전달되지 않는다.
즉, 없는 코드로 치기 때문에 의도치 않은 동작을 발생시키거나 오류가 발생할 가능성이 생긴다.
만일, 클라이언트 컴포넌트가 하위에 서버 컴포넌트를 가진다면?
Next.js는 위의 가능성 자체를 없애기 위해 자동으로 하위 서버 컴포넌트를 클라이언트 컴포넌트로 변경해버린다.
문제는 이렇게 된다면 불필요하게 클라이언트 컴포넌트 수를 늘리는 것이며, JS 번들의 용량을 크게 만들어 성능 저하까지 이어지게 된다는 점이다. 권장하지 않는다.
때문에 만일 꼭 서버 컴포넌트를 클라이언트 컴포넌트의 하위로 두어야 한다면?
서버 컴포넌트를 children props로 받아서 렌더링 시켜주는 방법을 사용해야 한다.
(4) 서버 컴포넌트에서 클라이언트 컴포넌트에게 직렬화되지 않는 Props는 전달 불가하다.
직렬화(Serialization)란? 객체, 배열, 클래스 등의 복잡한 데이터 구조의 데이터를 네트워크 상으로 전송하기 위해 아주 단순한 형태(문자열, Byte)로 변환하는 것을 말한다.
사전 렌더링 과정을 진행할 때, 서버 컴포넌트들은 RSC Payload라는 형태로 먼저 직렬화되는 중간 과정을 거친다.
만일 이 과정에서 특정 서버 컴포넌트가 자신의 자식인 클라이언트 컴포넌트에게 props를 전달한다면?
이 props 역시 함께 직렬화되어 RSC Payload 형태가 되어야 한다.
결론적으로 이 RSC Payload의 결과와 클라이언트 컴포넌트 실행 결과가 합쳐져서 HTML 페이지 생성을 완료한다.
Navigating
App Router 버전의 앱과 Page Router 버전의 앱은 모두
초기 접속 요청 이후 발생하는 페이지 이동들을 클라이언트 사이드 렌더링이라는 방식으로 처리하게 된다.
하지만 과정에서 조금 차이가 있다.
Page Router 버전 앱은 server로부터 이동할 페이지에 대한 모든 컴포넌트들이 포함된 JS Bundle 파일을 브라우저에게 전달하고, 브라우저가 이를 직접 실행해 현재 페이지에 맞도록 컴포넌트를 교체 한다.
App Router 버전 앱은 Server Component가 추가되어 방식이 조금 변경된다. 큰 틀은 거의 동일하다.
페이지 이동이나 프리패칭 시에 server가 브라우저에 JS Bundle 파일 뿐만 아니라 RSC Payload를 함께 전달한다.
앞 챕터에서 언급했듯이 JS Bundle이 클라이언트 컴포넌트만 포함하고 있기 때문에 서버 컴포넌트를 실행한 결과물인 RSC Payload를 함께 전달하는 것이다.
브라우저에서는 이렇게 받은 JS Bundle을 실행하고 RSC Payload와 합쳐 페이지를 적절히 교체하게 된다.
정리하자면,
server가 client(browser)로부터 현재 이동할 페이지에 대한 데이터를 받아야 하는데 그 형태에서 차이를 갖는다.
Page Router 버전 앱 | App Router 버전 앱 |
server가 client에 JS Bundle (모든 컴포넌트) 전달 |
server가 client에 JS Bundle (클라이언트 컴포넌트) + RSC Payload (서버 컴포넌트의 결과물) 전달 |
client에서 JS Bundle 실행 (모든 컴포넌트의 결과물 생성) |
client에서 JS Bundle 실행 (클라이언트 컴포넌트의 결과물 생성) |
결과물에 맞게 교체 | 결과물에 맞게 교체 |
'NEXT' 카테고리의 다른 글
[NEXT] App Router의 데이터 캐시에 대해 정리하며 이해하기 (0) | 2024.10.30 |
---|---|
[NEXT][SUPABASE] 수파베이스를 이용해서 RESTful API 만들고 사용하기 (2) | 2024.07.07 |
[NEXT] React.js와 Next.js 중 무엇을 고를까? CSR와 SSR (0) | 2024.01.20 |