☁️ goormTIL | Next.js #62

매루·2025년 12월 5일

goormTIL

목록 보기
60/67
post-thumbnail

📅 2025-12-05

➡️ Next.js Router Handler, Server Action에 대해 새롭게 알게 된 것 또는 헷갈리는 부분 정리


🔎 학습 리마인드

📌 Router Handler

  • Next.js의 app 디렉토리 안에서 특정 경로에 대한 API 엔드포인트를 만드는 기능
  • 브라우저의 Request / Response API를 직접 다룰 수 있으며, Next.js 안에서 간단한 백엔드 서버 역할을 수행할 수 있음

💡 파일 규칙

  • Route Handler는 반드시 app 디렉토리 안에서만 작동
    app/api/newBooks/route.ts → GET /api/newBooks
  • 파일 내부에서 HTTP 메서드별 함수를 통해 요청 처리
    export async function GET(request: Request) {}
    export async function POST(request: Request) {}
    export async function PUT(request: Request) {}
    export async function DELETE(request: Request) {}
  • Server / Client Component 어디서든 호출 가능
  • 독립적인 REST API 구성 가능

💡 예시

// app/api/books/route.ts
import { Book } from "@/types/book";

export async function GET() {
  const res = await fetch("http://localhost:4000/books");
  const data: Book[] = await res.json();
  
  return Response.json({ data: data });
}
  • http://localhost:3000/api/books 로 요청 시 정상 응답 (Postman 등 API 테스트 도구 사용 가능)

💡 언제 Route Handler를 사용할까?

  1. API 요청 시 민감 정보(API KEY, Token 등) 숨기고 싶을 때

    • 클라이언트에서 직접 노출 방지
  2. API 요청 시 CORS 문제 우회하고 싶을 때

    • 브라우저 → 서버 요청 중간에 Proxy 역할
  3. 외부 서비스나 앱에서도 사용할 수 있는 REST API 제공하고 싶을 때

    • Next.js 자체가 백엔드 역할 수행

💡 CORS 문제 해결과 Route Handler

Same-Origin Policy (동일 출처 정책)

  • 브라우저 보안 규칙으로 같은 도메인 간 요청만 허용
  • 예시 mywebsite.commywebsite.com/api ✅ (가능) mywebsite.comanother-website.com/api ❌ (CORS 에러)

CORS (Cross-Origin Resource Sharing)

  • 다른 도메인 간 데이터 요청을 허용하기 위한 방식
  • 서버에서 Access-Control-Allow-Origin 헤더를 통해 허용 도메인 지정

❓ CORS 에러란?

  • 서버가 허용하지 않은 출처에서 요청이 들어온 경우, 브라우저가 보안 정책으로 응답을 차단하는 현상

    • 즉, 브라우저가 보안상 적용하는 규칙
  • 동작 과정

    1. 클라이언트에서 API 요청 (fetch, axios 등)
    2. 브라우저가 CORS 정책 검사
    3. 서버 응답에서 Access-Control-Allow-Origin 헤더 확인
    4. 허용 시 응답 처리 / 아니면 차단

Simple Request(일반 요청) vs Preflight Request(사전 요청)

  • CORS 요청은 일반요청과, 사전요청으로 나뉨

    종류설명
    Simple Request바로 요청 가능 (일반 GET / POST)
    Preflight Request (OPTIONS)PUT / DELETE / PATCH 또는 커스텀 헤더 포함 시 사전 확인 요청 필요
  • 정상 요청 흐름

    1. OPTIONS 요청으로 사전 확인 (필요 시)
    2. 브라우저가 API 요청 (fetch 실행)
    3. 백엔드가 요청을 받고 응답 (200 OK + Access-Control-Allow-Origin 포함)
    4. 브라우저가 응답 확인
      • 허용됨 ✅ → 정상적으로 데이터 받음
      • 허용 안 됨 ❌ → CORS 에러 발생

💡 proxy란?

  • 브라우저 대신 외부 API 요청을 보내주는 중간 서버
  • 클라이언트가 직접 외부 도메인에 요청할 경우 발생하는 CORS 문제를 우회
  • Next.js Route Handler를 이용하여 proxy API 구성 가능
    // Next.js API Route (app/api/proxy/route.ts)
    import { NextResponse } from "next/server";
    
    export async function GET() {
      const response = await fetch("https://api.example.com/data"); // 실제 API 요청
      const data = await response.json();
    
      return NextResponse.json(data, {
        headers: {
          "Access-Control-Allow-Origin": "*", // 모든 도메인에서 접근 가능
        },
      });
    }

📌 Server Action

  • 서버에서만 실행되는 비동기 함수
  • React Component에서 직접 호출 가능 → API URL 불필요
  • 컴포넌트 내부에서 함수 호출처럼 사용
  • 메서드(GET/POST) 구분 없이 사용 → 내부적으로 POST로 처리
  • Form 동작 및 CRUD(Create, Update, Delete)에 최적
  • revalidatePath, revalidateTag 를 통해 자동 데이터 갱신 지원

💡 revalidatePath 및 revalidateTag

  1. 데이터 변경 후 캐시를 최신 상태로 유지하는 용도
  2. 서버 액션(또는 서버 컴포넌트) 내부에서만 호출 가능
  3. UI를 자동으로 최신 데이터로 업데이트

revalidateTag

  • 특정 데이터 그룹만 골라서 무효화

    revalidateTag("books");
    • "books" 태그가 붙은 모든 fetch 캐시 무효화
    • 다음 요청 시 새 데이터 fetch
  • TanStack Query 비교 → invalidateQueries(key)와 유사

    queryClient.invalidateQueries({ queryKey: ["books"] });
    • ["books"] key로 캐싱된 모든 데이터를 무효화
    • 관련 컴포넌트 자동 refetch

revalidatePath

  • 특정 페이지 전체에 대한 캐시 무효화

    revalidatePath("/dashboard");
    • /dashboard에서 사용된 모든 fetch 캐시 무효화
    • 다음 요청 시 페이지 전체가 최신 데이터로 재렌더링
  • TanStack Query 비교 → invalidateQueries 여러 개 + 페이지 전체 재검증

    queryClient.invalidateQueries(["products"]);
    queryClient.invalidateQueries(["user"]);
    queryClient.invalidateQueries(["recommendations"]);
    • 여러 데이터를 각각 무효화해야 함

💡 Server Action 작성 규칙

  • 파일 최상단에 반드시 "use server" 선언
    // app/server-action.ts
    "use server";
    
    import { Book } from "@/types/book";
    import { BASE_URL } from "./constants/api";
    
    export async function getBooks() {
      const res = await fetch(`${BASE_URL}/books`, {
        cache: "no-store",
      });
    
      const data: Book[] = await res.json();
    
      return { data };
    }
  • 클라이언트 컴포넌트에서 호출
     export default async function Home() {
      //const res = await fetch("http://localhost:4000/books");
      //const books: Book[] = await res.json();
    
      const { data: books } = await getBooks();
      return (
        <div>
          <h1>Books</h1>
          <p>개발 서적 목록</p>
          <BookList books={books} />
    	 )
    }
  • 서버 액션 form 예시
    import React from "react";
    import { sayHello } from "./actions";
    
    const Children = () => {
      console.log("children hello");
    
      return (
        <div>
          Children
          <form action={sayHello}>
            <button> say hello</button>
          </form>
        </div>
      );
    };
    
    export default Children;

💡 Route Handler vs. Server Action

항목Route HandlerServer Action
목적REST API 구축React 컴포넌트에 밀접한 서버 로직
접근 방식URL로 호출함수 호출처럼 사용
사용하는 곳FE/BE/모바일/외부 서비스 모두 사용 가능React 컴포넌트에서만 사용
보이는 HTTP 메서드GET / POST / PUT / DELETE항상 POST로 감춰짐
보안URL이 외부에 노출될 수 있음URL이 없음 → 더 안전
데이터 처리용읽기/쓰기 모두 적합쓰기(Create/Update/Delete)에 최적
캐싱 제어fetch 옵션으로 ISR 가능fetch 내부에서만 제어 가능

0개의 댓글