이렇게 레거시 코드가 되버려.. 1편 (feat. Next.13)

박정훈·2022년 10월 31일
11

Next.js

목록 보기
1/5

완전완전 그냥 제가 보고 싶어서 공식문서를 읽은 글에 불과합니다. :)

Next.js가 13 버전을 릴리즈 했다. 코드는 작성하는 순간부터 레거시 코드라는 말은 들어봤는데... 사이드 프로젝트인 알약이 Next 12버전이었으니까... 레거시 코드로 못박혀 버리니 뭔가 기분이 묘했다. 아마도 13버전에 대한 번역 및 해석들이 우수수 쏟아져 나오겠지만, 일단 내가 당장 궁금하니까
일단 보자

음 목차는

  • app/Directory(beta)
    • Layouts
    • React Server Components
    • Streaming
  • Turbopack (alpha)
  • New next/image (stable)
  • New @next/font (beta)
  • Improved next/link

놀라웠던건 Turbopack이었다. 최대 700배가 빠른 Rust언어 기반의 웹팩 대체제라니. 미쳤나보다. (Rust가 미래..?)

app/Directory

app/directory에 대한 beta 도입으로 routing과 layout을 개선한다. app은 커뮤니티의 피드백을 위해 게시되었던 Layouts RFC의 결과이다...?
처음 들어보는거 튀어나왔다. Layouts RFC란다. 상당히 뒷북이지만 돌부리에 걸려 넘어졌으니 해결하고 가야겠다. 그리고 Layouts RFC와 Next.js 13이 모두 다루고 있는 부분만 보려고 한다.

Layouts RFC

새로운 app directory(beta)

app은 다음과 같은 서포트를 포함한다.

  • Layouts : state를 보존하고 비싼 리렌더링을 피하면서 경로간 쉽게 UI를 공유한다.
  • Server Components : 가장 dynamic한 앱의 기본 server-first를 만든다(?)
  • Streaming : 렌더링 되는 즉시 로드 상태 및 스트림을 UI단위로 보여준다.
  • Support for Data Fetching : async 서버 컴포넌트와 확장된 fetch API는 컴포넌트 레벨의 fetching이 가능하다(?)

현재 내가 사용하던 12버전에서는 page폴더 아래 하위폴더로 내려가면서 라우팅이 잡혔다.
새로운 app 디렉토리에서 폴더가 경로를 정의하는데 사용된다고 한다. 경로를 만드려면 폴더안에 파일을 만들면 된다.

// app/page.js
export default function page() {
  return <h1>wow</h1>
}

https://nextjs.org/blog/layouts-rfc#route-segments

그리고 app 디렉토리는 page 디렉토리와 함께 작동한다고 한다. 이전 버전과의 호환성을 위해서라는데 page 디렉토리의 동작은 동알하게 유지되며 계속 지원된다고 한다. 그리고 app 디렉토리가 page 디렉토리보다 우선권을 가진다고 한다.

New file convention: layout.js

알약에서는 공통으로 사용할 레이아웃을 컴포넌트로 빼서 관리했다. 근데 이제 지원한다고 하넹.
layout.js 파일을 추가해서 페이지 간에 공유되는 중첩 레이아웃을 만들 수 있다고 한다.
URL 경로에 영향을 주지 않고, 형제 세그먼트 사이를 이동할 때 리렌더링이 발생하지 않는다고 하네요. 우왕.
layout에는 두가지 타입이 있다.

  • Root layout: 모든 라우트에 적용한다.
  • Regular layout: 특정 라우트에 적용한다.
    둘 이상의 레이아웃을 함께 중첩해서 중첩 레이아웃을 구성할 수도 있다고 하네요. 아마 Root + Regular 겠지..?

https://vercel.com/blog/next-js-layouts-rfc-in-5-minutes
app폴더 하위에 layout.js파일을 만들었네. 그리고 하위 라우트인 app/blog에 blog에서 사용할 layout을 만든 모양새다.

Root layout

app folder안에 layout.js파일을 추가해서 모든 라우트에 적용할 root layout을 만들 수 있다.

root layout은 모든 경로에 적용되므로 _app.js와 _document.js가 필요하다.
root layout을 사용해서 initial document shell(e.g. <html><body>)을 커스터마이즈 할수 있다.
root layout 내에서 fetch data가 가능하다고 한다.

// app/layout.js

export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        <h1>Next.js Layouts RFC</h1>
      </head>
      <body>{children}</body>
    </html>
  );
}

Regular layout

특정 폴더 아래 layout.js 파일을 추가해서 일부 application에만 적용되는 레이아웃을 만든다. 위 그림이 잘 표현하고 있다.
blog/* 내부의 모든 경로 세그먼트에 적용이 된다.

Nesting Layouts

역시 위 그림에서 잘 설명되고 있다. 루트 레이아웃은 blog 레이아웃에 적용되고, blog/* 내부의 모든 경로에도 적용된다!

New file convention: page.js

아.. 기존의 page폴더 구조에서 page파일 convention이 생겼다.
nested page각 경로에 하나씩 들어가는 UI 파일이네요. 폴더 안에 page.js를 추가해서 페이지를 만들 수 있다고 한다.

route가 유효하려면 리프 세그먼트에 page가 있어야 한다고 합니다. 그렇다면 기존의 index의 역할을 하는 아이인건가?

Layout과 Page의 Behavior

  • Pages와 Layouts의 파일 확장자는 js|jsx|ts|tsx 가 사용 가능하다.
  • Page Components는 page.js의 default export다.
  • Layout Components는 layout.js의 default export다.
  • Layout Components는 반드시 children prop을 가져야 한다.

Instant Loading States

server-side routing에서는 데이터를 가져오고 렌더링한 후에 탐색이 이루어지므로 데이터를 가져오는 동안에 로딩 UI를 표시하는 것이 중요하다. 맞다. 표시 안해주면 유저는 이게 뭔 상황이람 할 테니까...
loading.js는 자동으로 페이지 또는 중첩된 세그먼트를 React의 Suspense Boundary로 래핑한다. Next.js는 첫 번째 로드 시 즉시 로드 구성 요소를 표시하고 형제 경로 사이를 이동할 때 다시 보여준다.
loading

https://vercel.com/blog/next-js-layouts-rfc-in-5-minutes

Default loading skeletons

Suspense boundaries는 loading.js라는 새로운 파일 컨벤션을 사용해서 자동으로 백그라운드에서 핸들링 된다.
loading.js 파일을 폴더에 넣어서 default loading skeleton을 추가할 수 있다.

// loading.js
export default function Loading() {
  return <YourSkeleton />
}

// layout.js
export default function Layout({children}) {
  return (
    <>
      <Sidebar />
      {children}
    </>
  )
}

// Output
<>
  <Sidebar />
  <Suspense fallback={<Loading />}>{children}</Suspense>
</>

Error Handling

Error boundaries는 자식 컴포넌트 트리에서 JavsScript error를 포착하는 React components다.
error.js는 자동으로 페이지 또는 중첩된 세그먼트를 React Error Boundary로 래핑한다. Next.js는 하위 트리에서 오류가 발생할 때마다 오류 구성요소를 표시한다. 오류가 발생할 경우 구성요소가 fallback으로 표시되고, 오류를 기록, 오류에 대한 유용한 정보를 표시, 오류 복구를 시도하는데 사용할 수 있다.

세그먼트 및 레이아웃의 중첩된 특징 때문에 Error boundaries를 만들면 UI의 해당 부분에 대한 오류를 분리할 수 있다. 오류가 발생하는 동안 경계 의위 레이아웃은 interactive 상태로 유지되고 상태가 보존된다.

error.js와 동일선상에 있는 layout.js파일 내의 오류는 Error boundaries가 레이아웃의 하위 항목을 감싸고 레이아웃 자체는 감싸지 않으므로 탐지되지 않는다.

error

error-boundaried

React Server Components

Server Components RFC를 사용하면 Next.js 앱에 React Server Components를 점진적으로 채택할 수 있다.

Server Components를 기본으로 설정

One of the biggest changes between the pages and app directories is that, by default, files inside app will be rendered on the server as React Server Components.

오! pages와 app 디렉토리의 가장 큰 차이점은 app안에 파일들은 React Server Component로 렌더된다. 이제 서버 컴포넌트 알아야겠네. 다음에 정리해보자.

서버 컴포넌트는 app 폴더 또는 자신의 폴더에서 사용할 수 있지만 호환성을 위해서 페이지 디렉토리에서는 사용할 수 없다고 한다.

Client and Server Components Convention

app 폴더는 서버, 클라이언트, 공유 컴포넌트를 지원하고 이러한 컴포넌트들을 트리에 끼울수 있다(?)
클라이언트 컴포넌트 및 서버 컴포넌트를 정의하기 위한 규칙에 관한 논의는 현재도 진행중이다.

  • 현재는 서버 컴포넌트의 파일 이름에 server.js를 추가하여 정의할 수 있다. ex) layout.server.js
  • 클라이언트 컴포넌트는 파일 이름에 .client.js를 추가하여 정의할 수 있다. ex)page.client.js
  • .js파일을 공유 컴포넌트로 간주한다. 서버와 클라이언트에서 렌더링될 수 있으므로, 각 컨텍스트의 제약 조건을 준수해야 한다.

Hooks

헤더 개체, 쿠키, 경로 이름, 검색 매개 변수 등에 액세스할 수 있는 클라이언트 및 서버 컴포넌트 Hooks를 추가할 것이다!

Rendering Environments

app의 루트는 기본적으로 Static Generation을 사용하며, root segment가 요청 컨텍스트를 필요로 하는 server-side hooks를 사용할 때 dynamic rendering으로 바뀐다.

Interleaving Client and Server Components in a Route

React에선 클라이언트 컴포넌트안에 서버 컴포넌트를 importing하는데 제한이 있다. 서버 컴포넌트가 server-only code를 가질 수 있기 때문이다.
다음은 동작하지 않는다.

import ServerComponent from './ServerComponent.js';

export default function ClientComponent() {
  return (
    <>
      <ServerComponent />
    </>
  );
}

그러나~! 서버 컴포넌트는 클라이언트 컴포넌트의 자식으로 전달될 수 있다. 이 작업은 다른 서버 컴포넌트로 wrapping해서 할 수 있다.

// 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>
    </>
  );
}

컴포넌트가 서버에서 렌더링 되기에 가능하다고 하네요. 그러니까 이 패턴을 사용하면 React는 클라이언트에 결과를 전송하기 전에 서버에서 서버 컴포넌트를 렌더링해야 한다는 것을 알게 되고, 클라이언트 컴포넌트 관점에서 볼 때 child인 서버 컴포넌트는 이미 렌더링 된 것입니다.

layouts에서는 추가 wrapper component를 작성할 필요가 없다. children prop과 함께 적용되기 때문이라고 한다.

// 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>
    </>
  );
}

Data fetching

라우트의 여러 세그먼트 내에서 데이터를 가져올 수 있다. data fetching이 page-level로 제한되어 있던 page 디렉토리와 다른 점이다.

Data fetching in Layouts

layout.js 파일에서 Next.js의 data fetching methods인 getStaticPropsgetServerSideProps로 data를 fetch 할 수 있다.

// 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}
    </>
  );
}

Multiple data fetching methods in a route

라우트의 여러 segment에서 data를 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} />;
}

Parallel data fetching

Next.js는 waterfalls를 최소화하기 위해 병렬로 data fetching을 한다.
parallel data fetching

Partial Fetching and Rendering

형제간 라우트 segment를 왔다갔다 할 때 Layouts은 리렌더링 되지 않고 해당 세그먼트에서 가져온다. 레이아웃을 공유하는 페이지에서 사용자가 형제 페이지 사이를 이동할 때 레이아웃은 공유되고 Next.js는 fetch만 하고 해당 segment에서 가져와서 렌더링만 한다.

이 기능은 특히 React Server Components에서 유용하다. 그렇지 않으면 각 네이게이션이 서버에서 페이지의 변경된 부분만 렌더링하는 대신에 전체 페이지가 서버에서 다시 렌더링된다. 엄청난 비효율!

아래 사진에서 유저가 /analytics에서 /settings로 가면, React는 page segments는 리렌더링하지만 layouts는 보존한다.
partial fetching

Route Groups

app 폴더의 계층은 URL 경로에 직접 매핑된다. 하지만 Route Groups을 생성해서 이 패턴을 벗어날 수 있다.

  • URL 구조에 영향을 주지 않고 경로를 구성한다.
  • 레이아웃에서 벗어날 라우트를 고른다.
  • 앱을 분할해서 여러개의 루트 layout을 만든다.

Convention

폴더 이름을 괄호로 감싸 만들수 있다. 아래 그림에서 layout을 공유하지 않는것을 볼 수 있다. 하하 참 신기하다.

Route Group의 이름은 URL경로에 영향을 주지 않는다.

Route Group

여러개의 root layouts는 app 최상위 디렉토리에 2개 또는 그 이상의 route group을 만들면된다.
multiple root layouts

여기까지 우선... Next.js 13버전에서 소개하고 있는 부분과 Layouts RFC와 겹치는 애들은 다 본거 같다. 몇몇개가 빠졌는데 나중에 릴리즈 버전으로 올라오면 그때 봐야겠다.

여기까진 Layouts RFC내용이었다! 아래부터는 다시 Next.js 13버전 공식문서로 이동한다. 그리고 Beta Docs를 왔다갔다 거린다...
휴... 이제 다시 Next.js 13버전 공식문서를 살펴보자. 거의 중복되는 내용인거 같으니, Layouts RFC에 없던 내용이 있으면 적고 아니면 넘어가야징!

Layouts

아무래도 이 내용은 꽤 중요한거 같다.

Layouts는 여러 pages에서 UI를 공유한다. 이동시에 layouts는 상태를 보존하고, 상호작용을 유지하며, 리렌더링 하지 않는다.

Server Components를 보려 했으나 Beta docs로 이동한다.

app 디렉토리는 React의 새로운 서버 컴포넌트 아키텍쳐를 지원한다. 아래 링크들은 Beta docs다.
next.js server and client components 를 읽어보라길래 들어갔더니 우선 fundamentals를 읽어보랜다.
next.js rendering fundamentals 읽어봐야겠지?

우선 rendering fundamentals를 살펴보자.

React 18과 Next.js 13은 우리의 앱을 렌더링하는 새로운 방식을 소개했다.

Rendering Environments

우리 앱을 렌더링하는 환경은 clientserver 두가지가 있다.

  • client
    client는 앱 코드에 대한 요청을 서버로 보내는 사용자 장치의 브라우저를 나타낸다. 그다음 서버의 응답을 사용자가 상호 작용할 수 있는 인터페이스로 전환한다.
  • server
    server는 앱 코드를 저장하고, client로부터 요청을 수신하고, 어떤 계산을 하고, 적절한 응답을 돌려주는 데이터 센터의 컴퓨터를 나타낸다.

Component-level Client and Server Rendering

React 18 이전에는 앱을 렌더링 하는 기본적인 방식은 전적으로 클라이언트에 있었다.
Next.js는 앱을 더 쉽게 pages(Next.js pages 디렉토리를 의미하는거 같다.)로 break down 할 수 있는 방식을 제공했고, 서버에서 HTML을 생성함으로써 prerender하며, React에 의해서 hydrated되도록 client로 보냈다.
하지만 이 방식은 초기 HTML을 interactive하기 위해 clent에 추가적인 JS를 추가할 필요가 있었다.

이제는 component level에서 렌더링 환경을 선택할 수 있다. 죽, React는 client와 server 위에서 모두 렌더링 가능하다. 기본적으로, app 디렉토리Server Component를 사용하고, 이는 서버 위에서 컴포넌트를 렌더링하기 쉽게 하며, client로 보내는 JS의 양을 줄였다. 그렇지만, app안에서도 Client Components를 선택해서 client위에서 렌더링 할 수 있다. 어디까지나 선택이라는 것!

Static and Dynamic Rendering on the Server

Static Rendering

Static Rendering은 Server와 Client 컴포넌트 둘 다 서버에서 build time에 prerender 된다. 결과물은 cached되고 후속 요청에서 재사용된다.

SSG(Static Site Generation)와 ISR(Incremental Static Regeneration)과 동일하다.

Server와 Client 컴포넌트들은 Static Rendering동안 다르게 렌더링 된다.

  • Client 컴포넌트는 HTML과 JSON이 서버 위에서 prerenderd되고 cached 된다.
  • Server 컴포넌트는 서버에서 렌더링되며, 그들의 payload(서버 컴포넌트를 지칭하는건가?)는 HTML을 생성하는데 사용된다. 동일한 payload가 client의 컴포넌트를 hydrate하는데 사용되므로, client는 JS를 필요로 하지 않는다.

Dynamic Rendering

Dynamic Rendering은 Server와 Client 컴포넌트 둘 다 server에서 request time에 렌더링된다. 해당 작업은 Static Rendering과 달리 cached되지 않는다.

Server Side Rendering(getServerSideProps())과 동일하다.

Edge and Node.js Runtimes

서버 위에서, page를 렌더링하는 런타임은 두가지다.

  • 생태계의 모든 Node.js API및 호환 패키지에 접근 가능한 Node.js Runtime (default)
  • 웹 API를 기반으로 Edge Runtime 호환 패키지하고만 호환되는 Edge Runtime

두 방식 모두 배포 인프라에 따라 서버에서 데이터를 스트리밍 할 수 있다.

fundamental 끗!
드디어 다시 Server and Client Components다. 🤸‍♂️

다시 Server and Client Components

Why Server Components?

Server 컴포넌트를 통해 개발자는 서버 인프라를 보다 효과적으로 사용할 수 있다. 예를 들면, 이전에 client에서 JS 번들 사이즈 크기에 영향을 미쳤던 커다란 의존성은 온전히 서버에 남기에, 퍼포먼스 향상을 이끌어낼 수 있다. 그렇지만 app 디렉토리는 여전히 JS를 필요로 한다.

When a route is loaded, the Next.js and React runtime will be loaded,
which is cacheable and predictable in size. 
This runtime does not increase in size as your application grows. 
Further, the runtime is asynchronously loaded, enabling your HTML from the server to be progressively enhanced on the client.

이 부분은 잘 이해가 가지 않는다... 경로가 로드되면, 캐시 가능하고 크기를 예측 가능한 Next.js와 React runtime이 로드된다. 이 runtime은 당신의 앱이 커져도 증가하지 않는다. 또한 runtime은 비동기적으로 로드되고, 서버에서 온 HTML을 client에 점진적으로 향상시킬 수 있다(?). 점진적으로 받아올 수 있다는 얘긴가?

추가적인 JS는 client-side의 상호작용이 필요할 때만 추가된다.

Client Components

Client 컴포넌트는 client위에서 렌더링 된다. Next.js에서는 Client Components는 서버에서 pre-rendered가 가능하고 client에서 hydrated된다.

Convention

Client Component를 사용하기 위해서 app 안에 file을 만들고 "use client"를 code에 추가한다.

use strict가 생각나는 대목이다...

// app/Counter.js
'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

When to use Server vs. Client Components?

Client Component가 필요해 지기까지는 Server Component를 사용!

cmp

Recommendations

Data Fetching

Client 컴포넌트에서 데이터를 가져올 수 있지만, 특별한 이유가 없다면 서버 컴포넌트에서 데이터를 가져오는것을 추천한다. 서버로 data fetching을 옮기는 것이 성능과 사용자 경험을 향상시킨다.

Passing props from Server to Client Components (Serialization)

Server 컴포넌트에서 Client 컴포넌트로 보낸 Props는 직렬화가 필요하다. 함수, 날짜 등과 같은 값을 client컴포넌트로 직접 전달할 수 없다는 의미이다.

Keeping Server-Only Code out of Client Components (Poisoning)

의도하지 않은 클라이언트 코드 사용을 위해서 서버 전용 패키지를 깔아서 미연에 방지(build time error)할 수 있다고 합니다.

Moving Client Components to the Leaves

앞서 나왔던 설명인데 레이아웃을 Client쪽에 두지말고 Server component로 (layout.js)두고 사용하랍니다. 이렇게 해서 레이아웃의 JS를 client에 보내지 않아도 된다고 하네요.

Importing Server Components into Client Components

Server와 Client 컴포넌트는 같은 컴포넌트 트리 안에서 interleaved(?)될 수 있다고 합니다. React가 두 환경을 뒤에서 병합한다고 하네요. 다만! Client 컴포넌트 내부에서는 Server 컴포넌트의 요소를 가져올 수 없다고 합니다. 역시 앞서 나온 내용이죠.

휴! 다시 Next.js 13 docs로 가자.

그리고 도저히 하나의 포스트로 끝낼 자신이 없어졌다. 13버전이 궁금했고, 시작부터 Layouts RFC가 튀어나오길래 또 궁금해서 본게 이렇게 양이 많아질 줄이야...
여기까지 읽었다.
다음에 Streaming부터 읽어야겠다.

참고

Next.js 13
Nextjs Layouts RFC
Next.js Layouts RFC in 5minutes (vercel)
Next.js Beta Rendering: Server and Client Components
Next.js Beta Rendering: fundamentals

profile
그냥 개인적으로 공부한 글들에 불과

1개의 댓글

comment-user-thumbnail
2022년 12월 13일

잘 봤습니다 ^^

답글 달기