-
Next.js App Router Server Components와 Server Actions 알아보기 Part.3(Feat. react-hook-form, zod, react-query)Next.js 2025. 4. 12. 19:07728x90반응형
이번 글에서는 App Router의 개념들을 실제 e커머스/SaaS 예제에 적용해 보며, 구체적인 코드를 살펴보겠습니다.
e커머스 애플리케이션과 SaaS 대시보드를 예로 들어, Next.js 15 App Router에서 Server Components, Server Actions를 활용하는 방법과, react-hook-form, zod, react-query 같은 라이브러리를 어떻게 통합하는지 보여드리겠습니다.
각 예제는 이해를 돕기 위한 간략한 코드이며, 실제 구현에서는 추가적인 에러 처리나 보안 고려가 필요할 수 있습니다.
예제 1: e커머스 제품 페이지와 장바구니
e커머스 앱에서 "제품 목록 및 상세 페이지"와 "장바구니" 기능을 App Router로 구현한다고 가정해 봅시다.
제품 목록 페이지
여러 제품을 보여주는 페이지로, 제품들의 기본 정보와 재고 수량 등을 표시합니다.
App Router에서는 이 페이지를 서버 컴포넌트로 만들어 데이터 패칭을 직접 수행합니다.
// app/products/page.tsx // 제품 목록 페이지 컴포넌트 (Server Component) const ProductsPage = async () => { // 모든 제품 목록 fetch (정적 캐시, 60초마다 ISR 재검증) const products = await fetch('https://api.example.com/products', { next: { revalidate: 60 } }).then(res => res.json()); // 추천 상품 목록 fetch (빌드시 고정, SSG로 캐시) const featured = await fetch('https://api.example.com/featured-products', { cache: 'force-cache' }).then(res => res.json()); // 실시간 재고 정보 fetch (항상 최신, 매 요청 SSR) const inventory = await fetch('https://api.example.com/inventory', { cache: 'no-store' }).then(res => res.json()); return ( <div> <FeaturedProducts items={featured} /> <ProductGrid products={products} inventory={inventory} /> </div> ); }; export default ProductsPage;
위 코드에서는 세 종류의 데이터를 가져오는데, 각각 캐싱 전략을 다르게 지정했습니다.
- 전체 제품 목록은 revalidate: 60으로 설정하여 ISR 형태로 동작합니다. 최대 60초간 캐시 되므로 빈번한 요청에도 부담이 적고, 60초 지나면 다음 요청 때 최신 데이터를 가져와 갱신합니다.
- 추천 상품 목록은 거의 안 바뀐다고 가정하고 cache: 'force-cache'로 SSG처럼 캐시 합니다. 배포 시 한 번 가져온 뒤로는 변경이 없는 한 계속 정적 제공됩니다.
- 재고 정보는 실시간성을 요하므로 cache: 'no-store'로 SSR마다 최신으로 가져옵니다.
이렇게 컴포넌트 단위로 캐싱을 조절하면, 성능과 신선도의 균형을 상황에 맞게 취할 수 있습니다.
기존 Pages Router에서는 한 페이지 내에 이런 서로 다른 갱신 주기를 적용하기 어려웠지만 App Router에서는 매우 쉽게 가능함을 알 수 있습니다.
제품 상세 페이지
개별 제품의 상세 정보를 표시하는 페이지로, 리뷰, 관련 상품, 추천 아이템도 함께 보여준다고 해봅시다.
이 경우 App Router의 중첩 레이아웃과 Suspense를 활용하면 효율적입니다.
// app/product/[id]/page.tsx (동적 경로) import Reviews from './Reviews'; // 리뷰 리스트 (서버 컴포넌트) import RelatedProducts from './RelatedProducts'; // 관련 상품 (서버 컴포넌트) import Recommendations from './Recommendations'; // 추천 항목 (서버 컴포넌트) const ProductPage = async ({ params }) => { const { id } = params; const product = await getProductDetails(id); // 제품 상세 정보 (DB 호출 등) return ( <div> <ProductDetails data={product} /> {/* 리뷰, 관련상품, 추천목록은 병렬 로딩 */} <Suspense fallback={<ReviewsSkeleton />}> <Reviews productId={id} /> </Suspense> <Suspense fallback={<RelatedProductsSkeleton />}> <RelatedProducts productId={id} /> </Suspense> <Suspense fallback={<RecommendationsSkeleton />}> <Recommendations productId={id} /> </Suspense> </div> ); }; export default ProductPage;
위처럼 구현하면 페이지가 로드될 때 제품 상세 정보는 우선 바로 표시되고, 리뷰/관련상품/추천 컴포넌트들은 각자 서버에서 데이터를 가져오면서 동시에 Suspense 대기 상태에 들어갑니다.
브라우저는 ProductDetails 내용과 skeleton UI들을 먼저 렌더링 해주고, 나머지 데이터가 오는 대로 순차적으로 해당 부분을 교체해 줍니다.
이렇게 하면 "사용자가 상세 정보는 즉시 보고, 리뷰 등은 약간 지연되어도 UX상 문제없게" 만들 수 있습니다.
개발자는 Suspense와 skeleton 컴포넌트들만 준비해 두면 되니 구현도 간단합니다.
장바구니 추가 기능
e커머스에서 핵심인 "장바구니에 상품 추가"는 Server Action으로 구현하기에 딱 좋은 사례입니다.
앞서 소개했던 addToCart 액션을 재사용해보겠습니다.
이 액션은 DB에 사용자 장바구니 레코드를 업데이트하는 서버 함수였죠.
클라이언트에서 이 액션을 호출하는 AddToCartButton 컴포넌트는 이미 살펴보았듯이 useTransition으로 비동기 호출과 로딩 상태 관리를 합니다.
이로 인해 사용자가 버튼을 클릭하면 즉시 "Adding..." 상태를 보고, 백엔드 작업이 끝나면 UI가 업데이트됩니다.
서버 액션을 활용하면 제품 상세 페이지에서 장바구니에 추가 후 곧바로 상단 장바구니 아이콘의 item count를 업데이트하거나, "장바구니에 담겼습니다" 알림을 표시하는 등의 피드백도 쉽게 구현 가능합니다.
Next.js에서는 서버 액션 호출 후 해당 페이지 컴포넌트 전체를 다시 fetch 하여 새 상태로 그림으로써 (React의 re-render와 비슷한 개념) 상태 변화를 반영해 줍니다.
필요하다면 useRouter(). refresh()를 호출해 수동으로 새로고침 트리거도 가능합니다.
중요한 점은, 개발자가 일일이 응답을 받아 전역 상태를 바꾸고 컨텍스트를 업데이트하는 등의 번거로운 작업이 줄어든다는 것입니다.
서버 액션 호출 -> React 재렌더 사이클로 대부분 해결되므로, 코드 유지보수성이 올라갑니다.
예제 2: SaaS 대시보드의 폼 처리와 데이터 패칭
이번에는 SaaS 웹앱의 대시보드를 예로 들어보겠습니다.
이 대시보드에는 새 프로젝트 생성 폼, 프로젝트 목록 표시, 통계 차트 등이 있다고 가정합니다.
여기서는 react-hook-form과 zod를 이용한 폼 검증, 그리고 react-query를 활용한 클라이언트 데이터 패칭을 사용해 보겠습니다.
1. 새로운 프로젝트 생성 폼 (Server Action + react-hook-form + zod)
관리자나 사용자가 새로운 프로젝트를 생성하는 폼을 생각해 봅시다.
프로젝트 이름과 설명을 입력받고 "Create" 버튼을 누르면 프로젝트가 생성되는 시나리오입니다.
이 폼은 즉각적인 필드 검증(빈 값 등)을 위해 클라이언트에서 react-hook-form으로 관리하고, 제출 시 서버 액션을 호출하여 DB에 생성하는 방식으로 구현할 수 있습니다.
먼저 zod를 활용해 프로젝트 데이터의 스키마를 정의합니다.
// app/dashboard/project-schema.ts import { z } from 'zod'; export const ProjectSchema = z.object({ name: z.string().min(1, "Name is required"), description: z.string().max(500).optional() }); export type ProjectInput = z.infer<typeof ProjectSchema>;
다음으로 서버 액션을 정의합니다.
프로젝트를 생성하는 백엔드 로직을 캡슐화한 함수입니다.
// app/dashboard/actions.ts "use server"; import { ProjectSchema, ProjectInput } from './project-schema'; import { db } from '@/lib/db'; // 가상의 데이터베이스 모듈 import { revalidatePath } from 'next/cache'; export async function createProject(input: ProjectInput) { // 서버에서 입력 데이터 검증 const parsed = ProjectSchema.safeParse(input); if (!parsed.success) { // 검증 실패 시 오류 던지기 (예: name이 비었을 경우 등) throw new Error("ValidationError"); } const data = parsed.data; // DB에 새로운 프로젝트 생성 (예시, 실제론 try/catch로 에러처리 필요) await db.project.create({ data }); // 선택적으로, 프로젝트 목록을 다시 패칭하도록 현재 경로를 재검증 revalidatePath('/dashboard'); }
위 createProject 서버 액션은 ProjectInput 타입의 객체를 받아 zod로 검증한 후 데이터베이스에 저장하고 있습니다.
revalidatePath('/dashboard')는 Next.js의 캐시를 무효화하여 /dashboard 경로의 데이터를 최신화해 주는 함수입니다.
이를 호출함으로써 프로젝트 생성 후 대시보드의 프로젝트 목록 컴포넌트가 자동으로 갱신되게 할 수 있습니다.
이제 클라이언트 컴포넌트로 폼 UI를 작성해 보겠습니다. RHF를 이용해 폼 상태를 관리하고, zodResolver로 앞서 정의한 ProjectSchema를 적용합니다.
// app/dashboard/NewProjectForm.tsx "use client"; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { ProjectSchema, ProjectInput } from './project-schema'; import { createProject } from './actions'; import { useTransition } from 'react'; export default function NewProjectForm() { const { register, handleSubmit, formState: { errors } } = useForm<ProjectInput>({ resolver: zodResolver(ProjectSchema) }); const [isPending, startTransition] = useTransition(); const onSubmit = (data: ProjectInput) => { startTransition(() => { createProject(data); }); // 또는 createProject(data) 반환값을 받아 처리하거나 에러 캐치 가능 }; return ( <form onSubmit={handleSubmit(onSubmit)}> <div> <label>Project Name</label> <input {...register('name')} /> {errors.name && <p className="error">{errors.name.message}</p>} </div> <div> <label>Description (optional)</label> <textarea {...register('description')} /> </div> <button type="submit" disabled={isPending}> {isPending ? "Creating..." : "Create Project"} </button> </form> ); }
이 NewProjectForm 컴포넌트는 클라이언트에서 동작합니다.
사용자가 폼을 입력하면 RHF가 관리하고 있는 errors를 통해 즉각적으로 validation 피드백을 줄 수 있습니다.
(예: 이름이 비었으면 "Name is required" 에러 메시지 출력).
handleSubmit에 우리가 정의한 onSubmit 함수를 연결했는데, 이 함수 내부에서 startTransition으로 createProject 서버 액션을 호출합니다.
startTransition을 쓴 이유는 서버 액션 호출이 React의 상태변경(리프레시)을 유발하더라도 사용자 인터랙션을 블로킹하지 않고 병렬로 처리하기 위함입니다.
그리고 isPending을 통해 현재 서버 액션이 진행 중인지 여부를 알 수 있어, 버튼을 비활성화하거나 "Creating..."이라고 텍스트를 바꿔주는 로딩 UI 처리를 했습니다.
만약 서버에서 validation 에러가 발생하거나 다른 오류가 발생하면 Next.js는 기본적으로 해당 액션 호출이 있었던 컴포넌트를 리렌더 하지 않도록 하고 콘솔에 에러를 출력합니다.
이러한 에러 처리를 개선하려면 try-catch로 잡아서 상태로 관리하거나, <form> 제출 방식 대신 직접 createProject 호출의 결과를 받아 처리하는 등 추가 작업이 필요하지만, 개념 증명 차원에서는 단순하게 해 두었습니다.
이 예제에서 주목할 점
- 클라이언트와 서버가 동일한 ProjectSchema로 검증 로직을 공유하고 있습니다. 덕분에 클라이언트에서는 사용자의 잘못된 입력을 바로잡도록 돕고, 서버에서는 혹시 모를 우회 입력에 대비할 수 있습니다. (Server Action은 JS 없이 폼을 제출하는 경우도 처리하므로 서버 측 검증은 중요합니다.)
- react-hook-form을 사용하여 폼 상태 관리와 입력값 추출이 매우 간단합니다. 과거라면 이 폼 제출 시 API 호출 코드를 작성하고, 성공하면 Context나 상태를 바꿔 목록을 갱신하고, 에러나 validation 메시지도 일일이 수동 처리해야 했을 텐데, App Router에서는 서버 액션 호출 -> 페이지 갱신 흐름이 자동화되어 그런 부수적인 코드가 필요 없습니다.
- 이 패턴은 다양한 폼에 적용할 수 있습니다. 예컨대 프로필 편집 폼, 게시글 작성 폼 등에서도 동일하게 Server Action + RHF + zod 조합으로 구현하면 일관된 UX와 안정성을 확보할 수 있습니다.
2. react-query를 활용한 클라이언트 데이터 패칭
서버 컴포넌트와 서버 액션이 많은 데이터 작업을 커버하지만, 여전히 클라이언트에서 데이터 패칭이 유용하거나 필요한 경우가 있습니다.
대표적으로, 실시간으로 변하는 데이터를 폴링 하거나, 사용자 트리거에 따라 추가 데이터를 가져올 때, 또는 페이지 전환 없이 특정 컴포넌트만 갱신하고 싶을 때입니다.
이럴 때 react-query 같은 클라이언트 상태/패칭 라이브러리가 큰 도움이 됩니다.
예를 들어, 대시보드에 "최근 알림 목록"을 실시간으로 보여주는 컴포넌트를 생각해 보죠.
초기 알림은 서버 컴포넌트로 렌더링 했다 하더라도, 새로운 알림이 도착하면 클라이언트에서 주기적으로 API를 조회해 갱신하는 편이 유리할 수 있습니다. (서버에서 SSE나 WebSocket 푸시를 구현하지 않은 상황 가정)
다음은 react-query를 사용해 폴링 기반 데이터 갱신을 구현하는 간단한 예입니다.
// app/dashboard/Notifications.tsx "use client"; import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; interface Notification { id: string; message: string; createdAt: string; } export default function Notifications() { const { data, error, isLoading } = useQuery( ['notifications'], async () => { const res = await axios.get<Notification[]>('/api/notifications'); return res.data; }, { refetchInterval: 30000, // 30초마다 자동 재조회 (폴링) } ); if (isLoading) return <div>불러오는 중...</div>; if (error) return <div>알림을 불러오는데 오류가 발생했습니다.</div>; return ( <ul> {data!.map(notif => ( <li key={notif.id}> {notif.message} <span>({new Date(notif.createdAt).toLocaleTimeString()})</span> </li> ))} </ul> ); }
- 이 컴포넌트는 클라이언트 컴포넌트로 동작하며, Next.js의 내장 fetch가 아니라 axios로 /api/notifications 엔드포인트를 호출하고 있습니다. 여기서 /api/notifications는 Next.js의 Route Handler(/app/api/notifications/route.ts)나 Pages API(/pages/api/notifications.js)로 구현된 API라고 가정합니다. (서버 액션은 주기적 폴링에 적합하지 않으므로 API로 처리)
- useQuery 훅을 사용하여 notifications 키로 데이터를 관리합니다. refetchInterval 옵션을 주어 30초마다 자동 갱신되도록 했습니다. 따라서 사용자가 대시보드를 열고 있으면 30초마다 새로운 알림을 체크해 목록이 업데이트됩니다.
- isLoading, error 상태를 이용해 로딩 중이거나 실패 시의 UI도 간단히 처리했습니다.
이처럼 react-query는 클라이언트에서 데이터 캐싱과 상태 동기화를 매우 편리하게 해 줍니다.
초기 페이지 로드 시에는 서버 컴포넌트가 알림 데이터를 한번 렌더링 해서 보여줄 수 있겠지만, 지속적인 업데이트나 사용자 액션에 따른 부분 갱신은 react-query의 역할입니다.
특히 Server Actions가 항상 폼이나 이벤트와 1:1로 대응되는 반면, react-query는 전역적인 데이터 상태를 관리하기 좋습니다.
예컨대 위 알림 목록은 해당 컴포넌트가 언마운트될 때까지 캐시를 유지하고, 다른 페이지 갔다 돌아와도 기존 데이터를 즉시 보여주는 등의 UX 개선을 자동으로 지원합니다.
react-query를 이 아키텍처에서 사용하는 이유
- 서버 컴포넌트는 초기 로드에 최적화되었지만, 사용자 상호작용 이후의 데이터 변경에 대해서는 클라이언트 측 대응이 필요할 때가 있습니다. 예를 들어 필터 버튼 클릭에 따라 목록을 다시 불러온다거나, 특정 조건에서 자동 새로고침 등은 페이지 전체 SSR보다는 부분 업데이트가 효율적입니다.
- Next.js의 App Router에서는 페이지를 넘나들 때 캐싱된 서버 결과를 재사용하기 때문에, 클라이언트에서 관리해야 하는 전역 데이터(예: 모달에서 수정한 결과를 리스트에 반영하는 등)는 여전히 존재합니다. react-query는 이러한 부분을 캐시 키 기반으로 쉽게 업데이트하거나 refetch 할 수 있게 도와줍니다.
- Mutations와 optimistic update: react-query의 useMutation 훅을 사용하면 낙관적 업데이트(optimistic update)도 쉽게 구현 가능합니다. 예를 들어, 프로젝트 생성 폼을 서버 액션 대신 react-query의 mutation으로 구현했다면, 성공 시 useQuery(['projects']) 캐시를 갱신하여 곧바로 신규 프로젝트를 리스트에 반영하게 할 수 있습니다.(물론 Next의 서버 액션 + revalidatePath로도 비슷하게 가능하지만, UI에서 더 세밀한 제어가 필요하다면 이런 방법도 있습니다.)
결국 Server Actions와 react-query는 상호 배타적이지 않고, 보완적인 도구입니다.
서버 액션은 폼 제출이나 단발성 서버 작업에 적합하고, react-query는 클라이언트 주도 데이터 관리에 강점이 있습니다.
Next.js 15를 활용한 애플리케이션에서 개발자는 이 둘을 조합하여, 필요한 곳에는 서버 액션으로 간결하게 처리하고, 또 필요한 곳에서는 react-query로 유연하게 상태를 관리함으로써 최적의 결과를 얻을 수 있습니다.
고려해야 할 제약 사항과 트레이드오프
Next.js 15 App Router가 많은 이점을 제공하지만, 모든 것이 완벽한 것은 아닙니다.
새로운 아키텍처로 전환하면서 개발팀이 인지해야 할 한계나 트레이드오프도 몇 가지 존재합니다.
많이 거론되는 몇 가지를 간략히 짚어보겠습니다.
- 러닝 커브 및 복잡성: App Router는 RSC, Server Actions, 새로운 파일 구조 등 개념적으로 상당한 변화가 있었습니다. 기존 Next.js 개발자라도 처음에는 혼란을 느낄 수 있고, 특히 서버/클라이언트 컴포넌트 분리에 따른 코드 구조 설계에 고민이 필요합니다. 잘못하면 어떤 로직이 어디서 실행되는지 헷갈리거나, 서버 컴포넌트에서만 가능한 일을 클라이언트 컴포넌트에 넣어버려 오류를 만날 수도 있습니다. 따라서 팀 차원에서 모범 패턴을 정하고 코드 리뷰로 일관성을 유지하는 노력이 필요합니다.
- 서버 부하 증가 가능성: "서버 중심"이라는 말 그대로, 많은 로직이 서버에서 실행되다 보니 서버에 부담이 늘어날 수 있습니다. 예컨대 과거에 클라이언트가 하던 연산도 이제 서버가 대신하면, 서버 자원 소모가 커집니다. 물론 캐싱과 스트리밍으로 상쇄하고, 서버는 클라이언트보다 성능이 좋으니 이득이지만, 트래픽 패턴과 인프라 비용을 잘 따져봐야 합니다. 특히 대규모 서비스에서 모든 요청을 SSR로 처리하면 서버 비용이 증가할 수 있으므로, revalidate 등을 활용해 캐시 히트율을 높이고, 적절히 정적 생성할 부분은 해야 합니다.
- 실시간 상호작용 한계 및 해결: App Router의 기본 철학은 요청-응답 기반 SSR이지만, 현대 웹앱에서 요구되는 완전히 실시간 양방향(예: WebSocket을 통한 실시간 업데이트) 시나리오는 여전히 개발자가 수동으로 처리해야 합니다. Next.js 13.4부터 Route Handler에서 WebSocket을 지원하거나, 혹은 서버액션을 주기적으로 호출하는 식으로 가능하나, RSC 자체는 실시간 스트림 입력 개념이 아닙니다. 따라서 주기적 폴링이나 위에서 다룬 react-query, 또는 Next.js와 별도의 real-time channel (예: Pusher, Socket.io)을 결합하는 등 솔루션을 짜야할 것입니다. 이 부분은 기존에도 존재했던 과제이지만, App Router가 나오면서 특별히 더 쉬워지진 않았습니다.
- 상태 관리와 컨텍스트: 서버 컴포넌트는 본질적으로 무상태 (stateless)입니다. 요청마다 새로 렌더링 되며, 클라이언트와는 다른 환경이기 때문에, 클라이언트 전역 상태(Context API 등)를 서버 컴포넌트에서 직접 쓸 수 없습니다. 이로 인해 로그인 세션 정보나 테마 설정 등을 서버와 클라이언트에서 모두 필요로 하면, 클라이언트에서 Context로 제공하고 그 값을 서버 패치에 사용하는 식으로 관리해야 합니다. 예를 들어 NextAuth의 useSession() 훅은 클라이언트 컴포넌트에서 세션을 받아와 서버 컴포넌트로 prop을 전달하는 패턴을 권장합니다. 이런 부분은 기존보다 조금 복잡할 수 있습니다. 다행히 Next.js 13.5+에서는 cookies()나 headers() 함수를 서버 컴포넌트에서 호출해 쿠키나 헤더 (예: Authorization)를 읽어올 수 있어 인증 정보 활용이 가능해졌습니다. 요점은, 서버↔클라이언트 간 상태 공유는 명시적으로 이뤄져야 한다는 것입니다.
- 일부 기능의 성숙도: App Router 출시 초기에는 이미지 최적화(next/image)나 국제화(i18n) 지원 등이 Pages Router 대비 미흡하다는 지적이 있었습니다. 2025년 현재 Next.js 15에서는 대부분 개선되었지만, 여전히 Page Router에서 쓰던 일부 플러그인이나 패턴이 그대로는 안 되는 경우가 있습니다. 예를 들어, next-i18next 같은 라이브러리를 App Router에서 사용하는 방법이 달라지거나, middleware를 통한 국토 리다이렉션을 직접 구현해야 하는 등입니다. 이런 부분은 공식 문서와 커뮤니티 업데이터를 참고하여 최신 대응 방법을 따라야 합니다.
- 디버깅 난이도: RSC와 스트리밍 환경에서는 렌더링 과정이 복잡해져서 디버깅이 어려울 수 있습니다. React Developer Tools로 볼 때 서버 컴포넌트는 보이지 않고, 클라이언트 컴포넌트 경계만 나타난다든지, 네트워크 패널에 보이는 건 RSC 페이로드(JSON 비슷한 덩어리)라든지 하는 식입니다. Next.js는 이를 돕기 위해 React 프로파일러 지원이나, 서버 액션 로그, next dev 모드에서 경고 등을 제공하지만, 여전히 개발자가 개념을 정확히 알고 추적해야 하는 부분이 있습니다. 점차 도구들이 개선되고 있으므로, 향후에는 나아질 것입니다.
요약하자면, App Router의 이점이 크지만 그에 따른 새로운 고려사항들도 있다는 것입니다.
대부분은 시간과 함께 개선되거나, 패턴이 자리 잡고 있는 추세입니다.
Next.js 팀도 이러한 피드백을 바탕으로 Next.js 15에서 많은 부분을 다듬었고, 앞으로도 더 발전시킬 것으로 보입니다.
결론
Next.js 15의 App Router 도입으로 웹 애플리케이션 개발은 또 한 단계 진화했습니다.
React Server Components와 Server Actions를 중심으로 한 새로운 아키텍처는, 전통적인 CSR/SSR 방식의 한계를 극복하고 성능과 개발자 경험 양쪽에서 큰 개선을 이루어냈습니다.
요약하면, App Router의 서버 컴포넌트 모델은 초기 로드 속도를 높이고 클라이언트 부하를 줄여주었으며, 스트리밍 SSR을 통해 사용자에게 더 빠른 응답을 제공합니다.
실제 사례에서 FCP, TTFB 등 웹 바이탈 지표가 크게 향상되었고, JavaScript 번들 크기가 줄어든 덕에 저사양 기기에서도 더 부드러운 경험을 줄 수 있게 되었습니다.
e커머스, SaaS 등 SEO와 동적 콘텐츠 모두 중요한 분야에서 특히 유용한 변화입니다.
한편 개발자 경험 측면에서는 서버/클라이언트 경계가 명확해지면서도 코드 구조가 일관성 있게 바뀌었고, 데이터 패칭이나 폼 처리 등의 반복작업이 단순화되었습니다.
react-hook-form + zod + Server Actions 조합으로 안전하고 깔끔한 폼 처리가 가능하고, react-query와 같은 도구와도 잘 어울려 실시간 UX를 강화할 수 있습니다.
타입스크립트의 혜택을 더 살릴 수 있게 된 것도 생산성 향상의 큰 요소입니다.
물론 새로운 패러다임이기 때문에 기존 코드베이스를 마이그레이션 하거나 익숙해지는 데 시간은 필요합니다.
그러나 Next.js 15는 "미래의 웹 개발" 방향을 제시하고 있으며, 서버 중심 렌더링이 주는 이점들은 그 학습 비용을 상쇄하고도 남을 것입니다.
지금까지 알아본 개념들과 예제들을 토대로, 여러분의 프로젝트에도 App Router의 기능들을 적극 활용해 보시기 바랍니다.
초기 로드가 느린 페이지, 복잡한 데이터 패칭 로직, 중복된 API 코드 등에 변화가 필요하다면, Next.js 15가 큰 도움이 될 것입니다.
끝으로, Next.js는 활발하게 진화하고 있으므로 최신 공식 문서와 베스트 프랙티스를 꾸준히 확인하는 것이 좋습니다.
Server Components와 Server Actions도 계속 개선되고 있어, 앞으로 더 효율적인 패턴이 나오리라 기대됩니다.
Next.js 15 App Router를 도전해 보시기를 바랍니다!
- 참고자료
- Boosting Performance with Next.js and React Server Components: A geekyants.com Case Study
- Next.js 공식 문서: App Router 소개 및 가이드
- Smashing Magazine – The Forensics Of React Server Components (2024)
- Pagepro Tech Blog – Next.js App Router vs Page Router 비교 (2025)
반응형'Next.js' 카테고리의 다른 글
Next.js App Router Server Components와 Server Actions 알아보기 Part.2 (0) 2025.04.07 Next.js App Router Server Components와 Server Actions 알아보기 Part.1 (0) 2025.04.07 Next.js에서 On-Demand ISR 활용하기(Feat. API, Webhook, Serverless) (2) 2025.03.02 왜 useBack 커스텀 hook을 만들었는가? (0) 2025.02.15 Next.js App Router의 인터셉트 라우트(Intercept Route)와 병렬 라우트(Parallel Route) 알아보기 (2) 2024.12.15