[TIL] Next.js v13 (+v12와 다른점)

최소희·2023년 6월 1일
0

프론트엔드 학습

목록 보기
7/13

Next.js 13을 학습하기 위해 해당 프로젝트를 시작하였다.

Next.js 문서와 NextJS App Router 강의 자료 토대로 이해한 내용들을 정리하였다.

강의 커리큘럼에 맞춰 구성하였고, 공식 문서 내용을 토대로 내용들을 추가 정리하였다.

Next.js App Router: Learn Modern Web Development

Chapter 1 : Routing, Server Components, Loading & Error Handling

Routing

Routing in Next 12

/pages

- index.tsx
- dashboard.tsx

Routing in Next 13

/app

- page.tsx
/dashboard
  - page.tsx
/(auth)
  /login
    - page.tsx
	/signUp
    - page.tsx
  • (folder): url에는 표기가 안되지만 프로젝트 내부 폴더 구조로서 유관한 라우터들을 wrapping할 수 있다.

app 폴더 내에 생성된 경로들은 모두 Server Component이므로, console.log()의 로그들이 보이지 않는다. 그래서 만약 CSR을 하기 위해서는 상단에 use client를 명시해야한다.

Server Components

Server Components

초기 페이지 로드가 더 빠르며, 클라이언트 사이드 자바스크립트 번들 사이즈가 줄어든다.

그리고 중요한 자원들이 포함된 컴포넌트의 경우, 해당 자원들은 클라이언트에 로드된 자바스크립트 번들에 포함되지 않는다.

기본적으로 app에 설정된 라우터들은 Server Component로 되어있으며, 'use client'를 통해 클라이언트 컴포넌트를 설정할 수 있다.

Client Components

서버에서 pre rendering이 되어 클라이언트에서 클라이언트에서 hydrate된다.

즉 프리렌더링된 html DOM 요소 위에 JS 파일이 한 번 더 렌더링되면서 자바스크립트 코드들이 DOM 요소 위에 자기 자리를 찾아가며 매칭이 된다.

When to use?

Server Component
  • 데이터 요청
  • 백엔드 자원 접근
  • access token, api key 와 같은 중요한 정보들은 서버에서 핸들링
  • 서버에 크게 의존도를 유지해야할 경우
Client Component
  • onClick, onChange와 같은 이벤트 리스너와 상호작용을 추가할 경우
  • useState, useReducer와 같은 상태와 라이프사이클 이펙트들을 사용할 경우
  • Brower API를 사용할 경우
  • state, effect 혹은 브라우저 api 기반 커스텀 훅을 사용할 경우
  • 리액트 클래스 컴포넌트를 사용할 경우
info

[출처 : https://nextjs.org/docs/getting-started/react-essentials]

Pattern

어플리케이션 성능을 향상시키기 위해서는, Client 컴포넌트들을 일부로만 구성하게 한다.

예를 들어, 로고와 링크같은 정적 요소들과 상호작용이 일어나는 search bar가 있을 경우, search bar에 한해서만 와 같은 형태로 Client 컴포넌트로 구성하고, Server 컴포넌트로 이루어진 layout에 사용한다.

// 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>
    </>
  );
}
미지원 pattern

위와 반대로, Client 컴포넌트에 Server 컴포넌트를 import해서는 안된다.

[출처 : https://nextjs.org/docs/getting-started/react-essentials]

Loading

Next.js는 기본적으로 Server Component를 제공한다고 하였다.

그러면 비동기로 서버에서 데이터를 요청받을 때까지의 로딩을 어떻게 표시할 수 있을까?(우리는 useState와 같은 hook을 쓰지 못한다!)

이러한 상황을 대비하여 Next.jsd에서는 loading에 대한 스켈레톤 혹은 로딩 UI를 보여줄 파일명을 따로 지정하였다.

다음과 같이 loading.tsx을 추가하자.

/app

- page.tsx
/dashboard
  - page.tsx
/(auth)
  /login
    - page.tsx
    - loading.tsx
  /signUp
    - page.tsx

login의 page 컴포넌트에서 비동기로 특정 데이터가 요청되는 동안, loading 페이지가 렌더링된다.

Error

React에서는 Suspense동안 렌더링이 실패할 때, Error Boundary를 호출하여 에러를 표시한다.

loading.tsx처럼 error.tsx 컴포넌트를 만든다.

// page.tsx

const session = null

export default function Home() {
  if (!session) throw new Error("there is error related to session")
  
  return <main>This is an auth-only page</main>
}
// error.tsx
'use client'

const error = ({
  error,
  reset // redo the last code
}:{
  error : Error;
  reset : () => void
}) => {
  console.log(error.message) // there is error related to session
  return <div>error <button onClick={reset}>Try again</button></div>
  }
export default error

위와 같이 error 메세지에 접근이 가능하다.

좀 더 효율적으로 에러 메세지를 관리하기 위해서, 저렇게 컴포넌트마다 작성하는 것이 아닌, 모듈로 따로 관리를 해보자.

Error message 관리

  1. src/lib/exceptions.ts 생성
export class AuthRequiredError extends Error {
  constructor(message = "Auth is required to access this page.") {
    super(message)
    this.name = "AuthRequiredError"
  }
}
  1. message를 넣어주거나 default message 활용
// page.tsx
import {AuthRequiredError} from "@/libs/excepntions.tsx"

const session = null

export default function Home() {
  if (!session) throw new AuthRequiredError // or AuthRequiredError("hello")
  
  return <main>This is an auth-only page</main>
}

Dynamic Routing

/post
	/[postId]
  	- page.tsx
// src/app/post/[postId]/page.tsx

import { FC } from 'react'

interface PageProps {
  params: {
    postId : string
  }
}

const page: FC<PageProps> = ({params}) => {
  
  return <div>{params.postId}</div>
}

export default page

위와 같이 우리가 설정한 동적 파라미터 변수 값을 가져올 수 있다.

쿼리스트링 값 접근

/post/1?searchQuery=hello 와 같이 쿼리스트링이 있을 경우, 어떻게 값을 가져올 수 있을까?

동일하게 컴포넌트 내에서 props로 받는 변수에 접근이 가능하다.

nextProps

Dynamic router in dynamic router

/shoppingItems/1/blue url로 라우팅을 하고 싶을 때, 다음과 같이 ...을 추가하여 모든 세그먼트에 접근 가능하도록 설정한다.

/shoppingItems
	/[...postId]
  	- page.tsx

/shoppingItems/hello/1/blue 에 대한 파라미터들은 다음과 같이 postId가 배열 형태로 파싱된다.

{
	params : { postId : ["hello", "1", "blue"] },
	searchParams : {}
}
// shoppingItems/[...postId]/page.tsx

const page: FC<PageProps> = (props) => {
  console.log(props)
  /*
  {
    params : ["1", "blue"],
     searchParams : {}
   }
  */    
  return <div>{props.params.postId}</div>
}

export default page

Chapter 2 : Rendering for optimized page speeds

Next.js 12

getServerSideProps 메서드로 서버 사이드로 렌더링할 요소들을 요청한 다음, 객체 형태의 props를 해당 컴포넌트에 넘겨주는 형태로 서버 사이드 렌더링 컴포넌트를 구현한다.

const page = async (props: any) => {
  return <div>hello</div>;
};

export async function getServerSideProps() {
  return {
    props: {},
  };
}
export default page;

Next.js 13

13버전부터는 getStaticProps, getInitialProps, getServerSideProps 등 이전방법은 지원이 안된다.

fetch 옵션을 통해 getStaticProps, getInitialProps 처럼 사용할 수 있다.

build를 통해 각 경로 구성 확인

yarn build를 하면 각 경로(path)들이 어떻게(Server/Static) 빌드되는지 확인할 수 있다.

buildImg

  • 다이나믹 라우팅으로 구성된 경로의 경우, 파라미터의 변동성 때문에 서버사이드렌더링을 한다.

fetch 옵션

fetchOption

Next.js 13에서 fetch는 cache 옵션 기능을 제공한다.

force-cache && deafult: 처음에 요청한 데이터를 캐싱하고, 이후 재요청 시, 캐싱한 데이터를 반환한다. => getStaticProps와 유사

no-store : 매 요청마다 최신 데이터를 요청하려면, no-store로 설정한다. => getServerSideProps와 유사

revalidate : 주기적으로 캐싱된 데이터를 갱신하기 위해서는, revalidate에 초 단위의 시간을 정의하여 설정한다.

  const res = await fetch("https://jsonplaceholder.typicode.com/posts/1", {
    next: { revalidate: 10 },
  }); // 10 초 동안은 해당 response가 캐싱되어 반환됨

axois

axois의 경우, 자체적으로 fetch 옵션을 제공해주지 않으므로 커스터마이징을 하여 사용해야한다.

dynamic

해당 컴포넌트 혹은 해당 레이아웃 컴포넌트 내에서 http 요청에 대한 fetch 옵션을 설정하고 싶다면, 다음과 같이 dynamic 변수로 옵션을 설정해준다.

import axios from "axios";

export const dynamic = "force-dynamic";

const page = async ({}) => {
  const { data } = await axios.get(
    "https://jsonplaceholder.typicode.com/posts/1"
  );
  return <div>{JSON.stringify(data)}</div>;
};

export default page;

빌드 시, 해당 dashboard 컴포넌트는 server-side 컴포넌트로 빌드되는 것을 확인할 수 있다.

ssrImg

revalidate

주기적으로 데이터를 갱신하려면 revalidate 변수를 사용하여 갱신할 초단위의 숫자를 할당한다.

import axios from "axios";

export const revalidate = 10;

const page = async ({}) => {
  const { data } = await axios.get(
    "https://jsonplaceholder.typicode.com/posts/1"
  );
  return <div>{JSON.stringify(data)}</div>;
};

export default page;

빌드 시, 해당 dashboard 컴포넌트는 static 컴포넌트로 빌드되는 것을 확인할 수 있다.

staticImg

generateStaticParams

generateStaticParams함수는 빌드 타임 때, 동적 경로들을 정적으로 생성하여, 해당 컴포넌트를 ServerSideGenerate하게 해준다.

Single Dynamic Segment

// app/product/[id]/page.tsx

export function generateStaticParams() {
  return [{ id: '1' }, { id: '2' }, { id: '3' }];
}
 

// - /product/1
// - /product/2
// - /product/3
export default function Page({ params }: { params: { id: string } }) {
  const { id } = params;
  // ...
}

Multiple Dynamic Segments

// app/product/[category]/[product]/page.tsx

export function generateStaticParams() {
  return [
    { category: 'a', product: '1' },
    { category: 'b', product: '2' },
    { category: 'c', product: '3' },
  ];
}
 
// Three versions of this page will be statically generated
// using the `params` returned by `generateStaticParams`
// - /product/a/1
// - /product/b/2
// - /product/c/3
export default function Page({
  params,
}: {
  params: { category: string; product: string };
}) {
  const { category, product } = params;
  // ...
}

[코드 출처 : https://nextjs.org/docs/app/api-reference/functions/generate-static-params]

Catch-all Dynamic Segment

// app/post/[...postId]/page.tsx

export function generateStaticParams() {
  return [
    { postId: ["a", "1"] },
    { postId: ["b", "2"] },
    { postId: ["c", "3"] },
  ];
}

// 함수는 하나의 객체 매개변수를 받으며, 이 객체는 params라는 속성을 가짐
// params라는 속성은 다시 하나의 객체를 값으로 가지며, 그 객체는 postId라는 속성을 가지고 있음
const page = ({ params }: { params: { postId: string[] } }) => {
  const { postId } = params;
};

export default page;

빌드시, 해당 컴포넌트는 SSG로 생성이 되는 것을 확인할 수 있다.

ssgImg

Layout in Next.js 13

Next.js 13에서 생성되는 layout 컴포넌트는 해당 파일 하위에 속하는 파일들이 종속관계이다.

즉, 아래와 같은 구조에서 post의 인덱스 페이지는 layout 컴포넌트의 children형태로 전달된 후, props되어 렌더링된다.

/src
	/app
		layout.tsx
			/post
				page.tsx

그래서 아래와 같이 return문에 렌더링할 컴포넌트 역할을 하는 children을 같이 넣어줘야 해당 페이지가 컴포넌트 기준으로 렌더링된다.

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  )
}

중첩 레이아웃

특정 페이지에만 적용되는 레이아웃이다.

예를 들어, 마이페이지에서는 마이페이지에서만 적용되는 레이아웃이 있을 수 있다. 중첩 레이아웃을 사용하면 페이지별로 다른 레이아웃을 쉽게 적용할 수 있다.

이렇게 하위 컴포넌트 내의 자체 layout을 만들게 되면, 리렌더링 시, 상위 컴포넌트는 리렌더링 되지 않고, 해당 컴포넌트만 데이터 변경시 rerendering 하도록 할 수 있다.

Chapter 3 : SEO Best Practices & Optimization

sitemap

XML Sitemap은 웹사이트에 속한 url들을 나타내어, 업데이트 될 때마다 구글이 감지하고 웹사이트를 더 효율적으로 크롤링할 수 있도록 도와준다.

// sitemap.ts

type Post = {
  userId: number;
  id: number;
  title: string;
  body: string;
};

export default async function sitemap() {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts");
  const allPosts = (await res.json()) as Post[];

  const posts = allPosts.map((post) => ({
    url: `http://localhost:300/post/${post.id}`,
    lastModified: new Date().toISOString(),
  }));

  const routes = ["", "/about", "/post"].map((route) => ({
    url: `http://localhost:3000${route}`,
    lastModified: new Date().toISOString(),
  }));

  return [...routes, ...posts];
}

yarn build 후, 프로젝트를 실행하여 웹사이트에 파라미터를 sitemap.xml로 추가하면 정리된 url들을 확인할 수 있다.

opengraph-image

출처 : https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image

opengraph-image 파일명을 가진 이미지는 해당 경로에 대한 og(open graph)를 설정해준다.

metadata

metadata는 다음 코드로 각 레이아웃 혹은 페이지 컴포넌트에서 설정할 수 있다.

레이아웃의 경우, 해당 레이아웃의 하위 파일들에 공통적으로 메타데이터를 적용할 수 있으나, 페이지에서 메타데이터를 정의할 경우, 해당 페이지에 한해서만 적용된다.


export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

동적 라우팅에서의 metadata 설정

generateMetadata 함수로 동적 경로를 받아와서 해당 데이터 기반으로 메타데이터를 설정할 수 있다.

// post/[...postId]/page.tsx

export async function generateMetadata({
  params,
}: PageProps): Promise<Metadata> {
  const res = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${params.postId}`
  );
  const data = (await res.json()) as Post;

  return { title: data.title };
}

프로젝트를 실행하고 페이지에 들어가면, 메타데이터가 동적으로 적용된 것을 확인할 수 있다.

metadataTitleImg

Chapter 4 : Next.js 13 API Route Handlers

API Route는 Next.js 앱에서 API 엔드포인트를 생성하게 해준다.

app/api 폴더 내에 경로에 해당하는 폴더와 route 파일을 생성해준다.

파일명은 무조건 route여야 Next.js 가 인식할 수 있다.

참고 : https://nextjs.org/docs/app/building-your-application/routing/router-handlers

route 파일 내에서는 GET, POST. PATCH 등 여러 메서드들을 정의할 수 있다.

쿼리 스트링 값 가져오기

// app/api/user/route.ts

import { NextRequest } from "next/server";

export async function GET(req: NextRequest) {
  const { searchParams } = new URL(req.url);
  const mysearchParam = searchParams.get("mysearchParam");
  console.log(mysearchParam);
  console.log("GET REQUEST");

  return new Response(JSON.stringify({ name: "hui" }), { status: 401 });
}

export async function POST() {
  console.log("POST REQUEST");
}

API Route에서 요청하는 url의 쿼리스트링 값을 가져오기 위해서는 해당 요청 url의 key 값을 매개변수로 전달하여 가져올 수 있다.

첫번째 사진과 같이 GET 요청을 보내면 우리가 반환하는 데이터가 잘 전달되고, 서버에서는 해당 쿼리스트링 value 값을 확인할 수 있다.

requestImg

serverImg

runtime

runtime을 다음과 같이, edge로 설정하여 빌드를 기존 Node.js 기반 API route보다 더 빠르게 할 수 있다.

// app/api/user/route.ts

export const runtime = "edge"

...
profile
프론트엔드 개발자 👩🏻‍💻

0개의 댓글