Next.js Single Page Applications 가이드 정리

1. SPA 개요

Next.js는 SPA(Single-Page Application) 구축을 완전히 지원합니다.

  • 프리페칭 기반의 빠른 라우트 전환
  • 클라이언트 사이드 데이터 패칭
  • 브라우저 API 사용
  • 서드파티 클라이언트 라이브러리(SWR/React Query 등) 통합
  • 정적 라우트 생성, 필요 시 서버 기능(RSC/Server Actions) 점진 도입

가이드는 “strict SPA”를 다음처럼 정의합니다.

  • CSR 기반: index.html 같은 단일 HTML로 시작
  • 풀 리로드 없이 브라우저 JS가 라우팅/DOM 업데이트/데이터 패칭을 담당

2. 왜 Next.js로 SPA를 만들까?

Next.js는 SPA 경험을 유지하면서도, strict SPA의 단점을 줄여줍니다.

  • 자동 코드 스플리팅: 불필요한 JS를 덜 내려받아 번들 크기 감소
  • 라우트별 HTML 엔트리 생성 가능(초기 로딩/SEO/UX 유리)
  • next/link의 자동 프리페칭으로 SPA 같은 빠른 전환
  • 프로젝트 성장 시 RSC/Server Actions 등을 “필요한 만큼만” 점진적으로 추가 가능

3. 패턴 1: React use() + Context로 “서버에서 먼저 패칭” 시작하기

핵심 아이디어:

  • 서버(예: root layout)에서 데이터를 빨리 요청만 시작하고(Promise 생성)
  • 클라이언트 컴포넌트에서 use()로 Promise를 unwrap
  • 이렇게 하면 Next.js가 서버에서 일찍 스트리밍을 시작할 수 있고, 클라이언트 워터폴을 줄일 수 있음

3.1 Root Layout에서 Promise 만들기(await 하지 않음)

// app/layout.tsx
import { UserProvider } from './user-provider'
import { getUser } from './user' // server-side function

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const userPromise = getUser() // do NOT await

  return (
    <html lang="en">
      <body>
        <UserProvider userPromise={userPromise}>{children}</UserProvider>
      </body>
    </html>
  )
}

3.2 Context Provider로 Promise 전달

// app/user-provider.tsx
'use client'

import { createContext, useContext, ReactNode } from 'react'

type User = any
type UserContextType = { userPromise: Promise<User | null> }

const UserContext = createContext<UserContextType | null>(null)

export function useUser(): UserContextType {
  const ctx = useContext(UserContext)
  if (!ctx) throw new Error('useUser must be used within a UserProvider')
  return ctx
}

export function UserProvider({
  children,
  userPromise,
}: {
  children: ReactNode
  userPromise: Promise<User | null>
}) {
  return (
    <UserProvider userPromise={userPromise}>{children}</UserProvider>
  )
}

3.3 Client Component에서 use()로 Promise unwrap

// app/profile.tsx
'use client'

import { use } from 'react'
import { useUser } from './user-provider'

export function Profile() {
  const { userPromise } = useUser()
  const user = use(userPromise)

  return <div>...</div>
}

4. 패턴 2: SWR로 SPA 데이터 패칭 유지 + 서버 데이터 결합

SWR 2.3.0(React 19+)에서는 use() 패턴을 추상화해, 기존 클라이언트 패칭 코드를 유지하면서 서버 측 데이터 제공(RSC)과 결합할 수 있습니다.

  • Client-only: useSWR(key, fetcher)
  • Server-only: useSWR(key) + RSC 제공 데이터
  • Mixed: useSWR(key, fetcher) + RSC 제공 데이터

가이드는 이를 위해 <SWRConfig>fallback을 활용하는 접근을 안내합니다.


5. 브라우저에서만 렌더링해야 하는 컴포넌트

window, document 같은 브라우저 API에 의존하는 라이브러리는 서버 렌더링에서 문제가 될 수 있습니다.

가이드는 다음 같은 접근을 언급합니다.

  • 브라우저 API 존재 여부를 체크하는 useEffect를 두고, 없으면 null 또는 로딩 상태를 반환(이 반환값은 프리렌더링 가능)

6. Shallow routing (클라이언트에서 URL만 갱신)

Create React App/Vite 같은 strict SPA에서 넘어오면, URL 상태만 바꾸는 shallow routing 코드가 있을 수 있습니다.

Next.js는 window.history.pushState/replaceState를 통해 풀 리로드 없이 URL을 갱신하면서도, Router와 동기화(usePathname, useSearchParams)할 수 있습니다.

'use client'

import { useSearchParams } from 'next/navigation'

export default function SortProducts() {
  const searchParams = useSearchParams()

  function updateSorting(sortOrder: string) {
    const urlSearchParams = new URLSearchParams(searchParams.toString())
    urlSearchParams.set('sort', sortOrder)
    window.history.pushState(null, '', `?${urlSearchParams.toString()}`)
  }

  return (
    <>
      <button onClick={() => updateSorting('asc')}>Sort Ascending</button>
      <button onClick={() => updateSorting('desc')}>Sort Descending</button>
    </>
  )
}

7. Client Components에서 Server Actions 점진 도입

API Route를 따로 만들지 않고, 클라이언트에서 “함수 호출”처럼 서버 로직을 호출할 수 있습니다.

// app/actions.ts
'use server'

export async function create() {}
// app/button.tsx
'use client'

import { create } from './actions'

export function Button() {
  return <button onClick={() => create()}>Create</button>
}

8. Static export (선택)

Next.js는 완전 정적 사이트 생성도 지원합니다.

strict SPA 대비 장점(가이드에서 강조):

  • 라우트별 HTML 생성 → 초기 표시 빠름
  • 각 라우트가 “완성된 HTML”로 시작 → UX 개선
  • 클라이언트 이동은 여전히 SPA처럼 빠름

설정:

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  output: 'export',
}

export default nextConfig

next buildout/ 폴더가 생성됩니다.

주의: static export에서는 Next.js 서버 기능이 지원되지 않습니다.


9. 마이그레이션 가이드

기존 strict SPA는 큰 변경 없이 Next.js로 옮긴 뒤, 필요한 만큼 서버 기능을 점진적으로 도입할 수 있습니다.

  • Create React App → Next.js 마이그레이션 가이드
  • Vite → Next.js 마이그레이션 가이드
  • Pages Router 기반 SPA라면 App Router를 점진 도입하는 가이드도 참고 가능

10. 정리

  • Next.js는 “SPA 같은 UX”를 유지하면서도, 코드 스플리팅/라우트별 HTML/서버 기능 점진 도입으로 성능·확장성을 확보할 수 있습니다.
  • 데이터 패칭은 root layout에서 “먼저 시작”하고, client에서 use()로 소비하는 패턴이 핵심입니다.
  • SWR/React Query 등 기존 클라이언트 라이브러리도 함께 사용할 수 있습니다.
  • static export는 “서버 기능이 필요 없는” 순수 정적 배포에만 적합합니다.