Next.js App Router는 React Server Components(RSC)를 중심으로 설계되며, page.tsxlayout.tsx는 기본적으로 서버 컴포넌트예요[60][61]. use client는 클라이언트 번들 경계를 정하고[63], fetch 캐시 옵션·Suspense 스트리밍·중첩 레이아웃·Server Actions·<Image>·instrumentation.ts를 함께 쓰면 성능, 데이터 신선도, 관측 가능성을 체계적으로 관리할 수 있어요[62][64][65][66][67][68].

App Router는 React Server Components 중심 아키텍처예요.

Next.js 공식 문서는 App Router가 React Server Components를 중심으로 만들어졌다고 설명해요[60]. 이 구조의 핵심은 UI 일부를 서버에서 렌더링해 브라우저로 보내고, 클라이언트로 전송하는 JavaScript 양을 줄이는 데 있어요[60][69]. React 문서도 Server Components가 서버에서 데이터 패칭을 수행하고 클라이언트 렌더링용 번들 크기를 줄이는 데 기여한다고 말해요[69]. 즉, App Router는 단순한 파일 기반 라우터가 아니라 렌더링 위치와 번들 구성을 함께 바꾸는 아키텍처예요.

이 점이 Pages Router와 체감상 가장 크게 다른 지점이에요. 예전에는 페이지 단위 CSR 또는 SSR 선택이 주된 고민이었다면, App Router에서는 컴포넌트 단위로 서버와 클라이언트 책임을 더 세밀하게 나눌 수 있어요. 다만 이것이 모든 화면을 서버 우선으로 무조건 바꾸라는 뜻은 아니에요. 사용자 입력, 브라우저 API, 복잡한 상호작용이 많은 영역은 여전히 클라이언트 컴포넌트가 적합해요.

`page.tsx`와 `layout.tsx`는 기본적으로 서버 컴포넌트예요.

Next.js 문서에 따르면 App Router의 layout.tsx, page.tsx 같은 파일은 use client가 붙지 않는 한 서버 컴포넌트예요[61]. 이 기본값은 중요해요. 개발자가 아무 선언도 하지 않았을 때 클라이언트 번들로 보내는 것이 아니라, 서버에서 실행되는 컴포넌트로 취급되기 때문이에요. 그래서 데이터 조회, 비밀 키 접근, 무거운 가공 로직 같은 작업을 자연스럽게 서버 쪽에 둘 수 있어요.

// app/products/page.tsx
export default async function ProductsPage() {
  const res = await fetch('https://api.example.com/products', {
    next: { revalidate: 300 },
  })
  const products = await res.json()

  return (
    <main>
      <h1>상품 목록</h1>
      <pre>{JSON.stringify(products, null, 2)}</pre>
    </main>
  )
}

위 예시는 page.tsx가 기본 서버 컴포넌트라는 점을 보여줘요. 별도의 API 라우트를 만들지 않고 서버에서 직접 fetch를 호출할 수 있고, 그 결과를 JSX로 바로 렌더링해요. 초보자 설명에서 흔히 “클라이언트 컴포넌트는 브라우저에서 실행된다”고 단순화하지만, 공식 문서 기준으로 더 정확한 표현은 use client 경계에서 시작한 모듈이 클라이언트 번들에 포함된다는 것이에요[63]. 이 차이를 이해하면 번들 크기와 실행 위치를 더 정확히 추론할 수 있어요.

`use client`는 클라이언트 컴포넌트 경계를 정의해요.

Next.js는 use client 지시어가 클라이언트 컴포넌트의 경계를 정의한다고 설명해요[63]. 그리고 그 경계에서 import된 컴포넌트만 클라이언트 번들에 포함돼요[63]. 이 말은 버튼 1개 때문에 페이지 전체를 클라이언트 컴포넌트로 만들 필요가 없다는 뜻이에요. 가능한 한 작은 인터랙션 섬만 use client로 감싸면 번들 전송량을 줄이기 쉬워져요.

// app/dashboard/page.tsx
import SearchBox from './SearchBox'

export default async function DashboardPage() {
  const res = await fetch('https://api.example.com/stats', {
    next: { revalidate: 60 },
  })
  const stats = await res.json()

  return (
    <section>
      <h1>대시보드</h1>
      <SearchBox />
      <div>{stats.totalUsers} users</div>
    </section>
  )
}

// app/dashboard/SearchBox.tsx
'use client'

import { useState } from 'react'

export default function SearchBox() {
  const [q, setQ] = useState('')
  return <input value={q} onChange={(e) => setQ(e.target.value)} />
}

실무에서는 이 경계를 어디에 둘지가 품질을 크게 좌우해요. 예를 들어 검색 입력창, 탭 전환, 드래그 앤 드롭처럼 브라우저 상태가 필요한 부분만 use client로 분리하는 방식이 일반적으로 유리해요. 반대로 큰 상위 레이아웃 전체에 use client를 붙이면 하위 import들이 함께 클라이언트 번들로 따라갈 수 있어요[63]. 다만 “레이아웃 깊이를 2~3단계로 제한하라” 같은 수치는 공식 문서에서 직접 보장하는 규칙이 아니라 경험적 조언으로 보는 것이 안전해요.

`fetch` 캐시 옵션은 데이터 신선도와 비용을 제어하는 장치예요.

App Router의 데이터 패칭에서는 fetch 옵션으로 캐시 동작을 조절할 수 있어요[62]. 공식 문서가 명시한 대표 옵션은 cache: "no-store"next: { revalidate: number }예요[62]. 전자는 항상 최신 데이터를 가져오려는 성격이고, 후자는 예를 들어 revalidate: 60처럼 초 단위 재검증 주기를 두는 방식이에요[62]. 이 두 옵션만 제대로 구분해도 사용자에게 보여줄 데이터의 신선도와 서버 부하를 상당히 체계적으로 관리할 수 있어요.

상황추천 옵션의미
관리자 주문 현황cache: "no-store"요청마다 최신 상태를 우선해요[62]
상품 목록next: { revalidate: 300 }300초 주기로 갱신해요[62]
자주 안 바뀌는 소개 페이지 데이터next: { revalidate: 3600 }1시간 단위로 재검증해요[62]

실무에서 많이 생기는 문제는 캐시 정책을 코드 곳곳에 흩뿌려 놓는 경우예요. 공식 문서가 코드 리뷰 체크리스트까지 제시하지는 않지만, 팀 차원에서 “이 API는 no-store인지, 60초인지, 300초인지”를 명시적으로 관리하는 습관은 도움이 돼요. 다만 이것은 문서의 직접 주장이라기보다 운영 경험에서 나온 조언으로 구분하는 것이 맞아요. 특히 전체 마이그레이션 시 디버깅 비용이 늘 수 있다는 말도 타당한 경험칙이지만, 제공된 출처가 숫자나 보편 법칙으로 증명하는 내용은 아니에요.

Suspense 기반 스트리밍은 화면을 점진적으로 보내는 기능이에요.

Next.js는 Suspense와 함께 스트리밍을 기본 지원해요[64]. 문서 표현대로라면 전체 페이지 렌더링이 끝날 때까지 기다리지 않고, 라우트 세그먼트와 컴포넌트를 브라우저에 점진적으로 전송할 수 있어요[64]. 이 방식은 느린 데이터가 하나 있을 때 나머지 UI까지 함께 묶여 늦게 뜨는 문제를 완화하는 데 유용해요. 결과적으로 사용자는 헤더, 네비게이션, 빠른 본문 일부를 먼저 보고, 느린 영역은 뒤이어 받을 수 있어요.

// app/reports/page.tsx
import { Suspense } from 'react'
import SlowChart from './SlowChart'

export default function ReportsPage() {
  return (
    <main>
      <h1>리포트</h1>
      <Suspense fallback={<p>차트 불러오는 중...</p>}>
        <SlowChart />
      </Suspense>
    </main>
  )
}

스트리밍은 만능 가속기가 아니에요. 총 데이터 조회 시간이 5초라면 5초가 1초로 줄어드는 것은 아니지만, 5초를 모두 기다린 뒤 한 번에 보여주는 대신 먼저 보일 수 있는 부분을 1초 안팎에 전달하는 식의 체감 개선을 만들 수 있어요. 그래서 로딩 스켈레톤, 우선 노출 정보, 뒤늦게 와도 되는 정보의 순서를 설계하는 것이 중요해요. 즉, 성능 수치는 네트워크와 백엔드 상태에 따라 달라지지만, 전달 순서를 바꾸는 도구라는 점은 문서와 잘 맞아요[64].

중첩 레이아웃은 상태를 보존하고 불필요한 재렌더링을 줄여줘요.

App Router의 레이아웃 시스템은 중첩 레이아웃을 사용해요[65]. Next.js 문서는 공유 레이아웃을 사용하는 페이지 사이를 이동할 때 상태를 보존하고 불필요한 재렌더링을 피한다고 설명해요[65]. 예를 들어 /dashboard/settings/dashboard/billing이 같은 dashboard/layout.tsx를 공유하면, 공통 사이드바와 상단 바를 매번 새로 그리지 않아도 되는 구조를 만들 수 있어요. 관리자 콘솔, 문서 사이트, 커머스 마이페이지처럼 섹션 공통 UI가 많은 서비스에서 특히 유용해요.

  • 공통 네비게이션, 사이드바, 필터 패널을 상위 layout.tsx로 올려요[65].

  • 섹션별 헤더나 탭은 하위 레이아웃으로 분리해요[65].

  • 상태 보존이 필요 없는 일회성 UI는 페이지 내부에 두는 편이 단순할 수 있어요.

다만 레이아웃을 무한히 잘게 쪼개는 것이 정답은 아니에요. 공식 문서는 중첩 레이아웃의 장점을 설명하지만, 몇 단계가 최적인지 숫자로 규정하지는 않아요[65]. 따라서 “2~3단계가 좋다” 같은 표현은 팀 경험에 근거한 추천안으로만 제시하는 것이 안전해요. 설계 기준은 단계 수보다도 공유 UI의 수명주기, 상태 보존 필요성, 라우트 구조의 이해 가능성이어야 해요.

Server Actions는 별도 API 라우트 없이 서버 변이를 처리하게 해줘요.

Server Actions는 App Router에서 지원되며, 폼 변이와 서버 로직을 처리할 수 있어요[66]. Next.js 문서가 강조하는 장점은 모든 액션마다 별도의 API 라우트를 만들 필요가 없다는 점이에요[66]. 예를 들어 댓글 작성, 프로필 수정, 장바구니 추가 같은 작업을 폼과 가깝게 배치할 수 있어요. 이는 읽기용 fetch와 쓰기용 액션을 화면 단위 코드 안에서 더 자연스럽게 연결하게 해줘요.

// app/profile/actions.ts
'use server'

export async function updateProfile(formData: FormData) {
  const name = formData.get('name')
  // DB 저장 또는 서버 로직 수행
  return { ok: true, name }
}

// app/profile/page.tsx
import { updateProfile } from './actions'

export default function ProfilePage() {
  return (
    <form action={updateProfile}>
      <input name="name" />
      <button type="submit">저장</button>
    </form>
  )
}

물론 모든 쓰기 로직을 Server Actions로 통일해야 하는 것은 아니에요. 외부 시스템과의 복잡한 웹훅 흐름, 공개 API, 모바일 앱과의 공유 엔드포인트가 필요한 경우에는 전통적인 API 라우트나 별도 백엔드가 더 적절할 수 있어요. 그래도 브라우저 폼에서 서버 변이를 수행하는 단순 시나리오에서는 코드 이동량을 줄이고 구조를 단순하게 만드는 선택지가 돼요[66]. 즉, “API를 완전히 대체한다”기보다 “폼 중심 서버 변이에 강하다”라고 이해하는 편이 정확해요.

`<Image>` 컴포넌트는 적절한 크기의 이미지를 제공해 성능 개선에 도움을 줘요.

Next.js는 내장 <Image> 컴포넌트 사용을 권장해요[67]. 공식 문서가 직접 말하는 근거는 자동 이미지 최적화를 제공하고, 적절한 크기의 이미지를 서빙해 성능 개선에 도움이 될 수 있다는 점이에요[67]. 여기서 중요한 표현은 “성능 개선에 도움이 될 수 있다”예요. 특정 지표를 무조건 개선한다고 단정하기보다, 과도하게 큰 이미지를 줄이고 전송량을 최적화하는 수단으로 이해하는 것이 문서와 가장 잘 맞아요.

import Image from 'next/image'

export default function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="서비스 소개 이미지"
      width={1200}
      height={630}
      priority
    />
  )
}

실무에서 <Image>는 특히 마케팅 페이지, 블로그, 커머스 썸네일처럼 이미지 비중이 큰 화면에서 효과를 보기 쉬워요. 다만 “LCP를 줄여준다”처럼 지표 이름을 단정적으로 붙이기보다는, 적절한 크기 제공과 최적화가 결과적으로 LCP 같은 사용자 체감 성능 개선에 도움이 될 수 있다고 표현하는 편이 더 정확해요[67]. 또한 아이콘 스프라이트나 아주 단순한 장식 요소는 상황에 따라 일반 img가 더 단순할 때도 있어요. 핵심은 주력 콘텐츠 이미지에 대해 기본 최적화 수단을 놓치지 않는 것이에요.

`instrumentation.ts`는 서버 시작과 요청 수명주기에서 관측 코드를 실행하는 훅이에요.

Next.js 문서는 instrumentation.ts를 통해 서버 시작 시점과 요청 수명주기 동안 코드를 실행할 수 있다고 설명해요[68]. 문서가 제시하는 용도는 observability와 performance measurement예요[68]. 즉, 애플리케이션이 시작될 때 1회 초기화가 필요한 관측 코드나, 요청 흐름을 기록·측정하는 로직을 두는 자리에 가까워요. 이 파일의 가치는 기능 개발보다 운영 가시성을 높이는 데 있어요.

예를 들면 요청 단위 측정, 서버 부팅 시 계측 초기화, 성능 메트릭 수집, 오류 맥락 로깅 같은 작업을 떠올릴 수 있어요. 다만 OpenTelemetry나 특정 에러 추적 도구 이름이 제공된 출처의 추출 사실에 직접 들어 있지는 않으므로, 이런 예시는 어디까지나 가능한 활용 예로 제시하는 편이 안전해요[68]. 중요한 것은 instrumentation.ts가 관측과 성능 측정의 진입점이라는 방향성이에요. 운영 단계에서 병목을 찾고 변경 효과를 검증하려면 이 계층이 매우 중요해져요.

실무 적용은 서버 우선, 작은 클라이언트 경계, 명시적 캐시 정책 순서로 접근하면 돼요.

여기까지의 공식 근거를 바탕으로 하면, App Router 도입의 첫 원칙은 서버 컴포넌트를 기본값으로 받아들이는 것이에요[60][61]. 두 번째 원칙은 인터랙션이 필요한 최소 범위에만 use client를 적용하는 것이고[63], 세 번째는 모든 fetch에 데이터 신선도 의도를 드러내는 것이에요[62]. 여기에 스트리밍이 필요한 느린 영역을 Suspense로 분리하고[64], 공유 UI는 중첩 레이아웃으로 끌어올리면 돼요[65]. 쓰기 동작은 폼 중심이면 Server Actions를 우선 검토하고[66], 운영 단계에서는 instrumentation.ts로 측정 기반 개선 루프를 닫는 구조가 좋아요[68].

  1. 새 화면 1개를 app/ 아래에서 서버 컴포넌트 기본값으로 먼저 만들어요[61].

  2. 입력창, 모달, 토글처럼 브라우저 상태가 필요한 부분만 use client로 분리해요[63].

  3. fetch마다 no-store 또는 revalidate를 명시해요[62].

  4. 느린 위젯 1개를 Suspense로 감싸 점진 로딩을 시험해요[64].

  5. 공유 네비게이션이 있는 섹션 1곳에 중첩 레이아웃을 적용해요[65].

  6. 폼 제출 1개를 Server Actions로 바꿔 API 라우트 의존도를 비교해봐요[66].

  7. 핵심 이미지 3~5개를 <Image>로 전환하고 전송 크기와 체감 성능을 확인해요[67].

  8. 배포 전후 요청 시간과 오류 비율을 instrumentation.ts 기반 계측으로 측정해요[68].

FAQ

App Router를 쓰면 모든 컴포넌트를 서버 컴포넌트로 만들어야 하나요?

아니에요. 기본값은 서버 컴포넌트지만[61], 입력 처리·상태 관리·브라우저 API가 필요한 부분은 use client 경계 안에 두면 돼요[63].

`use client`를 상위 레이아웃에 붙이면 왜 조심해야 하나요?

공식 문서에 따르면 그 경계에서 import된 컴포넌트가 클라이언트 번들에 포함돼요[63]. 그래서 상위에 붙일수록 더 많은 코드가 클라이언트 쪽으로 이동할 수 있어요.

`cache: "no-store"`와 `revalidate`는 어떻게 고르나요?

실시간성이나 최신성이 매우 중요하면 no-store, 몇 초 또는 몇 분 단위 갱신으로 충분하면 next: { revalidate: number }를 고려하면 돼요[62].

Suspense 스트리밍은 페이지 속도를 무조건 높이나요?

무조건 총 처리 시간을 줄인다고 보긴 어려워요. 대신 전체가 끝날 때까지 기다리지 않고 일부 UI를 먼저 보여줄 수 있어 체감 응답성을 높이는 데 유리해요[64].

`instrumentation.ts`에는 어떤 도구를 넣어야 하나요?

출처는 특정 도구명을 직접 보증하지 않고, 서버 시작과 요청 수명주기에서 관측·성능 측정 코드를 실행하는 용도를 설명해요[68]. 따라서 팀의 로깅·메트릭·추적 체계에 맞춰 선택하면 돼요.