-
Next.js App Router Server Components와 Server Actions 알아보기 Part.2Next.js 2025. 4. 7. 22:29728x90반응형
이번 편에서는 이전 편에 이어 서버 컴포넌트 및 서버 액션을 중심으로, 성능 향상 효과, 그리고 개발자 경험(DX)의 변화를 깊이 있게 살펴보겠습니다.
Server Actions: 서버에서 직접 수행하는 액션 처리
App Router의 또 하나의 혁신은 Server Actions(서버 액션) 기능입니다.
서버 액션은 간단히 말해 서버에서 실행되는 함수를 클라이언트에서 호출할 수 있게 해주는 기능입니다.
Next.js 13.4에서 알파로 도입되어 Next.js 15에 이르러 더욱 안정화된 기능으로, 폼 제출이나 데이터 변경(mutation) 로직을 보다 직관적이고 타입 안전한 방법으로 구현할 수 있게 합니다.
Page Router 시절에는 사용자 입력을 처리하려면 보통 API Route를 만들고 (pages/api/*.js), 클라이언트에서 fetch나 axios로 그 API endpoint를 호출하는 식이었습니다.
혹은 getServerSideProps를 통해 폼 제출을 처리하면 리디렉션 등을 이용해야 했죠.
이 방식은 클라이언트와 서버가 명시적인 REST API나 AJAX 계약을 맺어야 하므로, 데이터 직렬화/역직렬화, 엔드포인트 관리, 보일러플레이트 코드 등이 필요했습니다.
Server Action을 활용하면 이러한 과정을 단순화할 수 있습니다.
서버 액션은 일종의 서버 전용 함수로 정의되어 클라이언트 컴포넌트에서 직접 호출되거나 <form>의 action으로 사용될 수 있습니다.
Next.js는 이 호출을 가로채어 해당 함수를 서버에서 실행하고, 필요한 경우 페이지를 갱신(refresh)해 줍니다.
1. 사용법
서버 액션은 주로 app 디렉토리 내에서 "use server" 지시어를 사용하여 정의합니다.
//app/products/actions.ts "use server"; export async function addToCart(productId, quantity) { // 서버에서 장바구니에 상품 추가 로직 수행 const result = await db.carts.update({ where: { userId: session.user.id }, data: { items: { create: { productId, quantity } } } }); return result; }
위 함수 addToCart는 Server Action으로, DB에 접근해 장바구니 데이터를 변경하는 순수 서버 측 로직입니다.
2. 클라이언트에서 호출
클라이언트 컴포넌트에서는 이 함수를 마치 일반 함수처럼 import하여 사용할 수 있습니다.
하지만 실제로 호출하면 브라우저에서 직접 실행되는 것이 아니라 Next.js가 서버로 호출을 위임합니다.
React는 이를 위해 내부적으로 폼 서브밋이나 RPC(Remote Procedure Call) 같은 매커니즘을 사용합니다.
개발자는 보통 React의 useTransition 훅과 함께 사용하여, 사용자 액션에 대한 응답으로 서버 액션을 트리거합니다.
// app/products/AddToCartButton.tsx - 클라이언트 컴포넌트 "use client"; import { useTransition } from 'react'; import { addToCart } from './actions'; function AddToCartButton({ productId }) { const [isPending, startTransition] = useTransition(); const handleAdd = () => { startTransition(() => { addToCart(productId, 1); }); }; return ( <button onClick={handleAdd} disabled={isPending}> {isPending ? "Adding..." : "Add to Cart"} </button> ); } export default AddToCartButton;
위 코드에서 startTransition(() => addToCart(...))를 호출하면, Next.js가 addToCart 함수를 백그라운드로 서버에서 실행하고, 그 동안 React는 isPending 상태를 통해 "Adding..."과 같은 로딩 UI를 표시합니다.
서버 액션이 완료되면 Next.js는 해당 컴포넌트가 포함된 페이지를 자동으로 리프레시(재검증)하여 최신 상태 (장바구니가 업데이트된 UI)를 반영합니다.
이때도 전체 페이지 리로드가 아니라 필요한 부분만 갱신됩니다.
3. 폼과의 연계
서버 액션은 <form>과도 자연스럽게 통합됩니다.
클라이언트 컴포넌트에서 <form action={serverActionFunc}> 속성을 지정하면, 해당 폼이 제출될 때 자바스크립트를 직접 쓰지 않고도 해당 서버 액션이 호출됩니다.
<form action={addToCart}> {/* ... input fields ... */} <button type="submit">Add to Cart</button> </form>
이렇게 하면 폼 데이터를 담은 요청이 자동으로 Next.js를 통해 addToCart 서버 함수로 전달되고, 처리 후 현재 페이지를 새로 고침하거나 특정 경로로 리디렉션할 수도 있습니다.
Progressive Enhancement 측면에서, 자바스크립트가 비활성화된 경우에도 서버 액션은 전통적인 폼 제출처럼 동작할 수 있기 때문에 유용합니다.
4. 타입 안전성과 DX
서버 액션을 사용하면 함수 파라미터와 반환값을 TypeScript로 정의하여 엔드투엔드 타입 안전한 데이터 처리가 가능합니다.
예를 들어 위 addToCart의 productId와 quantity에 타입을 지정하면, 클라이언트에서 잘못된 타입으로 호출할 경우 컴파일 타임에 에러를 잡을 수 있습니다.
이는 REST API 호출에서 JSON 구조를 주고받을 때 런타임까지 오류를 알기 어려운 점을 해소해줍니다.
또한 별도의 API 경로를 정의하고 HTTP 요청을 만들지 않아도 되므로 코드량이 줄고 한 곳에 응집됩니다.
팀 내에서 프론트엔드/백엔드 경계를 허무는 효과도 있어 개발 경험이 크게 향상됩니다.
5. 제약
서버 액션은 편리하지만 남용하면 안 됩니다.
호출마다 서버 측 연산이 일어나므로, 매우 빈번한 상호작용에는 신중해야 합니다.
또한 파일 업로드같이 스트림이 필요한 작업은 아직 지원되지 않을 수 있고 (Next.js 개선 중), 브라우저 네비게이션이나 URL 변경 없이 상태를 업데이트하기 때문에 전역 상태 관리와의 조합도 고려해야 합니다.
이에 대해서는 뒤에서 언급할 한계 사항에서 조금 더 다루겠습니다.
요약하면, Server Actions는 Next.js 앱의 서버 사이드 기능을 마치 클라이언트 함수 부르듯 사용할 수 있게 해주는 혁신적인 도구입니다.
복잡한 폼 처리나 데이터 변경 로직을 간결하게 작성할 수 있고, 이를 통해 e커머스나 SaaS 앱에서 폼 제출 -> 서버 처리 -> UI 갱신 흐름이 훨씬 매끄러워집니다.
성능 향상: 스트리밍과 번들 최적화의 효과
Next.js 15 App Router의 서버 중심 렌더링은 성능 측면에서 상당한 이점을 제공합니다.
이전 글의 CSR/SSR 비교 대목에서도 언급했지만, 이번 절에서는 이러한 향상이 구체적으로 어떤 지표와 사례로 나타나는지 정리해보겠습니다.
1. 초기 표시 및 응답 시간 개선
App Router의 스트리밍 기능은 SSR의 약점이었던 TTFB 지연을 극복합니다.
SSR에서는 서버가 모든 HTML을 완성할 때까지 첫 바이트를 기다려야 했지만, App Router는 React 18의 Suspense 및 스트리밍 SSR을 활용하여 데이터가 준비되는 대로 부분적으로 전송합니다.
이전 글의 Sentry 성능 비교 이미지에서 볼 수 있듯이, Page Router SSR에서는 First Contentful Paint (FCP)가 4초 이상 걸렸지만 App Router RSC 환경에서는 0.7초대로 크게 단축되었습니다.
또한 Time to First Byte (TTFB)도 150ms 수준으로 매우 빠르게 나타났습니다.
이것은 초기 HTML과 static 리소스들을 병렬로 전달할 수 있게 된 덕분이며, 결과적으로 사용자는 더 빠르게 콘텐츠를 보게 되고 인터랙션 가능 시점도 앞당겨지는 효과가 있습니다.
2. JS 번들 크기 감소 -> 로드/실행 최적화
대부분의 UI를 서버 컴포넌트로 전환함에 따라 클라이언트로 보내는 JS 양이 크게 줄어듭니다.
특히 대형 라이브러리나 복잡한 로직이 포함된 부분도 서버에서 처리되고 결과만 보내지므로, 클라이언트 입장에서는 필요한 최소한의 스크립트만 받습니다.
예를 들어 GeekyAnts사 사례에서 Next.js 13 RSC 도입 후 "불필요한 JS 실행이 줄어들어 사이트 인터랙션 속도가 개선"되었다고 보고했는데, 이는 RSC가 모든 DOM 요소마다 React 클라이언트 컴포넌트를 만들지 않기 때문이라고 설명합니다.
즉, 서버에서 완성한 부분은 굳이 클라이언트가 다시 렌더링 로직을 가질 필요가 없으니 메인스레드 작업량(Main-thread Work)이 감소하고 자바스크립트 실행 시간도 짧아집니다. 이러한 최적화는 Lighthouse 등의 성능 지표에서 90점대를 달성하는 데 크게 기여했습니다.
3. 네트워크 효율과 캐싱
Next.js 15의 fetch는 요청 중복 제거와 내장 캐싱을 제공합니다.
동일한 URL로 여러 서버 컴포넌트에서 데이터를 요청하면 한 번만 fetch하고 그 결과를 공유하거나 캐시를 활용합니다.
또 next: { revalidate: x } 옵션을 사용하면 ISR처럼 특정 시간 이후 자동으로 데이터 갱신이 가능합니다.
덕분에 불필요한 데이터 요청을 피하고, 정적 데이터는 CDN 수준에서 캐시하여 네트워크 응답 시간을 단축할 수 있습니다.
부분적으로 SSG/ISR 기법이 혼합되어 있는 셈입니다.
특히 e커머스처럼 데이터 일부는 자주 바뀌지만 일부는 거의 고정된 경우 (예: 제품 리뷰는 수시로 추가되지만 제품 설명은 거의 고정), revalidate 설정을 통해 성능과 신선도 균형을 잡을 수 있습니다.
4. 지각적 퍼포먼스(Perceived performance)
App Router의 로딩 상태 UI (loading.tsx)와 Suspense를 통한 점진적 로딩은 사용자 체감 성능을 높여줍니다.
사용자는 요청하자마자 화면에 뭔가 나타나는 것을 선호하며, 설령 그것이 실제 데이터가 아니고 placeholder일지라도 심리적으로 기다림이 줄어듭니다.
App Router에서는 경로 아래 loading.tsx를 정의하면 자동으로 전환 시 로딩 UI가 표시되고, 컴포넌트별로 Suspense fallback을 넣으면 그 부분만 별도로 로딩 인디케이터를 표시할 수 있습니다.
이전에도 이런 UI는 수동으로 구현할 수 있었지만, 이제 프레임워크 차원에서 지원하므로 개발자 부담 없이 일관된 로딩 UX를 제공할 수 있습니다.
이는 곧 사용자가 앱을 "빠르게" 느끼도록 만드는 중요한 요소입니다.
5. SEO 및 기타
SSR과 마찬가지로 App Router도 완성된 HTML 콘텐츠를 제공하므로 SEO에 유리합니다.
CSR의 SEO 문제를 해결했던 Next.js의 강점은 그대로 유지되고, 페이지별 메타데이터 설정도 강화되었습니다.
또한 스트리밍으로 인해 크롤러가 콘텐츠 일부를 늦게 받게 될 가능성은 있지만, 대부분의 크롤러는 초기 HTML 응답 내에 중요한 콘텐츠가 있으면 문제없으므로 SEO상 큰 이슈는 없습니다.
오히려 성능 개선으로 페이지 평가 점수가 올라 SEO에 간접적으로 긍정적 영향을 줄 것입니다.
전체적으로 Next.js 15 App Router는 성능 면에서 "빠른 응답, 적은 자바스크립트, 효과적인 캐싱"이라는 세 마리 토끼를 잡았다고 볼 수 있습니다.
이러한 개선은 특히 대규모 애플리케이션이나 데이터가 빈번히 갱신되는 서비스에서 빛을 발합니다.
다만, 성능을 극대화하려면 여전히 개발자가 컴포넌트를 어떻게 분리하고 caching 전략을 어떻게 설정하느냐에 달려 있으므로, 다음의 개발자 경험 섹션에서 이러한 패턴을 살펴보겠습니다.
개발자 경험(DX)의 변화: 더 나은 생산성과 새로운 패턴
Next.js 15 App Router는 단순히 성능 향상뿐만 아니라 개발자 경험에도 많은 변화를 가져옵니다.
"개발자에게 익숙한 React 코드 작성 방식으로 더 나은 성능을 끌어낸다"는 점에서, 처음에는 약간의 학습이 필요할 수 있지만 일단 적응하면 생산성이 향상되는 부분들을 짚어보겠습니다.
1. 데이터 패칭의 일원화
Page Router에서는 SSR용 데이터 패칭 (getServerSideProps)과 CSR 후 데이터 패칭(useEffect 등)이 분리되어 있었습니다. 하지만 App Router에서는 컴포넌트 함수 자체가 async가 될 수 있고 그 안에서 await fetch()를 호출하면 됩니다.
이는 곧 데이터 가져오는 코드와 UI 렌더링 코드가 한 곳에 공존한다는 뜻입니다.
예를 들어 이전에는 페이지 컴포넌트 파일 상단에 getServerSideProps로 데이터를 Fetch하고 props로 넘긴 뒤, 하단에서 React 컴포넌트로 렌더링했지만, 이제는 그 모든 게 한 함수 안에서 순차적으로 일어납니다.
코드 구조가 단순해지고, props 정의나 Serialize/Deserialize 같은 보일러플레이트가 줄어듭니다.
또한 fetch 결과를 변수에 담아놓고 바로 JSX에 사용할 수 있으므로 타입 추론이나 리팩토링도 수월합니다.
2. React API 활용 극대화
App Router의 기능들은 React의 최신 기능들과 맞닿아 있습니다.
예를 들어 Suspense 경계를 이용한 분할 로딩, useTransition을 이용한 비동기 액션 처리, React Context를 통한 클라이언트 상태 관리 등 React 18+ 기능들을 자연스럽게 활용하게 됩니다.
기존 CSR 환경에서는 Suspense for Data Fetching이 어려웠지만 RSC 도입으로 가능해졌고, useTransition은 서버 액션과 찰떡궁합을 이룹니다.
이처럼 React 핵심 기능들이 Next.js와 긴밀히 통합되어, 별도 상태머신이나 이팩트 관리 없이 React만으로도 꽤 복잡한 데이터 흐름을 제어할 수 있게 된 것이 DX 향상의 큰 부분입니다.
3. 타입스크립트 및 구조 개선
Next.js App Router는 TypeScript 사용자를 위한 개선도 눈에 띕니다.
예를 들어, 서버 액션 함수를 정의하면 자동으로 그 타입 정보를 토대로 클라이언트에서 사용할 수 있습니다.
또한 폴더 구조 상에서 컴포넌트, 레이아웃, 라우팅이 명확히 구분되므로 파일 관리가 용이해졌습니다.
app 디렉토리 하위에 page.tsx, layout.tsx, loading.tsx, error.tsx, route.ts (API 라우트 용도) 등을 구분함으로써, 관습적으로 어떤 역할의 코드인지 쉽게 파악됩니다.
Pages Router 시절 _app.js, _document.js, pages/api 등이 있던 것과 비교하면 더 체계적입니다.
이러한 구조화된 접근 덕분에 팀원 간 코드 합의가 쉬워지고, 새로운 기능 (예: 새로운 페이지 추가)도 정해진 대로만 하면 되어 생산성이 높아집니다.
4. 폼 처리와 입력 검증 간소화
Page Router에서는 폼 제출을 처리하려면 form onSubmit -> API 호출 -> 응답 처리 -> 상태 업데이트 순으로 매번 코드를 작성해야 했습니다.
그러나 App Router의 서버 액션과 RHF(react-hook-form) 등을 조합하면 이 흐름이 단순해집니다.
다음 편에서 살펴볼 예제처럼, 클라이언트에서는 RHF로 폼 상태와 기본 검증을 관리하고, 제출 시 서버 액션을 호출하여 처리를 끝마친 뒤 곧바로 React가 UI를 갱신합니다.
중간에 수동으로 HTTP 요청 코드를 쓰거나 Redux 등으로 상태를 조정하는 양이 줄어듭니다.
zod 같은 스키마 검증 라이브러리도 서버 액션과 궁합이 좋습니다.
zod 스키마를 정의해두면 클라이언트에서는 RHF의 resolver로 사용하고, 서버에서는 같은 스키마로 한번 더 validate하여 안전성을 확보하는 식입니다.
이처럼 동일한 검증 로직을 양쪽에서 재사용함으로써 버그를 줄이고 유지보수를 쉽게 합니다.
5. 백엔드 연동 용이성
Next.js 서버 컴포넌트 환경은 Node.js 환경이므로 axios, typeorm, Prisma 등 백엔드 라이브러리들을 직접 사용할 수 있습니다.
Pages Router의 getServerSideProps 등에서도 가능했지만, App Router에서는 아예 컴포넌트나 액션 코드에서 자유롭게 DB나 외부 API를 호출하는 것이 자연스러운 형태가 되었습니다.
예를 들어, axios로 제3의 REST API를 호출하여 데이터를 가져온 뒤 그 결과로 서버 컴포넌트를 렌더링할 수 있고, 서버 액션 내부에서도 axios.post를 호출해 다른 서비스에 데이터 전송을 수행할 수 있습니다.
덕분에 BFF(Backend-for-Frontend) 역할을 Next.js가 훨씬 수월하게 해냅니다.
개발자 입장에서도 데이터 fetching이나 업데이트를 어디서 할지 고민이 줄어드는데, "가능하면 서버 컴포넌트/액션에서 하고, 정말 필요할 때만 클라이언트에서 한다."는 명확한 가이드가 생겼기 때문입니다.
6. 기존 라이브러리와의 호환
App Router 도입 초기에 React 생태계 라이브러리들과 호환 문제가 일부 있었지만(예: 일부 UI 라이브러리는 클라이언트 컴포넌트로만 동작해야 함), 현재 대부분 가이드라인이 마련되었습니다.
use client 지시어를 이용해 특정 서드파티 컴포넌트를 감싸거나, Context/API를 쓰는 라이브러리는 클라이언트 컴포넌트 상에서 사용하고 하위는 서버 컴포넌트로 작성하는 등의 패턴이 정립되고 있습니다.
예를 들어 NextAuth 같은 인증 라이브러리도 App Router에 맞춰 hooks를 제공하므로, 클라이언트 컴포넌트에서 세션 정보를 받아 서버 컴포넌트에 prop으로 넘기는 형태로 구현합니다.
전반적으로 Next.js 팀과 커뮤니티가 이런 통합을 빠르게 개선하고 있어, Next.js 15 시점에서는 App Router로 거의 대부분의 기능을 안정적으로 구현할 수 있습니다.
결과적으로 Next.js 15의 개발자 경험은 "더 적은 코드로 더 빠른 웹을 만들 수 있다"는 것으로 요약할 수 있습니다.
초기에는 서버/클라이언트 컴포넌트 분리, 새로운 폴더 구조 등 학습곡선이 있지만, 이해하고 나면 과거에 비해 로직이 단순화되고 중복이 줄어들어 개발 생산성이 높아집니다.
다음편에서는 이러한 App Router의 개념들을 실제 e커머스/SaaS 예제에 적용해보며, 구체적인 예시를 살펴보겠습니다.
반응형'Next.js' 카테고리의 다른 글