문서
App Router 사용하기
컴포넌트
<Form>

<Form>

<Form> 컴포넌트는 HTML <form> 요소를 확장하여 프리페칭로딩 UI, 제출 시 클라이언트 사이드 네비게이션, 그리고 점진적 개선을 제공합니다.

URL 검색 파라미터를 업데이트하는 폼에 유용하며, 위의 기능을 구현하는 데 필요한 보일러플레이트 코드를 줄여줍니다.

기본 사용법:

/app/page.tsx
import Form from "next/form";
 
export default function Page() {
  return (
    <Form action="/search">
      {/* 제출 시, 입력값이 URL에 추가됩니다.
          예: /search?query=abc */}
      <input name="query" />
      <button type="submit">Submit</button>
    </Form>
  );
}
/app/ui/search.js
import Form from "next/form";
 
export default function Search() {
  return (
    <Form action="/search">
      {/* 제출 시, 입력값이 URL에 추가됩니다.
          예: /search?query=abc */}
      <input name="query" />
      <button type="submit">Submit</button>
    </Form>
  );
}

참조

<Form> 컴포넌트의 동작은 action prop에 string이나 function이 전달되는지에 따라 달라집니다.

  • action문자열인 경우, <Form>GET 메서드를 사용하는 네이티브 HTML 폼처럼 동작합니다. 폼 데이터는 URL의 검색 파라미터로 인코딩되며, 폼이 제출되면 지정된 URL로 이동합니다. 추가로 Next.js는:
    • 폼이 보이게 되면 경로를 프리페치합니다. 이는 공유 UI(예: layout.jsloading.js)를 미리 로드하여 더 빠른 네비게이션을 가능하게 합니다.
    • 폼이 제출될 때 전체 페이지 리로드 대신 클라이언트 사이드 네비게이션을 수행합니다. 이는 공유 UI와 클라이언트 사이드 상태를 유지합니다.
  • action함수(서버 액션)인 경우, <Form>React 폼처럼 동작하며, 폼이 제출될 때 액션을 실행합니다.

action (문자열) Props

action이 문자열일 때, <Form> 컴포넌트는 다음 props를 지원합니다:

Prop예시타입필수 여부
actionaction="/search"string (URL 또는 상대 경로)
replacereplace={false}boolean-
scrollscroll={true}boolean-
  • action: 폼이 제출될 때 이동할 URL 또는 경로입니다.
    • 빈 문자열 ""은 업데이트된 검색 파라미터와 함께 동일한 라우트로 이동합니다.
  • scroll: 네비게이션 중 스크롤 동작을 제어합니다. 기본값은 true로, 새 라우트의 상단으로 스크롤하며, 뒤로 가기와 앞으로 가기 네비게이션에서는 스크롤 위치를 유지합니다.
  • replace: 현재 히스토리 상태를 브라우저의 히스토리 스택에 새로 추가하는 대신 대체합니다. 기본값은 false입니다.

action (함수) Props

action이 함수일 때, <Form> 컴포넌트는 다음 prop을 지원합니다:

Prop예시타입필수 여부
actionaction={myAction}function (서버 액션)
  • action: 폼이 제출될 때 호출될 서버 액션입니다. 자세한 내용은 React 문서를 참조하세요.

알아두면 좋은 점: action이 함수일 때는 replacescroll props가 무시됩니다.

주의사항

  • onSubmit: 폼 제출 로직을 처리하는 데 사용할 수 있습니다. 하지만 event.preventDefault()를 호출하면 지정된 URL로 이동하는 등의 <Form> 동작을 재정의합니다.
  • formAction: <button> 또는 <input type="submit"> 필드에서 action prop을 재정의하는 데 사용할 수 있습니다. Next.js는 클라이언트 사이드 네비게이션을 수행하지만, 이 접근 방식은 프리페칭을 지원하지 않습니다.
    • basePath를 사용할 때는 formAction 경로에도 포함해야 합니다. 예: formAction="/base-path/search".
  • method, encType, target: <Form> 동작을 재정의하므로 지원되지 않습니다.
    • 마찬가지로 formMethod, formEncType, formTarget을 사용하여 method, encType, target props를 각각 재정의할 수 있지만, 이를 사용하면 네이티브 브라우저 동작으로 폴백됩니다.
    • 이러한 props를 사용해야 한다면 대신 HTML <form> 요소를 사용하세요.
  • key: 문자열 actionkey prop을 전달하는 것은 지원되지 않습니다. 리렌더링을 트리거하거나 변경을 수행하려면 대신 함수 action을 사용하는 것을 고려하세요.
  • <input type="file">: action이 문자열일 때 이 입력 타입을 사용하면 파일 객체 대신 파일 이름을 제출하는 브라우저 동작과 일치합니다.

예시

검색 결과 페이지로 이어지는 검색 폼

경로를 action으로 전달하여 검색 결과 페이지로 이동하는 검색 폼을 만들 수 있습니다:

/app/page.tsx
import Form from "next/form";
 
export default function Page() {
  return (
    <Form action="/search">
      <input name="query" />
      <button type="submit">Submit</button>
    </Form>
  );
}
/app/page.js
import Form from "next/form";
 
export default function Page() {
  return (
    <Form action="/search">
      <input name="query" />
      <button type="submit">Submit</button>
    </Form>
  );
}

사용자가 쿼리 입력 필드를 업데이트하고 폼을 제출하면 폼 데이터가 URL의 검색 파라미터로 인코딩됩니다. 예: /search?query=abc.

알아두면 좋은 점: action에 빈 문자열 ""을 전달하면 폼은 업데이트된 검색 파라미터와 함께 동일한 라우트로 이동합니다.

결과 페이지에서는 searchParams page.js prop을 사용하여 쿼리에 접근하고 외부 소스에서 데이터를 가져오는 데 사용할 수 있습니다.

/app/search/page.tsx
import { getSearchResults } from "@/lib/search";
 
export default async function SearchPage({
  searchParams,
}: {
  searchParams: { [key: string]: string | string[] | undefined };
}) {
  const results = await getSearchResults(searchParams.query);
 
  return <div>...</div>;
}
/app/search/page.js
import { getSearchResults } from "@/lib/search";
 
export default async function SearchPage({ searchParams }) {
  const results = await getSearchResults(searchParams.query);
 
  return <div>...</div>;
}

<Form>이 사용자의 뷰포트에 보이게 되면 /search 페이지의 공유 UI(layout.jsloading.js 같은)가 프리페치됩니다. 제출 시, 폼은 즉시 새 라우트로 이동하고 결과를 가져오는 동안 로딩 UI를 표시합니다. loading.js를 사용하여 폴백 UI를 디자인할 수 있습니다:

/app/search/loading.tsx
export default function Loading() {
  return <div>Loading...</div>;
}
/app/search/loading.js
export default function Loading() {
  return <div>Loading...</div>;
}

공유 UI가 아직 로드되지 않은 경우를 대비하여 useFormStatus를 사용하여 사용자에게 즉각적인 피드백을 보여줄 수 있습니다.

먼저 폼이 대기 중일 때 로딩 상태를 표시하는 컴포넌트를 만듭니다:

/app/ui/search-button.tsx
"use client";
import { useFormStatus } from "react-dom";
 
export default function SearchButton() {
  const status = useFormStatus();
  return (
    <button type="submit">{status.pending ? "Searching..." : "Search"}</button>
  );
}
/app/ui/search-button.js
"use client";
import { useFormStatus } from "react-dom";
 
export default function SearchButton() {
  const status = useFormStatus();
  return (
    <button type="submit">{status.pending ? "Searching..." : "Search"}</button>
  );
}

그런 다음 검색 폼 페이지를 업데이트하여 SearchButton 컴포넌트를 사용합니다:

/app/page.tsx
import Form from "next/form";
import { SearchButton } from "@/ui/search-button";
 
export default function Page() {
  return (
    <Form action="/search">
      <input name="query" />
      <SearchButton />
    </Form>
  );
}
/app/ui/search-button.js
import Form from "next/form";
import { SearchButton } from "@/ui/search-button";
 
export default function Page() {
  return (
    <Form action="/search">
      <input name="query" />
      <SearchButton />
    </Form>
  );
}

서버 액션을 사용한 데이터 변경

action prop에 함수를 전달하여 데이터 변경을 수행할 수 있습니다.

/app/posts/create/page.tsx
import Form from "next/form";
import { createPost } from "@/posts/actions";
 
export default function Page() {
  return (
    <Form action={createPost}>
      <input name="title" />
      {/* ... */}
      <button type="submit">Create Post</button>
    </Form>
  );
}
/app/posts/create/page.js
import Form from "next/form";
import { createPost } from "@/posts/actions";
 
export default function Page() {
  return (
    <Form action={createPost}>
      <input name="title" />
      {/* ... */}
      <button type="submit">Create Post</button>
    </Form>
  );
}

데이터 변경 후에는 새로운 리소스로 리다이렉트하는 것이 일반적입니다. next/navigationredirect 함수를 사용하여 새 게시물 페이지로 이동할 수 있습니다.

알아두면 좋은 점: 폼 제출의 "목적지"는 액션이 실행될 때까지 알 수 없기 때문에, <Form>은 공유 UI를 자동으로 프리페치할 수 없습니다.

/app/posts/actions.ts
"use server";
import { redirect } from "next/navigation";
 
export async function createPost(formData: FormData) {
  // 새 게시물 생성
  // ...
 
  // 새 게시물로 리다이렉트
  redirect(`/posts/${data.id}`);
}
/app/posts/actions.js
"use server";
import { redirect } from "next/navigation";
 
export async function createPost(formData) {
  // 새 게시물 생성
  // ...
 
  // 새 게시물로 리다이렉트
  redirect(`/posts/${data.id}`);
}

그런 다음, 새 페이지에서 params prop을 사용하여 데이터를 가져올 수 있습니다:

/app/posts/[id]/page.tsx
import { getPost } from "@/posts/data";
 
export default async function PostPage({ params }: { params: { id: string } }) {
  const data = await getPost(params.id);
 
  return (
    <div>
      <h1>{data.title}</h1>
      {/* ... */}
    </div>
  );
}
/app/posts/[id]/page.js
import { getPost } from "@/posts/data";
 
export default async function PostPage({ params }) {
  const data = await getPost(params.id);
 
  return (
    <div>
      <h1>{data.title}</h1>
      {/* ... */}
    </div>
  );
}

더 많은 예시는 서버 액션 문서를 참조하세요.