[Next JS] Layouts RFC , React 18에 최적화된 라우팅 방식 공개
Offscreen Stashing with Instant Back/ForwardLayouts RFC
올해 3월 말, 리액트 v18.0 이 공개가 되었죠, 이제 Concurrent mode, Suspense, RSC 등을 탑재한 리액트 18의 도입을 적극적으로 고려해볼 만한 시간이 얼마 안남은 것 같습니다. (https://reactjs.org/blog/2022/03/29/react-v18.html)
Next JS에서도 적극적으로 리액트 v18 의 변경사항들을 적용중임을 알려왔었고 이번에는 새롭게 변경되는 layout, 그리고 라우팅 방식을 공개했습니다 !
이 포스팅의 내용들은 99% 이상이 아래 링크된 Next JS의 공식 블로그 내용을 바탕으로 하고 있을 테니, 영어에 자신 있는 분들은 그냥 블로그 글 읽는게 더 도움이 되실 수도 있습니다 😄
22년 5월말 Next JS는 Layouts RFC 라는 제목으로 새롭게 개편되는 레이아웃, 라우팅 방식을 공개했는데요,
우선 1차적으로 새롭게 바뀌는 라우팅 시스템과, 그것이 어떻게 리액트 서버 컴퍼넌트와 데이터 페칭 방법과 연관되는지에 대해 공개했습니다. 이후 올라오는 2차 게시글은 실제 예시와 컨벤션, 그리고 이 구조가 어떻게 Suspense와 부분적인 hydration을 이용하고 있는지에 대해 다룰 예정이라네요.
현재 Next JS 는?
현재 라우팅 구조는 위 사진처럼 되어있죠?
pages 를 최상위 디렉터리로 가지고, 그 아래에 만들어지는 파일들의 이름으로 url이 라우팅이 됩니다.
파일 이름을 [param].js, 나 [...param].js 로 가져가서 동적인 라우팅을 사용할 수도 있죠.
뿐만 아니라 레이아웃 패턴을 제공하기도 합니다.
그리고 getServerSideProps, getStaticProps 등의 함수를 컴퍼넌트에 주입시켜줌으로서, 서버 단의 데이터 fetching을 컨트롤 할 수도 있죠. 다만, 이는 pages 루트 레벨에서만 사용할 수 있었습니다!
12.1.0 버전에서는 On-demain Revalidation 을 바탕으로한 ISR 구현도 가능하다고 하더군요~
새롭게 바뀌는 루트 디렉토리 'app'
새롭게 바뀌는 폴더구조를 실행해볼 수 있는 'app' 디렉토리가 생긴다고 합니다.
이전 버전을 지원해야하기 때문에 app 디렉토리는 pages 디렉토리와 병행해 사용할 수 있다고 합니다!
(헌데, app 하위와 pages 하위에 두개의 동일한 이름을 가진 파일이 있다면 어떤게 우선순위로 동작할지는 모르겠군요..?)
app도 pages 와 마찬가지로 Root - Leaf 를 기반으로 하는 트리 구조로 동작하는군요.
하지만 조금 다른 점이 있습니다.
기존 pages 기반의 파일들은 라우팅과 직접적으로 연관이 되어있어야 했기 때문에 컴퍼넌트를 반드시 export 하고있어야 했습니다. 그렇기 때문에, pageExtensions config 없이는 pages 하위에 페이지와 관련없는 파일들은 위치시킬 수 없었죠. (저는 굳이 pages 하위에 페이징 관련없는 파일을 둬볼일이 없어서 잘 몰랐네요.)
하지만 새로운 route 구조에서는 jsx가 아닌 다른 프로젝트 파일들 (UI 관련, 테스트파일, 스토리) 또한 배치할 수가 있다고 합니다.
Layouts
layout.js (layout.ts) 이라는 새로운 파일 컨벤션이 나왔습니다!
레이아웃은 route 하위에 layout.js 파일로 사용할 수 있습니다.
// Root layout (app/layout.js)
// - Applies to all routes
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header />
{children}
<Footer />
</body>
</html>
)
}
레이아웃 파일의 예시인데요, 매 화면마다 공통되게 보여주게될 화면 요소들을 이 RootLayout 컴퍼넌트에 작성하고, children 으로 하위 컴퍼넌트를 받아서 내려줍니다. 따라서, children 은 props에 반드시 포함되어있어야 합니다.
이전까지는 일반적으로 _app.js 하위에 이런 파일을 위치시켰죠. 이제는 layout.js 가 그 역할을 대신해주게 됩니다!
layout 파일은 컴퍼넌트를 담고있지만 URL path 에 적용되는 파일이 아니고, 리렌더링 되지도 않는다고 합니다.
레이아웃에는 두가지 타입의 레이아웃이 있습니다.
- Root layout: 모든 라우트에 적용되는 레이아웃
- Regular layout: 특정 라우트에서만 적용되는 레이아웃
위 예시는 Root layout 이며, 모든 라우트에 적용이 됩니다. 한마디로, 모든 페이지에 보여지게 될 요소들을 담고있는 레이아웃이라고 생각하면 됩니다.
따라서, _app.js 이나 _document.js 를 대신할 수 있습니다!
(그리고 레이아웃에서는 data fetching 작업들도 할 수 있습니다.)
Regular layout
그렇다면 regular layout 은 무엇을 의미할까요?
regular layout 이란, 하위 디렉토리들에 위치하는 layout.js 를 의미합니다.
위 예시를 보면 루트 레이아웃인 app/layout.js 가 있고, 그 하위에 레귤러 레이아웃은 app/dashboard/layout.js 가 있죠?
그렇다면 만약에 내가 dashboard 하위에 있는 페이지에 접근할 떄, app/layout.js 과 app/dashboard/layout.js 모두에 감싸진 컴퍼넌트를 보게되는 것입니다.
그림으로 보면 이해하기가 더 편할 것 같습니다.
Page
page.js 이라는 파일 컨벤션 또한 생겼습니다.
기존에는 index.js 가 곧 해당 페이지의 메인 컴퍼넌트를 의미했죠.
이제는 라우트 경로에 index.js 대신 page.js 를 사용한다고 보시면 됩니다.
Next JS 에서는 위 layout.js 와 연계해서 예시를 들어주고 있습니다.
settings/page.js 에서 export 하는 컴퍼넌트는 app/layout.js 과 app/dashboard/layout.js 에 감싸져서 사용자 화면에 보여지게 되겠죠?
Layout과 Page
한번 정리해서 생각해본다면,
- page.js 와 layout.js 의 파일 확장자로는 js | jsx | ts | tsx 를 사용할 수 있다.
- 화면에 그려질 개별 Page 컴퍼넌트는 page.js 에서 export 한다. (기존의 디렉토리/index.js 의 역할)
- Layout용 컴퍼넌트는 layout.js 에서 export 한다.
- Layout용 컴퍼넌트는 반드시 childern props를 가지고 있어야 한다.
// Root layout (app/layout.js)
// - Applies to all routes
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header />
{children}
<Footer />
</body>
</html>
)
}
// Regular layout (app/dashboard/layout.js)
// - Applies to route segments in app/dashboard/*
export default function DashboardLayout({ children }) {
return (
<>
<DashboardSidebar />
{children}
</>
)
}
// Page Component (app/dashboard/analytics/page.js)
// - The UI for the `app/dashboard/analytics` segment
// - Matches the `acme.com/dashboard/analytics` URL path
export default function AnalyticsPage() {
return (
<main>...</main>
)
}
위와 같은 예시는 실제로 아래처럼 렌더링 된다고 보시면 됩니다.
<RootLayout>
<Header />
<DashboardLayout>
<DahboardSidebar />
<AnalyticsPage>
<main>...</main>
</AnalyticsPage>
</DashboardLayout>
<Footer />
</RootLayout>
React Sever Component (RSC)
이 부분은 React Sever Component(RSC) 의 기본적인 개념을 알고 있다고 가정하고 간다고 합니다. RSC 에 대해 잘 모르시는 분은 RSC 문서를 읽고 오시길!
(아니면 부족하지만 제가 정리한 RSC 게시글을 읽으셔도 됩니다..😊)
https://programming119.tistory.com/252
위 변경된 라우팅 구조들은 RSC 를 고려해서 설계되었습니다.
'app' 하위의 파일들은 기본적으로 React Server Components로 동작하며, 서버 사이드에서 렌더링 됩니다!
지금까지 결정된 바로는 리액트 RSC 의 기본 컨벤션(*.client.js, *.server.js) 대로 동작하게 하려고 하는 것 같습니다. 이 discussion에서 현재까지 게속 논의중이라고 하네요.
다시말하자면, client 컴퍼넌트, server 컴퍼넌트 모두를 app 하위에서 선언할 수 있다는 뜻이죠!
이것이 기존의 'pages' 기반의 라우트 구조와의 가장 큰 차이점이고, 더이상 getStaticProps, getServerSideProps 를 사용할 필요가 없어질 것 같습니다.
하지만, 렌더링 환경은 반드시 data fetching 메소드와 분리되어야 하고 컴퍼넌트 단계에서 설정되어야 합니다. RSC 의 제약조건 들을 지켜야 된다고 하네요.
Client Component 와 Server Component
Server Component 는 server 관련 로직들만 들어가야 하죠, (데이터베이스 작업이라던가, 파일시스템 작업이라던가..) 그래서 아래와 같이 Client Component 에서 Server Component를 import 한 경우는 동작하지 않습니다.
import ServerComponent from './ServerComponent.js';
export default function ClientComponent() {
return (
<>
<ServerComponent />
</>
);
}
다만, Client Component는 Server Component 를 children 으로 받을 수 있습니다. 흔히 컴퍼넌트 합성(Component composition)이라고 하죠. 왜 이런 구조로는 가능할까요? 아래 예시를 보면 이해가 될겁니다.
// ClientComponent.js
export default function ClientComponent({ children }) {
return (
<>
<h1>Client Component</h1>
{children}
</>
);
}
// ServerComponent.js
export default function ServerComponent() {
return (
<>
<h1>Server Component</h1>
</>
);
}
// page.js
// It's possible to import Client and Server components inside Server Components
// because this component is rendered on the server
import ClientComponent from "./ClientComponent.js";
import ServerComponent from "./ServerComponent.js";
export default function ServerComponentPage() {
return (
<>
<ClientComponent>
<ServerComponent />
</ClientComponent>
</>
);
}
Server 파일 (페이지) 에서 Client Component 를 임포트하고, 그 Client Component 의 childeren 인, ServerComponent를 내려줍니다. 그러면 결국 Server 파일에서, Server Component 를 import 하는 것이기 때문에, 이러한 구조가 가능해지는 것이죠. ⭐
이 부분을 왜 설명하고 있을까요? 앞서 보여드렸던 layout.js 컨벤션 기억나시죠? layout.js 가 children을 반드시 props 로 받아야하는 이유가 바로 이 것 때문입니다. 아래 예시를 보면 더욱 이해가 될것입니다.
// The Dashboard Layout is a Client Component
// app/dashboard/layout.js
export default function ClientLayout({ children }) {
// Can use useState / useEffect here
return (
<>
<h1>Layout</h1>
{children}
</>
);
}
// The Page is a Server Component that will be passed to Dashboard Layout
// app/dashboard/settings/page.js
export default function ServerPage() {
return (
<>
<h1>Page</h1>
</>
);
}
데이터 Fetching
layout.js 에서도 데이터 fetching 메소드를 사용할 수 있다고 합니다. layout 들은 중첩될 수 있기 때문에, 다시 말하자면 여러 라우트에서 데이터를 fetching 하는 것 또한 가능하다는 뜻입니다.
이것 또한 pages 기반의 라우트 구조와 다른 점이죠? pages 구조에서는 항상 루트에 있는 페이지 파일만 데이터 fetching이 가능했으니까요.
// app/blog/layout.js
export async function getStaticProps() {
const categories = await getCategoriesFromCMS();
return {
props: { categories },
};
}
export default function BlogLayout({ categories, children }) {
return (
<>
<BlogSidebar categories={categories} />
{children}
</>
);
}
뿐만 아니라, layout 이 children으로 받게 될 page 에서 또한 데이터를 fetch 할 수 있다고 하는군요!
// app/blog/[slug]/page.js
export async function getStaticPaths() {
const posts = await getPostSlugsFromCMS();
return {
paths: posts.map((post) => ({
params: { slug: post.slug },
})),
};
}
export async function getStaticProps({ params }) {
const post = await getPostFromCMS(params.slug);
return {
props: { post },
};
}
export default function BlogPostPage({ post }) {
return <Post post={post} />;
}
app/blog/layout.js와 app/blog/[slug]/page.js 모두 getStaticProps를 사용하기 때문에 Next.js는 빌드 시 전체 /blog/[slug] 경로를 React Server Components로 정적으로 생성하므로 ,클라이언트 측으로 보내지는 JavaScript 용량이 줄고, 더욱 빠른 hydration이 가능해집니다.
더 나아가 클라이언트 탐색에서 캐시(서버 컴퍼넌트 데이터의 스냅샷)를 재사용하기에 다시 작업이 필요하지 않고, 그렇기에 CPU 시간이 줄어들게 된다고 합니다!
그리고 Data Fetching Methods (getServerSideProps 와 getStaticProps) 는 app 폴더 하위의 Server Components 에서만 사용 가능하다고 합니다.
Data fetching and rendering with React Server Components
사실 이 부분이 핵심인 것 같아서, 제목을 next js 블로그에 기재된 그대로 가져왔습니다.
Next JS 가 왜 이런 식으로 라우트 구조를 바꿔가는지, 어떻게 React 의 신규 feature들과 상호작용할 것인지에 대한 내용이거든요.
병렬적인 Fetching (Parallel fetching)
'병렬성을 통해 hydration 시간을 줄인다.' 리액트 18, concurrent mode 의 핵심 feature 중의 하나이죠?
위 라우팅 구조를 통해서 레이아웃 컴퍼넌트들의 data fetching들은 병렬적이게 이루어질 것이라고 합니다. 이는 기존의 sequential 한 구조보다 시간을 줄여주겠죠?
병렬적인 Data-fetching들이 실행되고, 이 과정이 끝나면, 해당 데이터에 dependant한 렌더링(Rendering)이 시작되게 됩니다. 뿐만 아니라 이후 Suspense 가 도입된다면, data가 전부 로드되지 않았다고 하더라도 렌더링이 동시에 수행되는 것도 기대할 수 있겠네요.
부분적인 fetching과 rendering (Partial fetching and rendering)
또한 매우매우 중요한 feature 가 소개가 되는데요,
이제는 layout을 공유하는 하위 페이지들이 layout의 data-fetching 과정들까지 공유하기 때문에, 페이지를 이동한다고 해도, 같은 layout을 공유하고 있다면 불필요한 refetch나 rerendering이 필요 없어진다는 점입니다!
이런 특별한 feature은 React Sever Components 에서 유용합니다. 제가 정확히 이해한지는 모르겠는데, 페이지의 변경된 부분만 렌더링하는 것이 RSC 와 함께하기 때문에 가능하다고 말하는 것 같습니다. 이 방식을 통해 전송되는 데이터 양은 물론 실행 시간도 줄어들 것을 기대할 수 있다고 합니다!
위 사진처럼, 같은 layout을 공유하는 페이지 (settings page, analytics page) 들 사이를 navigating 한다고 하더라도, 전체 리페칭과 리렌더링이 발생하지는 않습니다. Header, DashboardSidebar, Footer 는 그대로 가져갈 수 있죠.
다음 RFC는?
이번 Next JS RFC에서는 layout, routing, 그리고 data fetching 측면에서 새롭게 변화되는 부분에 대해 다뤘고, 다음 RFC에서는 어떤 내용을 다룰 것인지에 대해 간단하게 정리가 되어있습니다.
- Instant Loading States: Instant Loading States, 한국 말로 직역하면 (즉시 로딩되는 상태) 정도가 되겠는데요, 이게 무슨말이냐 하면, 이제 라우팅을 통해서 일정 부분만 바뀌다보면 페이지가 네비게이팅되는 과정 에서 특정 컴퍼넌트가 data fetching이 일어나면서 잠시 멈추거나 화면이 안보이는 경우가 있겠죠? 그럴 때를 대비해서 일반적으로 로딩 UI를 쓰곤 하는데, 이런 로딩 UI에 대한 지원을 Next JS 프레임워크 적으로 지원할 것이라고 합니다. 매우 흥미롭네요 ! 😮
- Offscreen Stashing with Instant Back/Forward: 리액트에서는 <Offscreen/>이라는 새로운 컴퍼넌트를 추가할 계획이라고 합니다. 이 Offsrceen은 리액트 트리와 상태를 화면에 렌더링과 상관없이 저장하고 있다고 하네요. 그래서 이것을 이용해서 방문했던, 혹은 앞으로 방문할 페이지를 미리 렌더링시킬 수 있다고 합니다. (이 부분은 해석이 맞는지 모르겠네요.. 다음 포스팅을 봐야 정확히 알 것 같습니다.)
- Parallel Routes: 페이지에 두개 이상의 탭 바(레이아웃) 이 있는 경우가 있죠? 이럴 때 각자 독립적으로 작동할 수 있게끔 한답니다. 개념적으로 <iframe/> 과 유사하게 동작한다고 합니다.
- Intercepting Routes: 때로는 페이지 내에서 다른 페이지의 경로를 가로챌 수 있는 것이 유용합니다. 일반적으로 다른 URL은 다른 UI로 구성되어있지만, 이 컨텍스트 내에서 방문할 때는 그렇지 않습니다. 예를 들어서, 확장된 트윗이나, 모달 포토 뷰어 같은 케이스에서 가능합니다. (Sometimes it's useful to be able to intercept a route from within another page. The URL would normally lead to a different part of the UI but not when it's visited within this context. For example, a tweet that can be expanded and presented inline or a modal photo viewer instead of a standalone gallery.) 직관적으로 해석이 잘 되지는 않네요.. 제 예견에는 한 page 에서 다른 page 파일을 불러올 수 있는 기능이 추가된다는 뜻 같습니다.
- Streaming and selective hydration: 서버 중심의 라우팅, RSC, 그리고 Suspesnse 와 스트리밍 등이 결합해서 어떻게 클라이언트 에 전송되는 js 들을 줄이고, 작업을 더 작은 청크로 세분화시키는지에 대한 내용을 공유하겠다고 합니다.
마치며
항상 느끼는 것이지만, NextJS 블로그는 정말 가독성도 좋고, 사진으로 적재적소에 추상화시켜줘서 읽기 편한 것 같습니다. 공식 문서가 아니라 일반인이 작성한 블로그 읽는 기분이랄까요?
내용면에서도 다음 포스트가 더욱 기대가 되네요! 이번 RFC는 도입부의 내용이었다면 다음 RFC 에서는 직접적으로 어떻게 사용할지 예시도 나올 것 같아요. 새롭게 나오는 feature들을 바로 적용해보고 싶어지군요 ㅎㅎ
이만 마치겠습니다.👍