개인공부

Next.js Docs (1) React Essentials

강물둘기 2023. 6. 2. 23:13

Next.js 공부를 하기위해 공식문서를 읽어보기로 했다.

뭔가 읽다 보니까 지나가면 까먹을거 같아서 번역하면서 기록을 남겨본다.

오역주의..

 

Introduction

Next.js는 React Framework이다. React Component로 만든 interface를 좀 더 좋은 구조, 특징, 최적화를 도와준다. 또한 번들링이나 컴파일링도 도와주기 때문에 개발자가 tool을 세팅하는 시간을 줄여준다.

 

Main Feature

1. Routing : Server Component 최상단에 위치한 라우터가 layout, nested routing, loading states, error handling 등을 도와준다.

 

2. Rendering : SSR, CSR을 지원한다. 또한 static, dynamic 렌더링도 지원한다.  Edge, Node.js 환경에서 동작한다.

 

3. Data Fetching : data fetching을 간단하게 구현하도록 도와준다.

 

4. Styling : CSS Modules, Tailwind CSS, CSS-in-JS를 지원한다.

 

5. Optimization : Web vitals, UX 향상을 위한 이미지, 폰트, Script 최적화를 지원한다.

 

6. Typescript : Typescript 및 custom Typescript Plugin, type checker를 지원한다.

 

7. API Reference : Next.js 전체에 걸쳐 API 설계를 업데이트 한다. API 섹션 참조

 

 

Getting Started

Installation

 16.8 버전 이상의 node.js 필요.

npx create-next-app@latest

 

React Essentials

Next.js에서는 Client Component와 Server Component를 구분하는데 무엇이 다르고 언제 사용하는지 알려준다.

 

Server Components

React Server Component는 서버, 클라이언트를 활용하는 하이브리드 앱 구축을 위한 사고를 도입한다.

모든곳에서 client-side rendering을 하는것이 아니라 목적에따라 유동적으로 client-side, server-side 렌더링을 할 수 있다. 기본적으로 유저와 상호작용이 많은곳은 CSR을, 그렇지 않은 곳은 SSR을 하도록 한다.

Server Components를 사용하는 이유는 서버 인프라를 좀 더 효과적으로 사용할 수 있게 해 주고, 초기 페이지 로드 속도가 빨라지고 javascript 번들 크기가 줄어든다. 기본적으로 server Component를 사용하고, 필요할 때 'use client' directive를 사용하여 client Component를 사용한다.

 

Client Components

Client Components는 유저와 상호작용을 용이하게 한다. 서버에서 미리 렌더링 되어 client에서 hydrate된다.(메소드인듯?) 'use client'라는 directive를 파일 최상단에 사용하여 client component라고 지정해준다. 모든 client component에 사용하지 않아도 되고 최상단 component에 directive를 사용하면 자식 컴포넌트는 모두 client component가 된다.

 

 

언제 Server and Client Component를 사용하는가?

기본적으로 Server Components를 사용하고, Client Component가 필요한 경우에만 Client Component를 사용하는 것을 추천한다.

Server Component를 사용하는 경우 Client Component를 사용하는 경우
Fetch data 유저와 상호작용하는 event listener를 사용하는 경우
백엔드 자원에 직접 접근하는 경우 상태나 lifecycle 효과를 사용하는 경우
서버의 민감한 정보를 보관하는 경우 브라우저 전용 api를 사용하는 경우
서버에 대한 대규모 종속성 유지 state,effect, 브라우저 전용 api에 의존하는 custom hook을 사용하는 경우
클라이언트측 javascript 감소시킬 때 React Class Component를 사용하는 경우

 

Patterns

 

1. 애플리케이션의 성능을 향상시키기위해 가능하다면 Client component는 component tree의 끝으로 이동시키는 것을 추천한다. 

// SearchBar is a Client Component
import SearchBar from './searchbar';
// Logo is a Server Component
import Logo from './logo';
 
// Layout is a Server Component by default
export default function Layout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <>
      <nav>
        <Logo />
        <SearchBar />
      </nav>
      <main>{children}</main>
    </>
  );
}

 예를 들어 검색바를 만드는 경우 <SearchBar /> 컴포넌트는 유저와 상호작용해야 하기 때문에 client component로 만들고 나머지 정적인 요소(e.g. 로고, 링크 등)들은 server component로 만든다. 

주석)) 클라이언트에게 해당 컴포넌트의 모든 javascript 코드를 보내지 않고 일부 layout은 만들어 보내기 때문에 페이지 로딩속도 향상을 시킬수 있는거 같다?

 

 

2. 같은 components tree에서 서버 컴포넌트와 클라이언트 컴포넌트를 혼용해서 사용할 수 있다.

 서버 컴포넌트는 모두 렌더링이 된 후 클라이언트로 보내진다.(클라이언트 컴포넌트에 중첩된 서버 컴포넌트 포함)

클라이언트는 이미 렌더링 된 서버 컴포넌트 및 클라이언트 컴포넌트를 받아 클라이언트 컴포넌트를 렌더링하고 이미 렌더링 된 서버 컴포넌트와 merge 한다. 

 

Next.js에서 첫페이지가 로드될 때 렌더링된 서버 컴포넌트 및 nested and merge 과정을 거친 클라이언트 컴포넌트는 서버에서 미리 html로 렌더링 되기 때문에 첫페이지 로딩속도가 빨라진다.

 

 

3. 위에서 설명한 렌더링 흐름에서 클라이언트 컴포넌트 안에 중첩된 서버 컴포넌트의 경우 제한 사항이 있다.

 클라이언트 컴포넌트에 서버 컴포넌트를 import할 수 없다.

 이러한 경우 import 하는 대신 React props로 서버 컴포넌트를 내려줄 구멍을 만들어 놓고 클라이언트 컴포넌트가 렌더링 될 때 그 구멍이 서버 컴포넌트로 채워질 수 있도록 구성한다. 구멍을 만들 때 React children을 활용하는 방법을 추천한다.

'use client';
 
import { useState } from 'react';
 
export default function ExampleClientComponent({
  children,
}: {
  children: React.ReactNode;
}) {
  const [count, setCount] = useState(0);
 
  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
 
      {children}
    </>
  );
}

 

부모 서버 컴포넌트에서는 클라이언트 컴포넌트와 서버 컴포넌트 모두 import할 수 있다.

// This pattern works:
// You can pass a Server Component as a child or prop of a
// Client Component.
import ExampleClientComponent from './example-client-component';
import ExampleServerComponent from './example-server-component';
 
// Pages in Next.js are Server Components by default
export default function Page() {
  return (
    <ExampleClientComponent>
      <ExampleServerComponent />
    </ExampleClientComponent>
  );
}

이러한 경우 ExampleClientComponent와 ExampleServerComponent 는 분리되어 독립적으로 렌더링 된다.

 

이러한 패턴은 이미 chidren props가 있는 페이지에 적용되기 때문에 따로 wrapper 컴포넌트를 만들지 않아도된다.

이렇게 되면 클라이언트 컴포넌트가 클라이언트에 전달되기 전 서버에서 독립적으로 렌더링된다.

"lifting content up" 전략과 동일한 전략이 사용되었다.(자식 컴포넌트의 리렌더링을 방지하기 위한 전략)

children을 꼭 고집하지 않고 jsx를 props로 내려줘도 된다.

 

 

4. 서버에서 클라이언트 컴포넌트로 직접적으로 props를 내려줄 수 없고 serialization 과정이 필요하다.

주석)) 객체를 json파일로 serialization 하는 것과 비슷한 과정을 이야기 하는 것 같다. 

 * Next.js 13에서 추가된 App Router를 사용하면 network 경계가 서버컴포넌트와 클라이언트 컴포넌트 사이에 있기 때문에 serialization이 필요없다. getStaticProps/getServerSideProps와 페이지 컴포넌트 사이의 경계가 되는 페이지와는 다르다.

 

 

5. 서버 전용 코드를 클라이언트 컴포넌트에서 제외(Poisoning)

  javascript로는 클라이언트 코드와 서버코드 모두 작성이 가능한데 서버에서만 실행되도록 의도된 코드가 클라이언트 코드로 숨어들 수 있다.

export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  });
 
  return res.json();
}

예를 들어 위 코드는 클라이언트와 서버 모두에서 동작 가능한데, 위의 API_KEY 환경변수는 NEXT_PUBLIC 접두사가 없기 때문에 서버에서만 접근이 가능하다. Next.js는 민감한 정보를 보호하기 위해 클라이언트 컴포넌트에서는 빈 문자열을 반환한다. 그러므로 위 코드는 클라이언트에서 동작은 가능하지만 에러를 발생시킨다.

따라서 서버에서만 동작 가능하도록 코드를 보호해야 한다.

 

서버 코드를 보호하기 위해 "server-only" 패키지를 사용할 수 있다. 패키지를 설치하고

import 'server-only';

한 줄만 추가하면 클라이언트 컴포넌트에서 해당 코드를 사용할 수 없다.

 

반대로 "client-only"를 사용하면 클라이언트 컴포넌트에서만 해당 코드를 사용 가능하게 할 수 있다.(window 객체를 사용할 때와 같은 상황에서 필요하다.)

 

 

6. Data Fetching

 특별한 이유가 있는것이 아니라면 data fetching은 서버 컴포넌트에서만 사용하는것을 권장한다. 서버 컴포넌트에서 data fetching을 하는것이 performance 측면이나 UX 측면에서 더 좋다.

 

 

7. Third-party packages

서버 컴포넌트는 최근에 나왔기 때문에 외부 패키지를 사용하는 경우 "use client" directive를 붙여 사용한다. 서버 컴포넌트에서는 작동하지 않을 수 있다. 

서버 컴포넌트에서 패키지를 사용해야 하는 경우 클라이언트 컴포넌트로 한번 감싸면

// carousel.tsx
'use client';
 
import { Carousel } from 'acme-carousel';	// 외부 패키지
 
export default Carousel;

 

서버 컴포넌트에서 사용 가능하다.

import Carousel from './carousel';
 
export default function Page() {
  return (
    <div>
      <p>View pictures</p>
 
      {/*  Works, since Carousel is a Client Component */}
      <Carousel />
    </div>
  );
}

provider를 사용하는 패키지의 경우는 하단 참조 ( Context 파트 하단에 나옴)

 

* 패키지 제작자는 "use client" directive를 패키지 진입점에 추가하면 사용자가 directive를 추가하지 않아도 동작한다.

 최적화를 위해 "use client"를 진입점이 아니라 tree 깊은곳에 추가하여 모듈의 일부를 서버 컴포넌트 모듈 그래프로 활용할 수 있다. 

 

 

 

Context

대부분의 React application이 context를 활용하여(createContext, third-party package를 사용해서) 컴포넌트 사이에서 data를 공유한다. 

Next.js에서 클라이언트 컴포넌트에서는 context를 전부 활용가능하지만 서버 컴포넌트에서는 직접적으로 context를 만들거나 사용하는것이 불가능하다. 서버 컴포넌트에는 React state가 없어서 state 변경에 의존하는 context를 사용하는 것이 불가능하기 때문이다.

 

1. 먼저 클라이언트 컴포넌트에서 context를 사용하는 방법을 알아본다.

context를 사용하기 위해 provider가 필요한데, root 컴포넌트가 서버 컴포넌트이면 provider를 사용할 때 에러가 발생한다. 이를 해결하기 위해 클라이언트 컴포넌트로 한 번 감싸서 provider를 사용한다.

// theme-provider.tsx
'use client';
 
import { createContext } from 'react';
 
export const ThemeContext = createContext({});
 
export default function ThemeProvider({ children }) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}

 

이제 서버 컴포넌트에서 위의 provider를 사용하면 된다.

import ThemeProvider from './theme-provider';
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

* Provider는 Tree의 최대한 깊은곳에 배치하는것이 좋다. 

 

2. Third-party package의 provider를 서버 컴포넌트에서 사용하는 방법

 Third-party 패키지에 provider가 포함되는 경우 context provider와 비슷하게 클라이언트 컴포넌트로 한번 감싸서 provider를 사용한다.

// providers.js
'use client';
 
import { ThemeProvider } from 'acme-theme';
import { AuthProvider } from 'acme-auth';
 
export function Providers({ children }) {
  return (
    <ThemeProvider>
      <AuthProvider>{children}</AuthProvider>
    </ThemeProvider>
  );
}

 

import { Providers } from './providers';
 
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

 

* Third-party 패키지에 "use client" directive가 포함되어 있다면 위 과정 없이 바로 사용가능하다.

 

3. 서버 컴포넌트간 데이터 공유

서버 컴포넌트는 state를 사용하지 않기 때문에 context가 필요없다. 대신 여러 서버 컴포넌트간 데이터 공유를 위해 javascript 기본 패턴을 사용한다. 

예를 들어 모듈을 사용하여 데이터를 공유할 수 있다.

// utils/database.ts
export const db = new DatabaseConnection();

 

// app/users/layout.tsx
import { db } from '@utils/database';
 
export async function UsersLayout() {
  let users = await db.query();
  // ...
}

 

// app/users/[id]/page.tsx
import { db } from '@utils/database';
 
export async function DashboardPage() {
  let user = await db.query();
  // ...
}

database 모듈을 만들어 여러 서버 컴포넌트에 import하여 사용한다.(이러한 javascript 패턴을 global singleton 패턴이라고 한다.)

 

4. 서버 컴포넌트간 fetch requests 공유

 요청 결과 데이터를 공유할 때 여러 컴포넌트에서 하나의 데이터를 공유하기보다는 데이터가 필요한 각각의 컴포넌트가 데이터를 요청하는 colocating data fetching을 추천한다. fetch요청을 할때 서버 컴포넌트에서 자동적으로 중복 요청을 제거하기 때문에 중복 요청은 걱정하지 않아도 된다. 한번의 요청으로 fetch 캐시에 데이터를 저장하고 Next.js가 fetch 캐시로부터 동일한 값을 읽어들인다. 

 

 

 

Reference

- https://nextjs.org/docs/getting-started/react-essentials

 

Getting Started: React Essentials | Next.js

To build applications with Next.js, it helps to be familiar with React's newer features such as Server Components. This page will go through the differences between Server and Client Components, when to use them, and recommended patterns. If you're new to

nextjs.org