Next.js의 App Router

코딩덕·2025년 1월 6일

app폴더의 모든 컴포넌트는 기본적으로 서버 컴포넌트

Next.js에서는 별도로 설정하지 않는 이상 기본적으로 서버 컴포넌트이다.

  • 페이지의 대부분을 서버 컴포넌트로 구성하는 것을 권장
  • 클라이언트 컴포넌트는 꼭 필요한 경우에만 사용할 것

서버 컴포넌트와 클라이언트 컴포넌트 역할

  • 유저와의 상호작용이 있다면 클라이언트 컴포넌트
  • 유저와의 상호작용이 없다면 서버 컴포넌트

Client Component를 사용할 때

  • interacive한 동작이나 Event Listener를 사용할 때
  • React Hook(useState, useEffect...)의 사용이 필요할 때
  • Browser API 사용이 필요할 때 (web storage...)
  • Search, Button user interaction이 발생하는 영역

Server Component를 사용할 때

  • 데이터를 가져올 때 (Fetch Data)
  • 백엔드 리소스에 직접 접근할 때
  • 서버에 민감한 정보를 보관해야 할 때 (access token, API keys,...)
  • Navbar, Sidebar, Footer, Main 컴포넌트
  • 컨텐츠 및 단순 링크(a태그)등을 담는 컨테이너
  • user interaction이 없거나 불필요할 때

서버 컴포넌트 사용시 주의 점



Next.js의 데이터 캐싱

Next.js에서 백엔드로부터 데이터를 받아오는 fetch 함수를 실행할때,
받아온 데이터를 캐싱하는 4가지 방법이 존재한다.

  • no-store
  • force-cache
  • revalidate
  • tags

1. no-store

데이터 패칭시 무조건 캐싱하지 않는 방법으로, fetch 함수에 아무것도 쓰지않으면 default로 적용된다.

async function fetchData() {
    const response = await fetch('https://example.com/data'); // 캐싱하지 않음
  	// const response = await fetch('https://example.com/data', {cache: "no-store"}); // 캐싱하지 않음
    return response.json();
}

function Component() {
    const data = fetchData();  
    return <div>{data}</div>;
}

2. force-cache

데이터 패칭시 무조건 캐싱하는 방법

async function fetchData() {
    const response = await fetch('https://example.com/data', {cache: "force-cache"}); // 무조건 캐싱함
    return response.json();
}

function Component() {
    const data = fetchData();  
    return <div>{data}</div>;
}

3. revalidate

특정시간을 기준으로 캐시 업데이트

async function fetchData() {
    const response = await fetch('https://example.com/data', {next: {revalidate: 3}}); // 3초 주기로 캐시 업데이트
    return response.json();
}

function Component() {
    const data = fetchData();  
    return <div>{data}</div>;
}

4. tags

특정태그를 기준으로 캐시 업데이트

revalidateTag서버액션과 관련되어있으므로 마지막 Server Action 항목에서 다룬다.



풀 라우트 캐시

풀 라우트 캐시는 빌드 타임에 페이지를 생성하고, 렌더링 결과를 Next.js 서버의 캐시에 저장한다.

이후 브라우저에서 해당 페이지 요청이 들어오면, 새롭게 페이지를 생성하지 않고 캐시된 페이지를 반환한다.

Next.js의 페이지는 크게 Static PageDynamic Page로 나뉜다.
하지만, 풀 라우트 캐시는 Static Page에만 적용된다.
따라서 Static Page를 적극적으로 활용하여 빠른 응답속도를 제공하는 것이 좋다.

1. Dynamic Page

  • 캐시 설정이 없는 fetch 요청이 있는 페이지

  • 서버 컴포넌트(클라이언트 컴포넌트는 페이지 유형에 영향 X)

  • 접속 요청에 따라서 언제든지 시간에 따라서 자유롭게 변화할 수 있는 쿠키, 헤더, 쿼리스트링과 같은 동적 값들을 꺼내 사용하는 경우.

2. Static Page

Dynamic Page 로 분류되지 않는 페이지


✅ Dynamic Page를 Static Page로 바꾸는 방법

  • generateStaticParams 사용
    아래 함수안에 특정 id로 저장된 값은 Static 페이지로 미리 저장 해놓을 수 있다.
export function generateStaticParams() {
  return [{ id: '1' }, { id: '2' }, { id: '3' }]; // 빌드타임에 id가 1,2,3인 페이지는 미리 만들어둠
}
  • export const dynamic = "force-dynamic" 사용
    특정 페이지를 강제로 Static, Dynamic 페이지로 설정할 수 있다.
export const dynamic = "force-dynamic";
// 1. auto : 기본값, 아무것도 강제하지 않음
// 2. force-dynamic : 페이지를 강제로 Dynamic 페이지로 설정
// 3. force-static : 페이지를 강제로 Static 페이지로 설정
// 4. error : 페이지를 강제로 Static 페이지 설정 (문제발생시 빌드시 오류 발생시킴)


Request Memoization

추가로, Next.js는 페이지 렌더링 과정 중 발생하는 중복 API 요청을 방지하고 데이터 페칭을 최적화하기 위해 Request Memoization 기능을 제공한다.

이 기능은 하나의 페이지 렌더링 과정에서 중복된 API 요청을 캐싱하여 동일한 요청을 한 번만 실행되도록 보장한다. 특정코드 없이, Next.js에서 자동으로 제공해준다는 것이 특징이다.



Next.js의 스트리밍

Next.js는 웹 페이지(주로 Dynamic Page)가 데이터를 가져오는동안 유저에게 대기 상태를 시각적으로 보여주기 위해 로딩 UI를 생성하는 두 가지 방법이 있다.

  • loading.tsx 파일
  • React Suspense 사용

1. loading.tsx 파일사용법(권장 X)

  • page.tsx, layout.tsx 와 같이 Next.js에서 약속한 파일명

  • 특정 페이지에 공통으로 로딩 UI를 적용하고 싶을 경우, layout 과 동일 하게 특정 폴더 내에 loading.tsx 파일을 아래와 같이 생성하면 된다.

하지만, 몇가지 단점이 존재한다.

  1. 해당 경로아래에 있는 모든 비동기 페이지가 같은 loading.tsx파일로 스트리밍된다.
  2. 무조건 페이지 컴포넌트에만 로딩을 적용할 수 있다.
  3. 브라우저에서 쿼리스트링이 변경될때에는 작동되지 않는다.

2. React Suspense 사용법(권장 O)

특정 페이지를 구성하고 있는 세분화된 컴포넌트 각각에 로딩 UI를 적용하고 싶을 경우, Suspense를 사용하여 이 구성 요소만 스트리밍하고 페이지 UI의 나머지 부분을 즉시 표시할 수 있다.

비동기로 데이터를 받아오는 컴포넌트를 아래와 같이 Suspense로 감싸주고 fallback에 로딩 컴포넌트를 설정해주면 페이지를 구성하는 각각의 컴포넌트별로 로딩 UI를 적용할 수 있다.

import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'
 
export default function Posts() {
  return (
    <section>
      <Suspense fallback={<p>Loading Postfeed...</p>}>
        <PostFeed />
      </Suspense>
      <Suspense fallback={<p>Loading weather...</p>}>
        <Weather />
      </Suspense>
    </section>
  )
}

🔍 스켈레톤 UI

위와 같이 로딩UI를 설정할때 기본적인 로딩창 대신,
아래 이미지와 같이 뼈대 UI 컴포넌트를 보여줌으로써 사용자의 경험을 향상시킬 수 있다.



Next.js의 에러처리

Next.js에서 에러처리는 try catch문을 사용하여 처리할 수 있지만, error.tsx 파일을 설정하여 에러를 설정할 수 있다.

페이지를 이루는 각각의 컴포넌트에 error.tsx를 생성하여 각각의 컴포넌트별로 에러처리를 해줄 수 있다.

error.tsx 파일

"use client";

import { useRouter } from "next/navigation";
import { startTransition, useEffect } from "react";

export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  const router = useRouter();

  useEffect(() => {
    console.error(error.message);
  }, [error]);

  return (
    <div>
      <h3>오류가 발생했습니다</h3>
      <button
        onClick={() => {
          // 함수 하나를 인수로받아서, 해당 함수 내부의 코드를 동기적으로 실행
          startTransition(() => {
            router.refresh(); // 현재 페이지에 필요한 서버컴포넌트들을 다시 불러옴
            reset(); // 에러 상태를 초기화, 컴포넌트들을 다시 렌더링
          });
        }}
      >
        다시 시도
      </button>
    </div>
  );
}


Server Action

Server Action서버에서 실행되는 비동기 함수를 클라이언트 컴포넌트에서 접근할 수 있는 특징을 가진다.

Server Action은 데이터를 변경해야할 때 API URI 를 작성하고 이를 호출하여 처리하는게 아닌,
작성한 비동기 함수를 서버에서 직접 실행하도록 해준다.

  • 서버에서 실행되므로 보안에 강점이 있다.
  • revalidatePathrevalidateTag 등을 사용해 연관된 캐시들을 다시 업데이트 가능
  • 코드의 간결성 증가


사용법

1. 서버컴포넌트

서버 컴포넌트내에 비동기 함수를 작성하고, 함수 본문에 use server를 선언하여 Server Action을 수행한다.

export default function Page() {
  // Server Action
  async function create() {
    'use server'
    // 비동기 작업 수행
    // ...
  }
 
  return (
    // ...
  )
}

2. 클라이언트컴포넌트

클라이언트 컴포넌트내에 직접 서버 액션 함수 작성은 불가능하다.
따로 서버액션 함수를 선언한 파일을 import 하여 사용해야한다.

✅ 서버액션 함수를 선언한 파일

'use server';

import {revalidatePath} from "next/cache";

export const myServerAction = async () => {
  // 비동기 작업 수행
};

✅ 클라이언트 컴포넌트

'use client';

import { myServerAction } from '@/app/actions';
 
export function Button() {
  return (
    // ...
  )
};


서버액션 revalidate(재검증)

Next.jsrevalidate를 통해 앞서, 서버액션을 통해 데이터 패칭이 이루어진 페이지를 새로고침 없이 최신 데이터로 업데이트 해주는 기능을 수행한다.

1. revalidatePath

revalidatePath를 서버액션 함수 안에 선언해줌으로써 인자로 넘긴 페이지나 레이아웃 등을 전체적으로 업데이트해준다. 이때 풀라우터캐시, 데이터캐시 등은 모두 초기화 된다.

'use server';

import { revalidatePath, revalidateTag } from 'next/cache';

export async function myServerAction(a, b) {

  try {
    const response = await fetch(`${process.env.NEXT_PUBLIC_API_SERVER_URL}/review/`, {
      method: 'POST',
      body: JSON.stringify({a, b}),
    });
    
    // 1. 특정 주소의 해당하는 페이지만 재검증
    revalidatePath(`/book/${bookId}`);
    
    // 2. 특정 경로의 모든 동적 페이지를 재검증
    // revalidatePath("/book/[id]", "page");

    // 3. 특정 레이아웃을 갖는 모든 페이지 재검증
    // revalidatePath("/(with-searchbar)", "layout");

    // 4. 모든 데이터 재검증
    // revalidatePath("/", "layout");
    
    return;
  } catch (err) {
    console.log(err);
  }
}

2. revalidateTag

revalidateTag를 서버액션 함수 안에 선언해줌으로써 태그에 연결된 모든 데이터를 전체적으로 업데이트해준다.

✅ 태그로 데이터 패칭

먼저 데이터를 불러오는 fetch 함수에 특정 캐시 태그인 next: { tags: ['myTag'] } })를 추가

async function ReviewList({ bookId }: { bookId: string }) {
  const response = await fetch(`경로...`, { next: { tags: ['myTag'] } });
  
  return (
    // ...
  )
}

✅ revalidateTag 사용

그 후 데이터를 추가하는 함수나 삭제하는 함수에 revalidateTag("myTag") 추가

'use server';

import { revalidatePath, revalidateTag } from 'next/cache';

export async function myServerAction(a, b) {

  try {
    const response = await fetch(`${process.env.NEXT_PUBLIC_API_SERVER_URL}/review/`, {
      method: 'POST',
      body: JSON.stringify({a, b}),
    });
    
    // 5. 태그 기준, 데이터 캐시 재검증
    revalidateTag('myTag');
    
    return;
  } catch (err) {
    console.log(err);
  }
}

이러면, 결과적으로 myTag 태그를 사용하는 fetch 데이터 캐시를 제거하고 최신 데이터를 다시 가져온다.

0개의 댓글