Next.js 13버전부터 가장 크게 변화된 점은 Page Router ➡️ App Router로 변경된 라우팅 방식이다.
App Router의 핵심 기능들에 대해 알아보자.
Route 설정
# 기본 구조
Next.js 는 기존 Page Router 에서는 pages/의 하위에 route 경로를 설정하여 페이지를 구성했다면, 이번 App Router에서는 app/폴더 하위에 경로를 설정하여 페이지를 구성한다. 하위 파일명 또한 pages/파일명.js 또는 pages/폴더명/index.js로 route가 설정되었지만 App Router에서는 app/폴더명/page.js로만 사용할 수 있다. 만약 폴더 하위에 page.js파일이 없다면 route로 인식되지 않으니 해당 폴더명의 경로로 접근해도 페이지가 뜨지 않는다.
💡app폴더 하위에 components폴더를 생성할 수도 있으나, 이를 root에 생성하고 app폴더 하위에는 라우팅과 관련된 폴더만 생성하는 것이 가독성 측면에서 좋다
위 사진은 스택오버플로우에서 캡처한 이미지인데 Next의 폴더명을 지정하는 여러 방법들을 활용해 앱 폴더 내에 그룹화를 시켜 구조를 짜는 것도 깔끔해보였다.
# 페이지 간 이동
페이지를 이동하는 경우에 <a>요소가 아닌 Next.js에 내장된 <Link>컴포넌트를 사용한다. 이 컴포넌트를 사용해 이동하게 되면 a태그를 사용했을 때 이동한 페이지를 새롭게 다운받지 않아도 되므로 최적화가 가능하다. 또한 빌드 후에 자동으로 <a>로 변환되어 a태그의 장점을 가지면서(SEO) SPA의 장점(필요한 부분만 리렌더링)도 함께 갖게 된다.
import Link from 'next/link'
export default function Page() {
return <Link href="/dashboard">Dashboard</Link>
}
# 동적 라우터
상세 페이지와 같이 동적인 경로가 생성되어야 하는 경우가 있다. 예를 들어 app/blog/post1/page.js, app/blog/post2/page.js 이렇게 여러 페이지가 존재한다고 하면 post1,post2… 에 대한 폴더를 계속 만들 수는 없다. 따라서 해당 폴더에 app/blog/[id]/page.js와 같이 [] 기호로 감싸주어 동적인 세그먼트를 만들어준다. 이 기호 안에 들어가는 폴더명은 반드시 id일 필요는 없다. 이 값은 컴포넌트에서 props로 전달 받아 확인할 수 있다.
export default function BlogPost({ params }) {
return (
<div>
<p>{params.id}</p>
</div>
);
}
# 기타 특수 파일
이외에도 해당 경로에서 특수 파일명을 설정하여 여러 상태의 UI 처리가 가능하다.
- page.js : 경로 설정
- layout.js : 레이아웃 UI
- loading.js : 로딩 컴포넌트 UI
- error.js : 에러 컴포넌트 UI
- not-found.js : Not Found 컴포넌트 UI
NextJS의 컴포넌트
# 서버 컴포넌트
Next.js 13버전부터 모든 컴포넌트에 기본적으로 리액트 서버 컴포넌트가 적용되었다. 서버 컴포넌트는 서버에서 실행되는 React 컴포넌트로 서버 측에서 미리 렌더링한 HTML을 클라이언트에 전송하여 보여주게 된다. 이는 클라이언트와 서버 간의 요청-응답 사이클을 줄일 수 있다는 장점이 있다. 게다라 클라이언트 측이 다운로드 해야하는 자바스크립트 코드가 줄어 웹사이트의 성능을 향상 시킬 수 있다. 이 말은 곧 클라이언트의 로딩 시간을 줄여 사용자 경험을 향상 시킬 수 있다. 또한, 완성된 컨텐츠를 클라이언트 측에 보내므로 검색 엔진 최적화가 가능하다.
# 클라이언트 컴포넌트
서버 컴포넌트를 사용하게 되면 useState, useEffect, event handler 와 같은 브라우저와 상호작용이 필요한 코드들은 사용할 수 없다. 그렇기 때문에 컴포넌트 상단에 ‘use client’를 작성하여 클라이언트 컴포넌트임을 명시해줘야 한다. 이러한 작업을 React의 hydration 이라고 한다. 클라이언트 컴포넌트를 마구 사용했다간 서버 컴포넌트의 장점을 잃을 수 있다. 따라서 가능한 클라이언트가 필요한 부분만 컴포넌트로 따로 빼주어 작성하고 나머지 부분은 서버 컴포넌트로 유지하는 것이 좋다.
💡hydration이란?
hydration은 수분을 보충하는 과정을 말한다. 브라우저와의 상호작용을 활성화하기 위해 SSR로 불러온 메마른 페이지(처음에는 HTML로만 구성됨)에서 JavaScript 코드를 실행할 수 있도록 한다.
# Suspense 활용한 로딩 처리
원활한 사용자 경험을 위해 효율적인 로딩 처리는 중요하다. 이미 Next.js에서는 로딩을 처리하는 UI를 그려줄 수 있는 loading.js을 제공한다. 이렇게 처리하면 간편하게 로딩 처리를 할 수 있지만 전체 페이지를 포함하여 로딩처리가 발생하여 데이터를 가져오는 동안 정적 요소가 계속 표시되어야 하는 경우 이상적이지 않을 수 있다. 이 때 사용할 수 있는 것이 React의 <Suspense>컴포넌트이다. 데이터를 호출하는 컴포넌트만 따로 만들어 <Suspense>로 감싸주면 정적이 요소는 유지하고, 데이터를 fetching하는 동안 로딩이 필요한 부분은 fallback props에 로딩 UI를 제공하면 된다.
import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'
export default function Posts() {
return (
<section>
<h1>Contents</h1>
<Suspense fallback={<p>Loading feed...</p>}>
<PostFeed />
</Suspense>
<Suspense fallback={<p>Loading weather...</p>}>
<Weather />
</Suspense>
</section>
)
}
Form과 Server Action
# Form 요소를 활용한 Server Action
form을 통해 사용자와 상호작용할 때 사용자 입력은 보통 React에서 useState, useEffect와 onChange메서드를 사용하여 관리한다. 이 방식은 클라이언트에서 상태 및 이벤트를 핸들링 해야하므로 클라이언트 컴포넌트로의 변경이 필요할 것으로 보인다. 그러나 Next.js 에서는 HTML의 <form> 요소와 서버 액션을 연결하여 POST요청을 통해 form을 처리할 수 있다. form에서 action prop에 함수를 전달하여 서버 액션을 호출한다. 서버 액션 함수 안에 ‘use server’로 서버 컴포넌트임을 명시한다. 이 함수에서 받은 파라미터인 formData는 get()을 통해 <input>에 있는 값을 받을 수 있다.
export default function ShareMeal() {
async function shareMeal(formData) {
"use server";
const meal = {
title: formData.get("title"),
creator: formData.get("name"),
};
}
return (
<form action={shareMeal}>
<p>
<label htmlFor="name">Your name</label>
<input type="text" id="name" name="name" required />
</p>
<p>
<label htmlFor="title">Title</label>
<input type="text" id="title" name="title" required />
</p>
</form>
);
}
위와 같은 방식으로 사용하게 되면 해당 페이지는 추후에 필요한 경우 클라이언트 컴포넌트로 변경할 수 없다. 즉, ‘use server’와 ‘use client’를 함께 쓸 수 없으므로 유연성이 떨어진다. 또한 서버 측 코드는 인증이나 데이터베이스와의 상호 작용 등 민감한 정보가 담겨 있을 수 있으니 클라이언트 코드에 노출되면 보안 측면에서 문제가 발생할 수 있다. 따라서 클라이언트 측 코드와 서버 측 코드를 분리하여 코드를 작성한다.(관심사 분리)
// lib/action.js
"use server";
export async function shareMeal(formData) {
const meal = {
title: formData.get("title"),
creator: formData.get("name"),
};
}
// app/meals/share/page.js
import { shareMeal } from "@/lib/action";
export default function ShareMeal() {
return <form action={shareMeal}>...</form>;
}
# Form 제출 시 로딩
사용자가 양식을 작성하고 제출하는 동안 로딩이 발생할 수 있다. 로딩이 발생하는 동안에 제출하는 버튼을 다시 누르거나 로딩 상태를 인지하지 못하고 페이지를 벗어날 수도 있다. 이를 제어하기 위해 useFormStatus훅을 사용해 제출 버튼에 상태를 적용 시켜볼수 있다. 이 훅은 form 제출의 status 정보를 제공하는 훅이다. 훅을 사용하기 위해서는 클라이언트 컴포넌트로 변경해야 하는데, 이 변경이 우선은 버튼에만 필요해 버튼만 컴포넌트를 생성한다.
// Button.js
"use client";
import { useFormStatus } from "react-dom";
export default function Button() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Submitting.." : "Share Meal"}
</button>
);
}
// app/meals/share/page.js
import { shareMeal } from "@/lib/action";
import Button from "@/components/Button";
export default function ShareMeal() {
return
<form action={shareMeal}>
...
<Button />
</form>;
}
이렇게 버튼 컴포넌트만 따로 빼서 코드를 작성하게 되면 page 전체를 클라이언트 컴포넌트로 변경할 필요 없다. 즉, 필요한 부분만 클라이언트 컴포넌트로 만들어 불러올 수 있다.
# 서버 유효성 검사와 useFormState
사용자는 필수 값이 필요하지 않도록 HTML 조작할 수 있다. 이를 방지하기 위해 서버 측에서도 유효성 검사를 해준다. 서버 액션 함수 내에서 받아온 입력값이 빈 문자열이면 직렬화된 객체를 반환한다. 클라이언트 측에서는 react-dom의 useFormState훅을 사용하여 이 값을 받아올 수 있다.
"use client";
import { useFormState } from "react-dom";
import { shareMeal } from "@/lib/action";
export default function ShareMeal() {
// 첫 번째 인수에는 액션 함수를, 두 번째 인수에는 초기값을 넣어준다.
const [state, formAction] = useFormState(shareMeal, { message: null });
return (
<form action={formAction}>
{state.message && <p>{state.message}</p>}
</form>
)
}
💡리액트 19버전부터는 useFormState()가 useActionState()로 변경되었다.
# NextJS의 강력한 캐싱 기능
Next.js는 성능을 향상과 비용을 절감하기 위해 캐싱 매커니즘을 사용한다. 이로 인해 애플리케이션을 프로덕션 모드에서 실행하게 되면 사용자가 제출한 정보가 UI에 즉시 반영되지 않는 경우가 발생한다. 즉, 정적으로 렌더링 되는 페이지에서 따로 캐싱 동작을 선택하지 않으면 빌드 시에 생성된 데이터가 유지되게 된다. 이를 해결하기 위해 Next.js는 캐싱 및 재검증을 효과적으로 관리하는 기능을 제공하는데 이 중 하나가 내장 함수인 revalidatePath()이다. 경로를 첫 번째 인수로 전달하고 layout매개변수를 두 번째 인수로 전달할 수 있습니다. layout이 전달되면 지정된 페이지의 모든 하위 경로가 다시 재검증하게 된다.
revalidatePath('/meals') // meals 페이지만 캐시 재검증
revalidatePath('/meals'. 'layout') // meals 페이지를 포함한 하위 페이지들 모두 캐시 재검증
revalidatePath('/'. 'layout') // 모든 페이지 캐시 재검증
이러한 캐싱 매커니즘을 통해 개발 환경 뿐만이 아니라 배포 환경에서의 테스트도 중요함을 알 수 있다.
메타데이터
메타데이터는 웹 페이지에 대한 정보를 말하며 사용자와 검색 엔진에 페이지 콘텐츠에 대한 자세한 정보를 제공하는 역할을 한다. 이는 검색 엔진 최적화(SEO)를 개선하는데 도움이 된다. 메타데이터를 효과적으로 처리하면 검색 엔진 크롤러가 웹 페이지를 더 효과적으로 색인화하고 소셜 미디어 플랫폼에서 웹 페이지 공유를 향상할 수 있다.
# 정적 메타데이터
정적인 메타데이터를 추가하려면 layout.js 또는 page.js에서 지정된 변수인 meatadata객체를 정의하여 설정할 수 있다.
export const metadata = {
title: '...',
description: '...',
}
export default function Page() {}
# 동적 메타데이터
메타데이터를 동적으로 생성하려면 지정된 함수인 generateMetadata()를 사용하면 된다. 이 메서드를 사용하여 경로 매개변수를 읽고, 데이터를 가져와 Open Graph의 이미지도 설정할 수 있다. 아래 코드는 공식사이트에 자세히 나와있다.
export async function generateMetadata({ params }) {
// read route params
const id = params.id
// fetch data
const product = await fetch(`https://.../${id}`).then((res) => res.json())
// optionally access and extend (rather than replace) parent metadata
const previousImages = (await parent).openGraph?.images || []
return {
title: product.title,
openGraph: {
images: ['/some-specific-page-image.jpg', ...previousImages],
},
}
}
export default function Page({ params }) {}
유데미 Next.js 14 & React - 완벽 가이드를 수강하고 직접 정리한 내용입니다.
'개발 > 프론트엔드' 카테고리의 다른 글
[NextJS]App Router - 앱 최적화와 캐싱 (0) | 2024.08.01 |
---|---|
[NextJS]App Router - 라우팅(Routing) 종류 (0) | 2024.07.05 |
[WIL] ReactJS 함수형과 클래스형 (0) | 2022.08.07 |
[WIL] ReactJS 기초 (1) | 2022.07.31 |
[WIL] JavaScript의 ES란? ES5/ES6 문법 차이 (0) | 2022.07.24 |