목차
이 글은 리액트를 했다는 전제하에 쓴 글 입니다.
페이지 라우팅
폴더 이름에 따라 자동으로 라우팅이 이루어진다.
아래는 meals 폴더에 페이지를 만들었다.
이때 컴포넌트이름과 페이지 이름은 상관이 없다.
동적 라우팅 방법
위처럼 폴더이름 [] 붙여주면 params를 받아서 쓸 수 있게 된다.
폴더이름에 따른 맞춤형 페이지
error, not- found, layout 등등
next가 자동으로 인식하여 그에 맞는 페이지를 보여준다.
layout.js
import MainHeader from "./components/header/main-header";
import "./globals.css";
export const metadata = {
title: "NextLevel Food",
description: "Delicious meals, shared by a food-loving community.",
};
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<MainHeader />
{children}
</body>
</html>
);
}
error.js
"use client";
//클라이언트 측에서 오류
export default function Error({ error }) {
return (
<main className="error">
<h1>An error occurred!</h1>
<p>{error?.message}</p>
</main>
);
}
not-found.js
export default function NotFound() {
return (
<main className="not-found">
<h1>Not found</h1>
<p>Unfortunately. we could not find the requested page or resource.</p>
</main>
);
}
nextjs 전용 이미지 피커 만들기
이때 미리보기 이미지 태그는 nextjs 전용 Image 태그를 사용해 주었다.
기존의 태그에 비해 다음과 같은 이점을 가진다.
- lazy loading
- 이미지 사이즈 최적화
- placeholder 제공
"use client";
import { useRef } from "react";
import classes from "./image-picker.module.css";
import Image from "next/image";
import { useState } from "react";
export default function ImagePicker({ label, name }) {
const imageInput = useRef();
const [pickedImage, setPickedImage] = useState();
function handlePickClick() {
imageInput.current.click();
}
function handleImageChange(event) {
const file = event.target.files[0];
if (!file) {
setPickedImage(null);
return;
}
const fileReader = new FileReader();
fileReader.onload = () => {
setPickedImage(fileReader.result);
};
fileReader.readAsDataURL(file);
}
return (
<div className={classes.picker}>
<label htmlFor={name}>{label}</label>
<div className={classes.controls}>
<div className={classes.preview}>
{!pickedImage && <p>No Image Yet</p>}
{pickedImage && <Image src={pickedImage} alt="picked!" fill />}
</div>
<input
className={classes.input}
type="file"
id="image"
accept="image/png, image/jpeg"
name={name}
ref={imageInput}
onChange={handleImageChange}
required
/>
<button
className={classes.button}
type="button"
onClick={handlePickClick}
>
Pick on Image
</button>
</div>
</div>
);
}
폼양식에서의 메소드 사용
useFormStatus
버튼의 상태가 바뀌는 것은 다음과 같이 해주면 된다.
이때 use client를 사용해 주어야 한다.
"use client";
import { useFormStatus } from "react-dom";
export default function MealsFormSubmit() {
const { pending } = useFormStatus();
return (
<button disabled={pending}>
{pending ? "Submitting..." : "Share Meal"}
</button>
);
}
useActionState
폼 제출 상태를 가져올 때 사용한다.
이때도 use client를 사용해 주어야 한다.
"use client";
import ImagePicker from "@/components/meals/image-picker";
import classes from "./page.module.css";
import { shareMeal } from "@@/lib/action";
import MealsFormSubmit from "@/components/meals/meals-form-submit";
import { useActionState } from "react";
export default function ShareMealPage() {
const [state, formAction] = useActionState(shareMeal, { message: null });
return (
<>
<header className={classes.header}>
<h1>
Share your <span className={classes.highlight}>favorite meal</span>
</h1>
<p>Or any other meal you feel needs sharing!</p>
</header>
<main className={classes.main}>
<form className={classes.form} action={formAction}>
<div className={classes.row}>
<p>
<label htmlFor="name">Your name</label>
<input type="text" id="name" name="name" required />
</p>
<p>
<label htmlFor="email">Your email</label>
<input type="email" id="email" name="email" required />
</p>
</div>
<p>
<label htmlFor="title">Title</label>
<input type="text" id="title" name="title" required />
</p>
<p>
<label htmlFor="summary">Short Summary</label>
<input type="text" id="summary" name="summary" required />
</p>
<p>
<label htmlFor="instructions">Instructions</label>
<textarea
id="instructions"
name="instructions"
rows="10"
required
></textarea>
</p>
<ImagePicker label="Your Image" name="image" />
{state.message && <p>{state.message}</p>}
<p className={classes.actions}>
<MealsFormSubmit />
</p>
</form>
</main>
</>
);
}
Server Component vs. Client Component를 언제 사용해야 할까?
Server Component와 Client Component 사이에서 선택하는 것을 간소화하기 위해, Client Component가 필요해질 때까지 Server Components(앱 디렉토리의 기본값)를 사용하는 것이 좋다.
다음 표를 참고하자
데이터 가져오기 | ✅ | ⚠️ |
백엔드 리소스에 직접 액세스하기 | ✅ | ❌ |
서버에 민감한 정보 유지하기 (액세스 토큰, API 키 등) | ✅ | ❌ |
큰 종속성을 서버에 유지/클라이언트 측 JavaScript 줄이기 | ✅ | ❌ |
상호 작용 및 이벤트 리스너 추가하기(onClick(), onChange() 등) | ❌ | ✅ |
상태 및 라이프사이클 효과 사용하기(useState(), useReducer(), useEffect() 등) | ❌ | ✅ |
브라우저 전용 API 사용하기 | ❌ | ✅ |
상태, 효과 또는 브라우저 전용 API에 의존하는 사용자 지정 훅 사용하기 | ❌ | ✅ |
React Class 컴포넌트 사용하기 | ❌ | ✅ |
nextjs에서 서버액션 사용하기
"use server";
import { redirect } from "next/navigation";
import { saveMeal } from "./meals";
import { revalidatePath } from "next/cache";
function isInvalidText(text) {
return !text || text.trim() === "";
}
export async function shareMeal(prevState, formData) {
const meal = {
title: formData.get("title"),
summary: formData.get("summary"),
instructions: formData.get("instructions"),
image: formData.get("image"),
creator: formData.get("name"),
creator_email: formData.get("email"),
};
if (
isInvalidText(meal.title) ||
isInvalidText(meal.summary) ||
isInvalidText(meal.instructions) ||
isInvalidText(meal.creator) ||
isInvalidText(meal.creator_email) ||
!meal.creator_email.includes("@") ||
!meal.image ||
meal.image.size == 0
) {
return {
message: "Invalid input",
};
}
revalidatePath("/meals");
await saveMeal(meal);
redirect("/meals");
}
이 코드에서 참고 할 것은 유효성 검사를 한 후 만족하지 않으면 message를 보내는 것이다.
여기서 준 메세지를 useActionState가 받는다.
revalidatePath
배포 이전의 환경에서는 이것을 쓰지 않아도 되지만 배포했을 때는 NextJs의 캐싱으로 인해 즉시 페이지에 반영되지 않는 것을 확인할 수 있다. 따라서 목적 경로에 따라 refresh해 주어야 한다.
페이지에 따른 메타 데이터 설정하기
정정 메타데이터 설정
metadata라는 이름만 설정하고 export 하면 알아서 next가 인식한다.
import Link from "next/link";
import classes from "./page.module.css";
import MealsGrid from "@/components/meals/meals-grid";
import { getMeals } from "@@/lib/meals";
import { Suspense } from "react";
import MealsLoadingPage from "./loading-out";
export const metadata = {
title: "All Meals",
description: "Browse the delicious meals shared by our vibrant community",
};
async function Meals() {
const meals = await getMeals();
return <MealsGrid meals={meals} />;
}
export default function MealsPage() {
return (
<>
<header className={classes.header}></header>
<h1>
Delicious Meals, created
<span className={classes.highlight}>by you</span>
</h1>
<p>
Choose your favorite recipe and cook it yourself. It is easy and fun!
</p>
<p className={classes.cta}>
<Link href="/meals/share">Share your Favorite Recipe</Link>
</p>
<main className={classes.main}>
<Suspense fallback={<MealsLoadingPage />}>
<Meals />
</Suspense>
</main>
</>
);
}
동적 메타데이터 설정
파람을 받은 후 데이터를 받아 그에 따라서 반환해 주면 된다.
이때 async 함수를 써주어야 한다.
nextjs 15 버전에서는 param을 받아올때 await를 해주어야 한다.
import { notFound } from "next/navigation";
import { getMeal } from "@@/lib/meals";
import classes from "./page.module.css";
import Image from "next/image";
export async function generateMetadata({ params }) {
const meal = getMeal((await params).mealSlug);
if (!meal) {
notFound();
}
return {
title: meal.title,
description: meal.summary,
};
}
export default async function MealsDetail({ params }) {
const meal = getMeal((await params).mealSlug);
if (!meal) {
notFound();
}
meal.instructions = meal.instructions.replace(/\n/g, "<br />");
return (
<>
<header className={classes.header}>
<div className={classes.image}>
<Image src={meal.image} fill alt={meal.title} />
</div>
<div className={classes.headerText}>
<h1>{meal.title}</h1>
<p className={classes.creator}>
by <a href={`mailto: ${meal.creator_email}`}>{meal.creator}</a>
</p>
<p className={classes.summary}>{meal.summary}</p>
</div>
</header>
<main>
<p
className={classes.instructions}
dangerouslySetInnerHTML={{ __html: meal.instructions }}
></p>
</main>
</>
);
}
//xss 공격 방어
'코딩 정보 > NextJs' 카테고리의 다른 글
[NextJs] 서버 액션과 관련하여 더 자세히 알아보자 (1) | 2025.04.13 |
---|---|
[NextJs] 서버 컴포넌트를 통한 데이터 가져오기 (0) | 2025.04.05 |
[NextJs] 라우터 기능에 대해 빠르게 알아보자 (0) | 2025.04.03 |