DYO 공부하는 블로그
[Next.js] Next 정리 본문
Next란?
React 기반 풀스택 웹 프레임워크이고 SSR(서버 사이드 렌더링)과 SSG(정적 웹 페이지 생성)를 지원한다. Next는 초기 로딩 속도 개선, SPA 작동 방식의 가장 큰 문제인 SEO(검색 엔진 최적화) 문제를 해결하기 위해서 등장했다.
Next는 풀스택 프레임워크이므로 프론트, 백 구분 없이 작성 가능하고 번들러 또한 terbopack으로 지원하고 있다. ( Vercel 배포 환경에서 최적화되어 있고, 다른 배포 환경에서는 문제가 있을 수 있으니 Webpack 사용도 고려 )
다양한 내장 컴포넌트를 이용한 최적화 기능이 다수 마련되어 있고, JS 파일도 함께 내려줘야 하는 CSR(클라이언트 사이드 렌더링)방식과 달리 정적 HTML을 보내 줄 수 있는 SSR(서버 사이드 렌더링)의 속도 차이도 있기 때문에 Next를 적용했을 때 로드 속도를 크게 개선시킬 수 있다.
Vercel의 라우팅 방식
Next는 파일 기반 라우팅을 사용한다. Vercel 규칙에 따라 정해진 디렉터리명, 파일명으로 중첩 라우트, 동적 라우트를 관리 할 수 있다. 라우팅 방식은 크게 나누어 Page Router, App Router 방식이 있다. 둘은 문법 차이도 있고, 동작 철학도 다르다.
- Page Router : 주로 클라이언트 방식,
getStaticProps,getServerSideProps등으로 SSR/SSG 제어 - App Router : 주로 서버 방식, 기본적으로 RSC(리액트 서버 컴포넌트)이고, ‘use client’ 명령어로 RCC(리액트 클라이언트 컴포넌트)로 선택한다. Segment + Layout 방식 지원
아직 Page Router를 많이 사용하기도 하지만, 학습자 입장에서는 최신 기술인 App Router 위주로 다뤄보도록 하자.
1. Next.js App Router 기본 개념
1.1 메타데이터 관리
// src/app/layout.tsx
// 메타데이터는 Component export function 바깥에서 쓴다.
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
openGraph: {
title: "Triangle",
description: "다양한 작가 다양한 사진",
url: "https://triangle.com",
siteName: "Triangle",
type: "website",
images: [
{
url: "https://triangle.com/og-image.png",
width: 1200,
height: 630,
alt: "Triangle 사이트 이미지",
},
],
locale: "ko-KR",
},
twitter: {
title: "Triangle",
description: "다양한 작가 다양한 사진",
images: [
{
url: "https://triangle.com/og-image.png",
width: 1200,
height: 630,
alt: "Triangle 사이트 이미지",
},
],
},
};
핵심 개념:
- metadata 객체로 SEO 최적화
- OpenGraph와 Twitter 카드 설정으로 소셜 미디어 공유 최적화
- 루트 레이아웃에서 전역 메타데이터 설정
1.2 동적 메타데이터 생성
// src/app/(top-badge-layout)/photos/[id]/page.tsx
// Dynamic 페이지에서 메타데이터를 넣기 위해 생성
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
// 필요하다면 이렇게 가져올 수 있음. 실패 확률이 있으니 try/catch문 사용
// const data = await fetchPhotosByOne(id);
return {
title: `Triangle | Photos-${id}`
}
}
핵심 개념:
- 동적 라우트에서 generateMetadata 함수로 페이지별 메타데이터 생성
- params는 Promise로 래핑되어 있어 await 필요
2. 서버 컴포넌트와 데이터 페칭
2.1 서버 컴포넌트 기본
// src/app/page.tsx
// 10개의 랜덤 이미지를 가져와 렌더링
export default async function Home() {
// RSC에서는 Promise all을 사용할 경우 같은 시간에 동시에 보내므로 추가 처리가 필요
const data = await getRandomPhotos();
console.log(data);
return (
<div>
<div className="flex flex-col items-center py-10">
<h1 className="text-2xl font-bold">Triangle에서</h1>
<p>다양한 작가들의</p>
<p>사진을 만나보세요</p>
</div>
<ul>
<AnimatedPhotoList data={data} />
</ul>
</div>
);
}
핵심 개념:
- 서버 컴포넌트는 async 함수로 작성 가능
- 서버에서 데이터를 미리 가져와서 렌더링
2.2 Next.js Data Cache 정책 typescript
// src/utils/fetchPhotos.ts
/*
next.js(APP ROUTER)는 서버에서 fetch로 데이터를 가져올 때 Data Cache라는 저장소에 결과를 보관함.
options.cache 속성을 사용하여 캐시 정책을 설정할 수 있음.
- default :
(개발 환경) : 항상 새로운 요청
(프로덕션 환경) : '우선' 정적 데이터 사용
- Dynamic API => cookies(), headers(), searchParams() => 동적 라우팅
⭐ Next.js가 자동으로 SSR/SSG를 판단하여 빌드한다.
- force-cache : 무조건 정적 데이터만 관리하도록 함.
= export const dynamic = 'force-cache' : 이 페이지는 무조건 정적 페이지만 가져옴.
같은 URL 요청이 있으면 Data Cache에서 데이터를 가져옴. 데이터가 없으면 새로 패치해서 캐시에 저장, 이후 정적 데이터 사용
- 'no-store' : 무조건 새로운 데이터만 관리하도록 함.
= export const dynamic = 'no-store' : 이 페이지는 무조건 동적 페이지만 가져옴.
새로운 데이터만 관리하도록 함.
*/
export default async function fetchPhotos(
init: RequestInit = { cache: 'no-store' }
): Promise<Photo[]> {
const END_POINT = `${process.env.NEXT_PUBLIC_BASE_URL}/v2/list?page=5&limit=10`;
try {
const res = await fetch(END_POINT, { ...init});
if (!res.ok) {
throw new Error();
}
return await res.json();
} catch {
console.error("error!");
return [];
}
}
핵심 개념:
- cache: 'no-store': 항상 새로운 데이터 요청 (동적 페이지)
- cache: 'force-cache': 캐시된 데이터 우선 사용 (정적 페이지)
- Next.js가 자동으로 SSR/SSG 판단
2.3 ISR (Incremental Static Regeneration)
// src/utils/getRandomPhotos.ts
export const getRandomPhotos = async () => {
const promises = Array.from({ length: 10 }).map((_, i) =>
fetch(
`${process.env.NEXT_PUBLIC_BASE_URL}/seed/${Math.random()}/400/300`,
{next: {revalidate:10} }
// ISR 설정
).then((res) => res.url)
);
const urls = await Promise.all(promises);
return urls;
};
핵심 개념:
- {next: {revalidate:10}}: 10초마다 재검증하여 데이터 갱신
- ISR로 정적 페이지의 장점과 동적 데이터의 장점을 모두 활용
3. 동적 라우팅과 정적 생성
3.1 정적 파라미터 생성
// src/app/(top-badge-layout)/photos/[id]/page.tsx
// 데이터 몇개 만들어 두도록 함. 이 페이지 링크가 보인다면 다른 근처 페이지도 prefetch함.
export async function generateStaticParams() {
return [{id: '40'}, {id: '71'}, {id:'72'}]
}
// false : 미리 만들어진 페이지 이외의 다른 페이지는 접근 불가 404
// true(default) : 블로킹 콜백 -> 없으면 만들어줌. 있으면 사용함.
export const dynamicParams = true;
핵심 개념:
- generateStaticParams: 빌드 시 미리 생성할 페이지들 지정
- dynamicParams: true: 미리 생성되지 않은 페이지도 동적으로 생성 허용
3.2 404 처리
// src/app/(top-badge-layout)/photos/page.tsx
// 낫파운드 페이지로 자동으로 넘어감
if(data.length === 0) notFound();
// src/app/(top-badge-layout)/photos/[id]/page.tsx
// 404 페이지로 리다이렉트, server component에서만 사용 가능
if(!data) notFound();
핵심 개념:
- notFound(): 서버 컴포넌트에서만 사용 가능한 404 리다이렉트
- not-found.tsx 파일은 루트 폴더에 위치해야 함
4. 클라이언트 컴포넌트와 상태 관리
4.1 React Query 설정
// src/providers/QueryProvider.tsx
function QueryProvider({ children }: { children: React.ReactNode }) {
// 마운트 될 때 한번만 실행하고 메모이제이션을 사용하여 값을 저장
const [client] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1분
gcTime: 1000 * 60 * 10, // 10분
refetchOnWindowFocus: false,
refetchIntervalInBackground: false, // 백그라운드에서 refetch여부
retry: 1, // 실패시 재시도 횟수
},
},
})
);
// 데이터를 클라이언트에 저장하고 사용할 수 있게 해주는 프로바이더.
return (
<QueryClientProvider client={client}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
핵심 개념:
- useState로 QueryClient 메모이제이션
- staleTime: 데이터가 신선한 상태로 유지되는 시간
- gcTime: 캐시에서 데이터가 제거되는 시간
4.2 Server Actions와 useActionState
// src/actions/createUser.action.ts
"use server";
export async function createUser(_: unknown, formData: FormData) {
const email = (formData.get("email") ?? "").toString().trim();
const password = (formData.get("password") ?? "").toString().trim();
if (!email || !password) throw new Error("이메일과 비밀번호를 입력해주세요.");
****const res = await fetch("https://jsonplaceholder.typicode.com/users", {
method: "POST",
headers: { "Content-type": "application/json" },
body: JSON.stringify({ email, password }),
cache: "no-store",
});
if (!res.ok) {
throw new Error("회원가입 실패");
}
// 토큰을 JSON으로 돌려준다고 가정
const {accessToken, refreshToken} = await res.json();
const cookieStore = await cookies(); // server client에서만 가능
// K, V, OPTIONS
cookieStore.set('access_token', accessToken, {
httpOnly: true,
secure: true,
sameSite:'lax',
path:'/',
maxAge: 60 * 15 // 15분
})
cookieStore.set('refresh_token', refreshToken, {
httpOnly: true,
secure: true,
sameSite:'lax',
path:'/',
maxAge: 60 * 60 * 24 * 30 // 30일
})
redirect("/login");
}
// src/components/SubmitForm.tsx
function SubmitForm() {
const [state, submitAction, isPending] = useActionState(createUser, null)
return (
<form action={submitAction}>
{/* 여기에 액션을 추가 */}
<div>
<label htmlFor="">이메일</label>
<input
type="email"
name="email"
id=""
className="border-b m-2 focus:outline-none"
/>
</div>
<div>
<label htmlFor="">
비밀번호
<input
type="password"
name="password"
className="border-b m-2 focus:outline-none"
/>
</label>
</div>
<div>
<button
type="submit"
className="w-full bg-emerald-400 px-3 py-2 mt-10 font-bold rounded-md"
>
{isPending ? 'wait...' : '회원가입'}
</button>
</div>
</form>
);
}
핵심 개념:
- "use server": 서버에서만 실행되는 함수
- useActionState: Server Action의 상태를 관리하는 훅
- cookies(): 서버에서만 사용 가능한 쿠키 관리
4.3 Revalidation 전략
// src/actions/createUser.action.ts
// Actions는 서버 데이터를 추가하지만 트리거를 작동시키지는 않음.
// 이 경우는 루트를 revalidate를 시도함 ( 모든 데이터 초기화 )
// revalidatePath('/')
// 이 경우는 특정 위치를 revalidate를 시도함 ( 타겟 링크 초기화 )
// 이런 방법을 PURGE라고 함.
// revalidatePath('/photos/75')
// 이 경우는 관련 연결된 모든 페이지를 초기화함
// revalidatePath('/photos/[id]', 'page')
// 이 경우는 관련 레이아웃을 공유하는 모든 페이지를 초기화함
// revalidatePath('/(top-badge-layout)', 'layout')
// 이 경우는 특정 fetch만 초기화함
// revalidatePath('tagName')
핵심 개념:
- revalidatePath('/'): 모든 데이터 초기화
- revalidatePath('/photos/75'): 특정 페이지만 초기화 (PURGE)
- revalidatePath('/photos/[id]', 'page'): 동적 라우트의 모든 페이지 초기화
- revalidatePath('/(top-badge-layout)', 'layout'): 레이아웃 공유 페이지들 초기화
5. 라우팅과 네비게이션
5.1 클라이언트 사이드 네비게이션
// src/components/SearchForm.tsx
function SearchForm() {
// back, forward, push 등으로 CSR로 만들 수 있음.
// SSR 만들고 싶으면 redirect
const router = useRouter();
const pathname = usePathname();
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const input = e.currentTarget.querySelector("#search")! as HTMLInputElement;
const keyword = input.value.trim();
router.push(`${pathname}?q=${keyword}`)
};
핵심 개념:
- useRouter(): 클라이언트 사이드 라우팅
- usePathname(): 현재 경로 정보
- router.push(): 프로그래매틱 네비게이션
5.2 동적 라우트와 searchParams
// src/app/(top-badge-layout)/search/page.tsx
async function Page({
searchParams,
}: {
searchParams: Promise<{ q?: string }>;
}) {
// searchParams를 사용하면 dynamic으로 분류됨 ( no-store 적용 안 됨. )
const { q } = await searchParams;
const data = await fetchPhotos({cache:'no-store'});
const result = q
? data.filter((p) => p.author.toLowerCase().includes(q.toLowerCase()))
: [];
핵심 개념:
- searchParams는 Promise로 래핑
- searchParams 사용 시 자동으로 dynamic 페이지로 분류
6. 애니메이션과 GSAP
6.1 GSAP ScrollTrigger
// src/components/AnimatedPhotoList.tsx
useEffect(() => {
let ctx: { revert(): void } | undefined;
(async () => {
if (typeof window === "undefined") return;
const gsap = (await import("gsap")).default;
const { ScrollTrigger } = await import("gsap/dist/ScrollTrigger");
gsap.registerPlugin(ScrollTrigger);
ctx = gsap.context(() => {
const items = gsap.utils.toArray<HTMLElement>(".photo-item");
items.forEach((el, i) => {
gsap.from(el, {
opacity: 0,
y: 30,
duration: 0.6,
ease: "power2.out",
scrollTrigger: {
trigger: el,
start: "top 65%",
toggleActions: "play none none reverse",
// markers:true
},
});
});
}, listRef);
return () => ctx?.revert();
})();
}, []);
핵심 개념:
- 동적 import로 GSAP 로드
- gsap.context(): 성능 최적화를 위한 컨텍스트 사용
- ScrollTrigger: 스크롤 기반 애니메이션
6.2 ScrollSmoother 설정
// src/providers/ScrollSmooterProvider.tsx
useEffect(() => {
if (initialized.current) return;
let smoother: { kill(): void };
(async () => {
if (typeof window === "undefined") return;
try {
const gsap = (await import("gsap")).default;
const { ScrollTrigger } = await import("gsap/dist/ScrollTrigger");
const { ScrollSmoother } = await import("gsap/dist/ScrollSmoother");
gsap.registerPlugin(ScrollTrigger, ScrollSmoother);
// DOM이 완전히 로드된 후 실행
await new Promise((resolve) => {
if (document.readyState === "complete") {
resolve(true);
} else {
window.addEventListener("load", () => resolve(true));
}
});
smoother = ScrollSmoother.create({
wrapper: "#smooth-wrapper",
content: "#smooth-content",
smooth: 1.2,
effects: true,
normalizeScroll: true, // 모바일 호환성
ignoreMobileResize: true, // 모바일에서 resize event 무시
});
initialized.current = true;
} catch {
console.error("ScrollSmoother 초기화 실패");
}
})();
핵심 개념:
- ScrollSmoother: 부드러운 스크롤 효과
- normalizeScroll: true: 모바일 호환성
- ignoreMobileResize: true: 모바일 리사이즈 이벤트 무시
7. 로딩과 에러 처리
7.1 로딩 UI
// src/app/(top-badge-layout)/photos/loading.tsx
function Loading() {
return (
<div className="p-2">
<ul className="grid grid-cols-2 gap-2 mt-2 animate-pulse">
{
Array.from({length:8}).map((_, i) => (
<li key={i}>
<div className="w-full h-40 bg-gray-600 rounded mb-2"></div>
<div className="w-3/4 h-4 bg-gray-600 rounded"></div>
</li>
))
}
</ul>
</div>
)
}
핵심 개념:
- loading.tsx: 자동으로 로딩 UI 표시
- 스켈레톤 UI로 사용자 경험 향상
7.2 에러 바운더리
// src/app/(top-badge-layout)/photos/error.tsx
"use client";
function Error() {
return <div>Error</div>;
}
export default Error;
핵심 개념:
- error.tsx: 에러 발생 시 표시할 UI
- "use client" 지시어 필요
8. 타입스크립트 활용
8.1 타입 정의
// src/@types/type.d.ts
export interface Photo {
id: string;
author: string;
width: number;
height: number;
url: string;
download_url: string;
}
8.2 컴포넌트 Props 타입typescript
// src/components/AnimatedPhotoList.tsx
interface Props {
data: string[];
}
function AnimatedPhotoList({ data }: Props) {
// ...
}
핵심 개념:
- 인터페이스로 타입 정의
- 컴포넌트 Props에 타입 적용
'React > Next.js' 카테고리의 다른 글
| [Next] Next에서 Tanstack Query 사용하기 (0) | 2025.11.20 |
|---|