Fullstack React Apps with Next.JS
Next.js에 대해 알아보자!!
- What is Next.JS & Why Would You Use it?
- Routing, Pages & Server Components
- Fetching & Sending Data
- Styling, Image Upload & Managing Page Metadata
npx create-next-app@latest <project-name>
* 필자는 Next.js, ts기반으로 사용했다는 것을 알려드립니다. (jsx -> tsx)
파일 기반 라우팅과 리액트 서버 컴포넌트의 이해
보호된 파일명
중요: 이 파일명들은 app/폴더(부 폴더 포함) 내부에서 생성될 때만 보호됩니다.
app/폴더 외부에서 생성될 경우 이 파일명들을 특별한 방식으로 처리하지 않습니다.
다음 목록은 NextJS에서 보호된 파일명이며 이 섹션에서 중요한 파일명을 배울 것입니다:
- page.js => 신규 페이지 생성 (예: app/about/page.js은 <your-domain>/about page을 생성)
내부에서 export하는 함수의 이름은 중요하지 X - layout.js => 형제 및 중첩 페이지를 감싸는 신규 레이아웃 생성
- not-found.js => ‘Not Found’ 오류에 대한 폴백 페이지(형제 또는 중첩 페이지 또는 레이아웃에서 전달된)
- error.js => 기타 오류에 대한 폴백 페이지(형제 또는 중첩 페이지 또는 레이아웃에서 전달된)
- loading.js => 형제 또는 중첩 페이지(또는 레이아웃)가 데이터를 가져오는 동안 표시되는 폴백 페이지
- route.js => API 경로 생성(즉, JSX 코드가 아닌 데이터를 반환하는 페이지, 예: JSON 형식)
- icon.png : favicon으로 사용. (탭에 보이는 작은 아이콘 이미지)
디렉토리 경로간의 파일 라우팅에서의 주의점
<a> 링크를 클릭해 새로운 페이지로 이동 ? 현재 페이지를 벗어나 새로 페이지를 다운받게 됨. (재로딩)
SPA가 아니게 됨...
=> Next.js의 장점을 이용하자! <Link href=""> </Link>를 사용하자.
라우팅에 따른 파일명
[ ]를 통한 동적 라우팅
post-1, post-2 와 같은 동적 라우팅을 하기 위해선 대괄호를 사용할 수 있다. (Next.js의 특수 문법)
폴더 경로 [~] -> 나중에 정해짐. params prop이 자동으로 전달되며, 객체들이 url의 key로서 사용
경로 상 [key]과 url의 ~ /value , key-value mapping되어 사용
만약 정적으로 지정된 형제 라우트가 있다면, 그것을 우선시.
ex ) meals / [mealSlug] 와 meals/share 페이지가 있는데, meals/share로 접속한다해도 동적으로 작동 X!
ex) [id].js 파일
import { useRouter } from 'next/router';
export default function ItemPage() {
const router = useRouter();
const { id } = router.query; // URL에서 id 값을 가져옴
if (!id) {
return <p>Loading...</p>;
}
return (
<div>
<h1>Item ID: {id}</h1>
{/* id를 사용하여 데이터를 가져와 렌더링 */}
</div>
);
}
( )로 그룹화
폴더 이름에 괄호를 사용하면 해당 경로가 URL에 포함되지 않는다.
이를 통해 중첩 레이아웃을 구성하거나 페이지를 논리적으로 그룹화할 수 있다.
_ (언더스코어)로 시작하는 폴더
언더스코어로 시작하는 폴더는 라우팅 시스템에서 무시됩니다.
예를 들어 `_components`, `_utils` 같은 폴더에서 사용할 수 있다. (UI 로직이나 유틸리티 함수들을 분리하여 관리)
File Colocation
File Colocation은 관련된 파일을 논리적 흐름에 맞게 물리적으로 가까운 위치에 두는 것
예를 들어, 컴포넌트 파일과 관련된 스타일 시트나 테스트 파일을 같은 폴더에 두는 방식
@를 통한 Root 경로 활용
@로 시작하는 경로는 프로젝트의 루트 경로를 참조하는 방식
예를 들어 `@/components/Button`은 `src/components/Button.js` 파일을 참조
tsconfig.json에서 기본 경로를 수정할 수 있다.
보호된 파일명 전체 내용 : https://nextjs.org/docs/app/api-reference/file-conventions
프로젝트 구성 스타일 관련 : https://nextjs.org/docs/app/building-your-application/routing/colocation
MetaData 설정
export const metada = ~ 를 설정함으로써 페이지 제목 및 설명에 대해 설정 가능.
( <head/>를 대체, page나 layout 파일 내부에 작성)
!만약 metadata를 동적으로 생성하고 싶다면...?
export async function generateMetadata( {params} ) {
if(!meal) {notFound();}
return {
title : meal.title,
description : meal.summary,
};
}
params는 페이지 컴포넌트가 속성으로 받는것과 동일한 데이터를 받아옴.
초기, meal은 undefined 상태이므로 if문을 통해 조건을 걸어주어야 함.
* 모든 파일명에 대해 소문자로 시작. layout과 page는 결국 react component임.
page,layout을 제외한 components 파일을 app 폴더 외부에 두는 것을 강의자는 선호.
=> app폴더에서는 라우팅만을 다루도록.
metadata 관련: https://nextjs.org/docs/app/api-reference/functions/generate-metadata
Next의 Image
기존의 <img/> 보다 next/image의 <Image/> 를 사용하는 것을 권장.
자동으로 크기 최적화등 다양한 기능 제공.
이미지의 크기를 사전에 알 수 없는 경우라면, fill을 통해 맞추도록 설정.
https://nextjs.org/docs/app/api-reference/components/image
Server vs Client Components
Server-side (Backend)
The backend executes the server comopnent functions & hence derivers the to-be-rendered HTML code
React Server Components(RSC)
Components that are only rendered on the server
By default, all React components (in NextJS apps) are RSCs
Advantage : Less client-side JS, great for SEO
Clinet-side (Frontend)
The client-side receives & renders the to-be-rendered HTML code
Client Components
Components that are pre-rendered on the server but then also potentially on the client
Opt-in via "use client" directive
Advantage : Client-side interactivity
useState, useEffect, eventHandler과 같은 것들은 반드시 client-side에서 수행되어야 함.
usePathname(); 도메인 뒷부분의 현재 접속 경로를 제공해줌.
=> 현재 접속중인 nav의 부분에 하이라이트를 표시하고자 할 때 사용. Link에 동적으로 className부여.
로딩 페이지 구현하기
데이터를 로딩하는 페이지 옆에 loading.js 파일 추가.
실제 비동기 작업이 진행중일 때에만 해당 페이지를 표시해줌.
리액트에서 제공해주는 Suspend & Streamed Response를 통한 세분화 로딩 상태 관리
<Suspense fallback = {} >
<Meals />
</Suspense>
에러페이지 구현하기
error.js
client component로 만들어야만 함.
경로에 따라 error를 다룰 페이지를 정할 수도 있고, root 경로에 만든 후 코드를 통해서도 처리 가능.
404 Not found 페이지 구현하기
not-found.js
특정 경로가 존재하지 않을 때 Next.js가 렌더링하는 404 페이지, 기본 404 페이지를 커스터마이즈
export default function NotFound() {
return (
<div className="flex flex-col items-center justify-center h-screen">
<h1 className="text-2xl font-bold">404 - Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
</div>
);
}
notFound(); 제일 가까운 notfound나 error페이지를 보여줌.
존재하지 않는 데이터에 데한 요청을 하거나... etc
사용자 입력받기 - Form 작성
useFormStatus ( )
Form의 제출 상태를 관리한다. (pending, success, error)
제출 버튼을 눌렀을 때 더 이상 버튼을 클릭하지 못하게 하거나,
제출 중이라는 상태를 사용자에게 보여줄 수 있다.
- 클라이언트 컴포넌트에서만 사용 가능
- From 내부에서만 사용 가능
- React의 react-dom에서 import
클라이언트 사이드 입력 유효성 확인
- HTML (Input의 속성을 활용)
- type - email, number, file, submit, date, checkbox, radio, url...
- required
- min, max
- JavaScript : addEventListner와 같은 이벤트롤 사용해 실시간 검사
- React : 상태관리 및 훅을 이용해 검사
서버 사이드 입력 유효성 확인 추가 방법
- 공백 확인 : trim( ) 사용
- 이메일 형식 확인 : @ 포함 여부 확인 (고급- 정규식 사용)
- 파일 크기 확인 ㅣ 파일의 사이즈가 0인지, 너무 크진 않는지 확인
- 파일 확장자 확인
- 날짜 형식 및 유효성 확인
- 데이터 중복확인
- XSS 방지
서버에 데이터가 넘어오면서 변형되었을 수 있기에, 이중으로 유효성 검사를 수행하여야 한다.
Servr Action
Server Action 정의시 서버에서 실행되는것을 보장해주어야함!
Next.js의 파일들은 기본적으로 백엔드에 위치하기에, Form을 서버측에서 처리하도록 할 수 있다!
- 함수 내부에 'use server'을 작성하는 경우
`async` 함수 내부에 'use server'를 사용하여 해당 함수가 서버에서만 실행되도록 명시할 수 있다.
하지만 이 방식은 `use client`가 있는 페이지에서는 충돌이 발생할 수 있음 - Server Action을 별도의 모듈로 관리 (권장)
Next.js에서는 `lib` 폴더를 사용하여 Server Action을 별도로 관리하는 방식을 권장한다.
해당 파일의 맨 위에 `'use server'`를 명시하여 모든 함수가 서버에서만 실행되도록 보장
'use server';
import slugify from 'slugify';
import xss from 'xss';
export interface FormData {
title: string;
description: string;
}
// 폼 데이터를 처리하는 서버 액션
export async function handleFormSubmission(formData: FormData) {
// XSS 방지 및 입력 데이터 처리
const sanitizedTitle = xss(formData.title.trim());
const sanitizedDescription = xss(formData.description.trim());
// 슬러그 생성 (URL-friendly)
const slug = slugify(sanitizedTitle, { lower: true, strict: true });
// 데이터베이스에 저장하는 로직 (예시)
// await saveToDatabase({ title: sanitizedTitle, description: sanitizedDescription, slug });
// 처리 완료 후 리다이렉트 또는 응답
return { message: '폼이 성공적으로 제출되었습니다!' };
}
폼 제출 시 Server Action 호출
Next.js에서 `Server Action`과 함께 `<form>` 태그를 사용할 수 있습니다.
**form 데이터**는 서버 측에서 **`FormData`** 객체로 수집되며, 이를 통해 데이터를 쉽게 접근하고 처리할 수 있습니다.
'use client';
import { useState } from 'react';
import { handleFormSubmission, FormData } from '../lib/serverActions';
export default function MyForm() {
const [formData, setFormData] = useState<FormData>({ title: '', description: '' });
const [message, setMessage] = useState<string>('');
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsSubmitting(true);
try {
// 서버 액션 호출 및 폼 데이터 처리
const response = await handleFormSubmission(formData);
setMessage(response.message);
} catch (error) {
setMessage('폼 제출 중 오류가 발생했습니다.');
console.error('폼 제출 오류:', error);
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="title">제목</label>
<input
type="text"
id="title"
name="title"
value={formData.title}
onChange={handleChange}
required
/>
</div>
<div>
<label htmlFor="description">설명</label>
<textarea
id="description"
name="description"
value={formData.description}
onChange={handleChange}
required
/>
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '제출 중...' : '제출'}
</button>
{message && <p>{message}</p>}
</form>
);
파일 업로드 처리 (이미지는 파일 시스템에 저장)
Next.js에서 이미지 파일이나 기타 파일을 업로드할 때는 DB에 저장하기보다는 파일 시스템에 저장하는 것을 권장
이미지는 보통 서버의 `public` 폴더에 저장. ( 파일 시스템 모듈을 사용)
파일 시스템 API (fs) : 파일의 확장자 및 파일명을 확인한 뒤, 지정된 경로에 파일을 저장합니다.
데이터베이스에 데이터 저장 (SQL 예시)
데이터베이스 클라이언트(`pg`, `mysql`, `sqlite` 등)를 사용하여 저장하는 로직을 추가
일반적으로 SQL 문을 사용하여 데이터를 삽입
리다이렉션 처리
폼 제출 후 ' redirect()' 함수를 사용하여 서버 측에서 사용자를 특정 URL로 리다이렉트
XSS보호를 위한 슬러그 생성 및 유저 입력 무결 처리하기
npm install slugify xss
- XSS 보호: xss 라이브러리는 사용자의 입력에 포함된 악의적인 스크립트를 제거하여 보안 문제를 방지합니다.
- 슬러그 생성: slugify를 사용하여 제목을 URL-friendly 문자열로 변환합니다.
예를 들어, 제목이 "My New Post!"라면 슬러그는 my-new-post가 됩니다.
// lib/serverActions.js
'use server';
import slugify from 'slugify'; // 슬러그 생성용
import xss from 'xss'; // XSS 방지용
import fs from 'fs'; // 파일 시스템 접근을 위한 Node.js fs 모듈
export async function handleFormSubmission(formData) {
// 폼 데이터 수집 및 XSS 보호
const title = formData.get('title');
const description = formData.get('description');
const email = formData.get('email');
const image = formData.get('image'); // 이미지 파일
// XSS 방지를 위해 입력값을 정제
const sanitizedTitle = xss(title);
const sanitizedDescription = xss(description);
const sanitizedEmail = xss(email);
// 슬러그 생성 (URL에 적합한 형식으로 변환)
const slug = slugify(sanitizedTitle, { lower: true, strict: true });
// 이미지 파일을 public 폴더에 저장
const imageName = `${slug}-${Date.now()}.jpg`; // 고유한 파일 이름 생성
const imagePath = `./public/uploads/${imageName}`;
// 이미지 파일을 파일 시스템에 저장
const buffer = await image.arrayBuffer(); // 파일 데이터를 ArrayBuffer로 변환
fs.writeFileSync(imagePath, Buffer.from(buffer), (error) => {
if (error) throw new Error('이미지 저장 중 오류 발생');
});
// 데이터베이스에 저장하는 코드 (예시, 실제 DB 코드는 필요에 맞게 작성)
// await db.prepare('INSERT INTO posts (title, description, email, image, slug) VALUES (?, ?, ?, ?, ?)')
// .run(sanitizedTitle, sanitizedDescription, sanitizedEmail, imageName, slug);
// 성공적으로 폼 처리가 완료되면 리다이렉트
redirect('/thank-you');
}
Cashing
NextJs 캐싱 구축 및 이해
개발 환경 => 배포 환경
npm run build
npm start
배포 서버 시작
실제로 사전 생성할 수 있는 모든 페이지를 사전 렌더링하고 생성 (공격적인 캐싱)
=> 페이지를 재생성하지 않는다면 데이터를 새로 가져오지 않고 이전에 불러온걸 사용하는 문제가 발생...
=> 캐시의 전체 혹은 일부를 비우도록 설정.
접속한 모든 페이지에 대하여 캐싱을 수행. (해당 페이지의 데이터 포함)
새로고침(떠났다가 다시 돌아올 경우)에만 다시 수행
revalidatePath ( ) : 특정 path에 속하는 캐시의 유효성을 재검사하도록 함.
이전에 public/assets에 이미지를 저장해두고 있기에,
배포 환경에서는 새롭게 추가한 이미지를 불러 올 수 없음. (public은 배포할 필요 x)
=> 런타임에 생성된 모든 파일은 AWS S3와 같은 파일 저장 서비스를 사용하는것이 바람직함.