JiSoo's Devlog

[Next.js] NextJS 핵심 본문

Frontend/Next.js

[Next.js] NextJS 핵심

지숭숭숭 2025. 3. 7. 12:25

프로젝트 생성

npx create-next-app@latest

 

 


 

 

app 폴더 -> 다양한 페이지 설정

                    새로운 폴더를 추가해 라우트로 취급하는 새로운 경로 생성 가능

                     /about이라는 라우트 지원하고 싶으면 about 폴더 생성

                     이 폴더만으로는 아무것도 할 수 없음! -> page.js 파일 추가

 

page.js 파일 -> NextJS에게 페이지를 렌더링해야 한다고 알려주는 것

서버 컴포넌트 -> 컴포넌트가 서버에 렌더링 되고 컴포넌트 함수가 서버에 실행되는 것 보장

                            서버 컴포넌트는 HTML로 렌더링 되고 전환되어 브라우저로 보내진다

 

 

페이지 간 이동

NextJS는 단일 페이지 애플리케이션에 머무르는 것을 허용하고 클라이언트 측 자바스크립트 코드로 UI 업데이트

Next 페이지의 내용은 서버에 렌더링 전이지만 클라이언트 측 자바스크립트 코드로 클라이언트 사이드에 업데이트

단일 페이지 애플리케이션에서 벗어나지 않을려면 앵커 대신 Link 사용

Link를 사용하면 다른 페이지로 이동했을 때 페이지를 벗어나는 게 아니라 서버에 렌더링 되고 클라이언트로 보내져서 클라이언트 사이드 자바스크립트 코드가 처리해서 화면에 보이는 것 업데이트

 

 

layout.js 파일 -> 하나 또는 그 이상의 페이지를 감싸는 껍데기 정의

                           모든 Next 프로젝트에는 최소 하나의 근본 layout.js 파일이 필요

                            웹사이트의 일반적인 HTML 뼈대를 잡기 위해 필수

                            children은 현재 활성 중인 page.js 파일의 내용

 

metadata -> 이 이름의 변수나 상수를 export 하면 페이지 제목, 설명 설정 가능

                      특정 레이아웃에 포함된 모든 페이지에 적용

 

icon.png -> icon이라는 이름의 이미지를 넣으면 NextJS에서 favicon으로 사용

 

not-found.js -> 'Not Found' 오류에 대한 폴백 페이지

 

error.js -> 기타 오류에 대한 폴백 페이지

 

loading.js -> 형제 또는 중첩 페이지가 데이터 가져오는 동안 표시되는 페이지

 

route.js -> API 경로 생성 

 

커스텀 컴포넌트 -> 일반 리액트 컴포넌트 추가가 가능하지만 페이지로 취급 X

 

import 경로에 @ 를 사용해 root 프로젝트 조회 가능

 

 


 

 

동적 라우트

폴더명을 [ ] 대괄호 사이에 임의 값을 넣어 중첩 폴더 생성

NextJS는 props 객체를 모든 페이지 컴포넌트에 넘긴다

모든 페이지 컴포넌트는 프로퍼티가 있는데 구조 분해 할당으로 뽑아내기 가능

 

 

Next.js에서 이미지 경로 불러올 때 src 프로퍼티를 액세스해야 한다

<img src={logoImg.src} alt="A plate with food on it"/>

 

 

루트 layout.js 파일로 임포트되는 것은 모든 페이지에서 유효하다

그 파일에서 설정된 스타일도 모든 파일에서 사용 가능

 

CSS 모듈

.module.css 로 끝나는 파일 추가하면 그 파일로부터 객체 불러올 수 있다

 

 


 

 

Image 컴포넌트로 이미지 최적화

Next.js에는 내장 이미지 요소가 있어 최적화된 방법으로 이미지 출력 가능

페이지에서 실제로 보이는 경우에만 이미지가 표시되게 이미지를 지연 로딩해 구현

소스를 src 속성값만이 아닌 전체 객체로 설정

priority 속성을 추가해 필요하지 않은 컨텐츠 변경이나 깜빡임이 없도록 가능

<Image src={logoImg} alt="A plate with food on it" priority/>

 

 


 

 

리액트 서버 컴포넌트 vs 클라이언트 컴포넌트

React.js는 순수 클라이언트 사이드 라이브러리로 브라우저에서 클라이언트 측 코드 실행

Next.js는 풀스택 프레임워크로 백엔드에서도 실행된다

Next.js 프로젝트에서 가지고 있는 모든 리액트 컴포넌트들은 서버에서만 렌더링 -> 서버 컴포넌트

기본적으로 모든 리액트 컴포넌트는 서버에서만 렌더링된다 -> 브라우저에서 실행 X

개발자 서버 시작하는 터미널 확인해 보면 서버 사이드 코드가 돌아가고 있고 실행 중인 로그 확인 가능

서버 컴포넌트를 사용하면 다운로드해야 하는 클라이언트 측 자바스크립트 코드가 줄어들어 웹사이트 성능 향상, 검색엔진 최적화에도 좋다

Next.js에서도 클라이언트 컴포넌트 생성 가능 -> 서버에서 사전 렌더링되는 것들이고 잠재적으로 클라이언트에 렌더링 될 수 있다

useState나 useEffect 같은 훅들은 서버 측에서 사용 불가

eventHandler는 클라이언트 컴포넌트에서만 사용 가능

Next.js에서 클라이언트 컴포넌트 만들려면 "use client" 지시어 사용

 

 

usePathName 훅으로 현재 활동 경로에서 도메인 다음 부분을 준다 -> /community, /meals

const path = usePathname();
.
.
.

<Link
  href="/meals"
  className={
  path.startsWith("/meals") ? classes.active : undefined
  }
>
  Browse Meals
</Link>

startWith 사용하면 /meals 로 시작하는 경우에 링크 활성으로 설정 가능, 중첩 페이지도 가능

usePathName은 클라이언트 컴포넌트에서만 작동하기 때문에 "use client" 추가 필요

서버 컴포넌트의 이점을 유지하려면 컴포넌트 트리를 가능한 아래로 내려가서 use client를 추가하는 게 좋다

 

 


 

 

SQLite 데이터베이스

npm install better-sqlite3

추가적인 설정이나 데이터베이스 없이 로컬에서 사용 가능한 SQL 데이터베이스

node 파일명

데이터베이스 파일 실행

import sql from 'better-sqlite3';

const db = sql('meals.db');

export async function getMeals(){
  db.prepare('SELECT * FROM meals').all();
}

all은 데이터를 불러올 때 사용

run은 데이터를 바꿀 때 사용

get은 한가지 열만 찾을 때 사용

async로 함수를 바꿔주면 proimse가 리턴된다

 

import { getMeals } from "@/lib/meals";

export default async function MealsPage() {
  const meals = await getMeals();
 .
 .
 .
 <main className={classes.main}>
    <MealsGrid meals={meals} />
 </main>

 

 

세분화 로딩 상태 관리

데이터 가져오는 부분을 별도 컴포넌트로 분리

async function Meals() {
  const meals = await getMeals();

  return <MealsGrid meals={meals} />;
}

export default function MealsPage() {
  return (
    <>
      <header className={classes.header}>
        ...
      </header>
      <main className={classes.main}>
        <Meals />
      </main>
    </>
  );
}

 

Suspense는 리액트에서 제공된 컴포넌트로 일부 데이터 또는 리소스가 불러올 때까지 로딩 상태 처리, 대체 컨텐츠 표시

fallback 속성을 사용해 래핑된 컴포넌트가 일부 데이터 로딩하는 동안 표시될 대체 컨텐츠를 찾는다

<main className={classes.main}>
  <Suspense
     fallback={<p className={classes.loading}>Fetching meals...</p>}
  >
    <Meals />
  </Suspense>
</main>

 

 


 

 

오류 처리 방법

error.js 파일을 추가해 페이지나 컴포넌트가 생성할 수 있는 잠재적 에러를 다룬다

Next.js는 일부 props를 전달하는데 error 속성이 이 오류에 대한 정보를 더 가지고 있다

error.js에 저장된 error 컴포넌트는 꼭 클라이언트 컴포넌트여야 한다 -> 발생하는 오류를 포함한 해당 컴포넌트의 오류들을 잡을 수 있게 보장하기 때문에

 

Not Found 처리 방법

잘못된 URL을 입력했을 때 기본 404 페이지가 나오는데 not-found.js 파일로 수정 가능

 

사진파일의 정확한 크기를 모를 때 fill 속성 사용

 

 

경로 매개변수를 이용한 렌더링

const meal = getMeal(params.mealSlug);

mealSlug가 데이터베이스에서 meal을 가져올 수 있게 해주는 식별자가 된다

추가지연이나 특정 로딩 페이지가 있길 원하지 않으면 async 키워드 없애기

 

NotFound 함수

next/navigation에서 import 가능한 notFound 함수를 사용하면 제일 가까운 not-found나 오류화면을 보여줄 수 있다

 

onClick 속성에 어떤 함수나 값을 설정하거나 다른 이벤트 핸들러들은 서버 컴포넌트에 사용 X -> "use client"

 

이미지 미리보기

FileReader 클래스를 사용해 Data URL이라는 것으로 변환

const fileReader = new FileReader();
fileReader.readAsDataURL(file);

 

 

양식 제출을 위한 서버 액션

'use server' 지시어 사용

'use server'는 Server Action이라는 것을 생성하는데 오직 서버에서만 실행될 수 있게 보장해 주는 기능

함수를 진짜 서버 액션으로 바꾸려면 async를 붙여줘야 한다

서버 액션을 가지고 form의 action 속성에 값으로 할당 가능

<form className={classes.form} action={shareMeal}>

action 속성에 서버 액션 기능을 설정하고 폼이 제출되면 NextJS가 자동으로 요청을 생성해 웹사이트를 제공하는 NextJS 서버로 보내게 된다

async function shareMeal(){
    'use server';
  }

이 함수는 자동적으로 제출된 formData를 받게 된다

formData 객체는 get 메소드를 갖고 있는데 input 필드에 입력된 값을 얻기 위해 사용할 수 있고 input 필드는 name으로 구분된다

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')
    };

이 방법은 추가되는 컴포넌트가 클라이언트 컴포넌트가 아닐 때에만 동작한다

컴포넌트 내 어딘가에 'use client'를 사용했으면 에러 발생 -> 서버 액션은 클라이언트 컴포넌트 파일에서 사용 X

 

 

개별 파일로 서버 액션 저장

파일 상단에 'use server'를 적어주면 해당 파일에서 정의하는 모든 함수가 서버 액션이 된다

서버 측 코드가 클라이언트 측에 위치하면 보안 문제나 다른 문제가 발생할 수 있다

서버 액션을 다른 파일에서 임포트해서 클라이언트 컴포넌트에서 사용 가능

'use server';

export async function shareMeal() {

  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')
  };

  console.log(meal);
  
}

 

 

XSS 보호를 위한 슬러그 생성

npm install slugify xss

slugify 패키지, xss 패키지 설치 -> 크로스 사이트 스크립팅 공격 방지

export function saveMeal(meal) {
  meal.slug = slugify(meal.title, { lower: true });
  meal.instructions = xss(meal.instructions);
}

 

slugify 함수를 실행해 slug 생성하고 slug의 설정 객체를 설정해 모든 문자를 소문자로 설정

xss 함수로 instructions에서 해로운 컨텐츠 제거

 

 


 

 

업로드된 이미지 저장

이미지는 데이터베이스가 아닌 파일 시스템에 저장되어야 한다

Node JS가 제공하는 파일 시스템 API 사용

fs 모듈을 통해 createWriteStream() 함수 실행해 어떤 파일에 데이터를 쓸 수 있게 하는 stream 생성

createWriteStream() 함수는 파일을 쓰고 싶은 경로 path가 필요

path 뒤에는 반드시 파일명 입력

const stream = fs.createWriteStream(`public/images/${fileName}`);

stream에 write() 함수를 쓸 수 있는데 인수로 chunk가 필요하다

이미지는 buffer로 변환해야 하는데 이미지 객체를 이용해 arrayBuffer 함수 사용

arrayBuffer 함수가 promise를 반환하고 buffer로 변환되게 되기 때문에 await 키워드 사용해야 한다

write 함수는 일반 buffer가 필요하기 때문에 Buffer.from() 함수를 사용하고 arrayBuffer를 인수로 전달

const bufferedImage = await meal.image.arrayBuffer();

stream.write(Buffer.from(bufferedImage));

write 함수의 첫 번째 인수는 저장할 파일, 두 번째 인수는 쓰기를 마치면 실행될 함수

데이터베이스는 파일 저장하는 곳이 아니므로 파일 경로만 저장

meal.image = `/images/${fileName}`;

public을 지우는 이유는 모든 이미지 요청은 자동으로 public으로 보내져서 이미지 요청이 보내졌을 때 public이 포함되지 않게 해야 한다

 

 

useFormStatus으로 양식 제출 상태 관리

status 객체를 받고 요청이 진행 중이면 true, 아니면 false인 pending 속성을 가진다

status는 객체 구조 분해 할당을 통해 한 가지 속성만 추출 가능

"use client";
import { useFormStatus } from "react-dom";

export default function MealsFormSubmit() {
  const { pending } = useFormStatus();

  return (
    <button disabled={pending}>
      {pending ? "Submitting..." : "Share Meal"}
    </button>
  );
}

 

 

useActionState 훅

서버 액션을 통해 제출될 form을 사용하는 페이지나 컴포넌트의 상태를 관리한다

첫 번째 인수는 form이 제출될 때 동작하는 실제 서버 액션

두 번째 인수는 컴포넌트의 초기 상태로 돌아오기 전에 useActionState가 반환할 초기값

useActionState는 두 요소가 든 배열 반환

const [state, formAction] = useActionState(shareMeal, { message: null });

 

 


 

 

 

NextJS 캐싱 구축

npm run build

↑ 서버에 배포할 수 있는 프로젝트를 만든다

npm start

↑ 최적화된 코드의 배포 서버 실행 가능

npm run build -> npm start

동일한 주소로 서버가 열리지만 최적화된 코드로 서버가 열린다

npm run build를 실행하면 NextJS는 실제로 앱에서 사전 생성될 수 있는 모든 페이지를 사전 렌더링하고 생성해 기본적으로 동적 웹페이지가 아니게 된다

revalidatePath 함수는 특정 path에 속하는 캐시의 유효성 재검사를 하게 한다

revalidatePath('/meals', 'page');

이렇게 하면 meals 경로는 유효성이 다시 검사 되고 중첩 path는 영향을 받지 않는다

revalidatePath의 두 번째 인수로 layout을 전달하는데 이 인수의 기본값은 page로 path의 이 페이지만 재검사하겠다는 뜻

layout을 설정하면 중첩된 모든 페이지를 재검사하게 된다

유효성 재검사는 NextJS가 해당 페이지에 연관된 캐시를 비우는 것을 의미한다

 

업로드한 이미지 클라우드에 저장(AWS S3)

업로드된 파일을 로컬 파일 시스템에 저장하는 것은 이상적이지 않다

AWS S3와 같은 클라우드 파일 저장소를 통해 저장하는 게 좋다

 

 


 

 

정적 메타데이터 추가

page.js 파일에 metadata 추가

export const metadata = {
  title: "All Meals",
  description: "Browse the delicious meals shared by our vibrant community.",
};

 

동적 메타데이터 추가

generateMetadata라는 async 함수를 export 해서 등록해야 한다

이 함수는 페이지 컴포넌트가 속성으로 받는 것과 동일한 데이터를 받는다

export async function generateMetadate({ params }) {
  const meal = getMeal(params.mealSlug);

  if (!meal) {
    notFound();
  }
  
  return {
    title: meal.title,
    description: meal.summary,
  };
}

 

728x90

'Frontend > Next.js' 카테고리의 다른 글

[Next.js] 데이터 변이  (0) 2025.03.17
[Next.js] 데이터 가져오기  (0) 2025.03.12
[Next.js] 라우팅 및 페이지 렌더링  (0) 2025.03.08