카테고리 없음

Fullstack React Apps with Next.JS

co-yong 2024. 10. 14. 13:31

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을 서버측에서 처리하도록 할 수 있다!

  1. 함수 내부에 'use server'을 작성하는 경우
    `async` 함수 내부에 'use server'를 사용하여 해당 함수가 서버에서만 실행되도록 명시할 수 있다.
    하지만 이 방식은 `use client`가 있는 페이지에서는 충돌이 발생할 수 있음
  2. 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와 같은 파일 저장 서비스를 사용하는것이 바람직함.