오늘날의 현대적인 메타 프레임워크인
Nuxt4와Next를 비교하여 살펴보고,
프론트앤드 기술에 대한 Deep Dive를 해보도록 해요!
들어가기 앞서,
“어떤 도구를 사용해서 서비스를 구현해야 할까?”
개발자라면, 제품을 구현하기에 앞서서, 이런 이야기나 혹은 생각을 해보셨으리라 생각합니다.
팀원을 구성하여 팀내의 도구(프레임워크)를 결정하거나, 제품구현을 수월하게 도와줄 라이브러리 또는 패키지, 유틸리티가 어떤 프레임워크에서 더 많이 지원하는지에 따라 결정되기도 합니다.
개발을 하다보니, 이러한 도구 선택은 지극히 팀내의 성향, 그리고 기술을 결정하는 결정권자에 의존하게 된다고 학습해왔습니다.
그렇다면 우리는, 반드시 프론트앤드 기술 중에 가장 많이 사용하고 있는 라이브러이인 Next만을 사용하여 기술을 개발해야할 것일까요?
저는 이러한 물음에서부터 이 글을 작성하려고 합니다!
(사실 제 글의 대부분이 편견을 타파하자! 와 같은 스멜 이 나는 것들이지만요!)
이러한 의미에서, 이번 글에서는 웹서비스를 쉽게 구현해주는 메타프레임워크인 Nuxt4 와 Next 에서 제공하고 있는 핵심기능 및 유용한 API들을 살펴보면서 기능이 얼마나 유사한지 확인해보고, 마지막에는 미래의 프론트앤드 기술은 어떻게 나아갈 것인지 이야기 해볼 수 있는 시간을 가지려고 합니다.
(앞은 한치앞도 모르는 것이지만요!)
랜더링 모드
우선, 두 메타프레임워크의 가장 기본적인 랜더링 방식에 대해서 설명을 해보도록 할게요!
두개의 메타프레임워크의 기본적인 랜더링 기본적인 동작방식은 유사하지만, 차이점은 명확하게 존재하기 때문에 공통점과 차이점을 열거 해보도록 할게요!
기본적인 랜더링 모드
Nuxt4는 기본 랜더링 방식을 유니버셜 랜더링(Universal rendering) 이라는 SSR 기반에서 유연하게 CSR을 사용하는 랜더링 방식을 지원하며, Next.js에서는 SSR 모드가 기본적으로 제공되게 됩니다.
두 프레임워크 모두 기본설정이라는 가정하에, 아래와 같이 두 프레임워크는 SSR 로 동작하게 됩니다.

공통적으로, 클라이언트 측에서만 랜더링되는 컴포넌트를 설정하는 방법이 모두 존재하는데요,
아래와 같이 설정하면 Client Side Component를 사용할 수 있습니다!
🟢 Nuxt4
// 1. 컴포넌트 이름 뒷단에 `client`를 추가하여 Client Component를 정의
// ex) CustomButton.client.vue
// 1-1. 컴포넌트 명은 그대로 사용하고, template 영역에 <Client-Only></Client-Only>를 추가하여 Client Component를 정의
// ex) CustomButton.vue
<script setup lang="ts">
const count = ref(0)
const countUp = () => {
count++
}
</script>
<template>
<ClientOnly>
<button @click="countUp()">
{{ Count: `${count}` }}
</button>
</ClientOnly>
</template>
// 필요시, 서버측에서만 랜더링하는 컴포넌트를 컴포넌트 명으로 정의도 가능!
// ex) CustomInput.server.vue
🔵 Next.js
// Next.js에서는 모든 .tsx 상단에 'use client'를 작성하여 정의함
'use client'
import { useState } from 'react'
export default const Count() => {
const [count, countUp] = useState(0)
}
..
//
아래와 같이, Nuxt4에서는 config 설정을 통해 각 라우트별 랜더링 방식을 쉽게 조절할 수 있습니다.
Nuxt 에서는 이를 Hybrid Rendering 이라고 부르고 있어요.
<script setup lang="ts">
export default defineNuxtConfig({
routeRules: {
// 루트 라우트는 빌드 시 미리 생성
'/': { prerender: true },
// 새로운 URL 로 리디렉션 처리
'/pre-page': { redirect: '/next-page' },
// 관리페이지 하위 라우트 들은 모두 CSR 랜더링
'/admin/**': { ssr: false },
// 필요할 때 생성: 백그라운드에서 재검증하며, API 응답이 변경될 때까지 캐시
'/products': { swr: true },
// 하위 페이지들은 필요할 때 생성: 백그라운드에서 재검증하며, 1시간(3600초) 동안 캐시
'/products/**': { swr: 3600 },
// 하위 블로그 페이지는 필요할 때 한 번 생성 후 다음 배포까지 유지: CDN에 캐시
'/blog/**': { isr: true },
// 블로그 목록 페이지는 필요할 때 한번 생성 후 다음 배포까지 유지: CDN에서 1시간(3600초) 동안 캐시 후 백그라운드에서 재검증 진행
'/blog': { isr: 3600 }
}
})
</script>
이름과 기능이 동일한 API 들
앞서 살펴본것과 같이, 기본적인 랜더링 구조에서부터 Nuxt와 Next는 유사하게 나아가고 있습니다.
하지만, 이뿐만 아니라 동일한 기능, 나아가 이름까지 동일한(또는 거의유사한) Api들에 대해서 알아보도록 하겠습니다!
다음부터 소개할 API들은 순수하게 프레임워크에서 제공하는 기능이지만, 이 또한 살펴보면서 두 프레임워크를 더욱 자세히 비교해보도록 할게요!
1. Suspense
두 프레임워크에서 모두 Suspense는 비동기 작업을의 대기과정을 처리할 목적으로 사용합니다.
이를 통해서 컴포넌트가 데이터를 조회할 경우에 로딩상태와 같은 Fallback UI 를 표시할 수 있게 도와줍니다.
즉, 데이터가 준비될 때 까지 Streaming 랜더링을 통해서 먼저 보여줄 부분을 빠르게 랜더링하여 보여주고, 나머지는 데이터가 준비가 완료되는 대로 보여주는 형태라고 생각하면 됩니다.
이러한 공통적인 기능을 보유한 Suspense는 다음과 같이 사용할 수 있습니다!
🟢 Nuxt4
<script setup lang='ts'>
const { data, pending } = await useAsyncData('lists', () =>
$fetch('/api/lists')
)
</script>
<template>
<Suspense>
<template #default>
<ul>
<li v-for="list in data" :key="list.id">
{{ list.title }}
</li>
</ul>
</template>
<template #fallback>
<p>데이터를 조회중입니다!</p>
</template>
</Suspense>
</template>
🔵 Next.js
import { Suspense } from 'react'
import Lists from './Lists'
export default function Page() {
return (
<div>
<Suspense fallback={<p>데이터를 조회중입니다!</p>}>
<Lists />
</Suspense>
</div>
)
}
2. useRouter()
당연하게도, 두 프레임워크 모두 페이지기반의 Route 기능을 제공하고 있기 때문에 useRouter() 는 route history 조작을 하는 동일한 기능을 가지게 된 것 같습니다.
랜더링모드와 마찬가지로, 해당 API 또한 Nuxt4에서만 별도의 추가기능을 제공합니다.
🟢 Nuxt4
const router = useRouter()
const route = useRoute()
// Next와 유사한 기능
router.push({ path: '/main' }) // 페이지 이동 | NavigateTo() 를 사용권장
router.back() // Route History Stack 에서 뒤페이지 가기
router.forward() // Route History Stack 에서 앞페이지 가기
router.replace('/user') // Route History 교체 | NavigateTo() 를 사용권장
route.fullpath // 경로, 쿼리 및 해시를 포함하는 현재 경로와 연결된 인코딩된 URL
route.path // URL의 인코딩된 경로명 섹션
route.hash // #으로 시작하는 URL의 디코딩된 해시 섹션
route.query // 쿼리 파라미터
route.match // 현재 경로 위치와 일치하는 정규화된 경로 배열
route.meta // 레코드에 첨부된 metadata
route.name // 경로 레코드의 고유 이름
route.redirectedFrom // 현재 경로 위치에 도달하기 전에 접근을 시도했던 경로 위치 (유용함)
// Nuxt에만 존재하는 기본 기능
router.go(-1) // router.back() 과 동일
router.go(3) // Route History Stack 에서 3번째 앞페이지 가기
router.addRoute({ name: 'home', path: '/home', component: Home }) // 새로운 Route History 추가
router.removeRoute('home') // Route History 삭제
router.getRoutes() // 모든 Route History 목록 조회
router.hasRoute('home') // 특정 Route History 에서 경로명을 조회하여 가지고 있는지 확인
router.resolve({ name: 'home' }) // 정규화된 route 위치를 전달
// 네비게이션 가드도 지원하지만, Route Middleware를 이용하는 것이 좋습니다!
🔵 Next.js
Next에서는 ‘use client’ 즉, 클라이언트 컴포넌트로 정의해야만 사용이 가능합니다!
import { usePathname, useSearchParams } from 'next/navigation'
const router = useRouter()
const path = usePathname()
const searchParams = useSearchParams()
// Nuxt와 유사한 기능
router.push('/main', { scroll: true }) // 페이지 이동
router.back() // Route History Stack 에서 뒤페이지 가기
router.forward() // Route History Stack 에서 앞페이지 가기
router.replace() // Route History 교체
path //현재 URL의 경로
searchParams // 쿼리 파라미터
// Next에만 존재하는 기본 기능
router.prefetch() // Route 프리패치
이렇게 비교해보니, Nuxt4가 Next에 비해 기본 내장함수가 더 단단하게 지원해주는것으로 확인됩니다!
다양한 조건에서 세부적으로 사용할 수 있게 되기 때문에 보다 더 나은 DX환경을 제공해주는 것으로 보입니다.
3. useState()
놀라셨나요?? Nuxt4에서도 기본 내장 상태함수를 제공합니다. 그것도 Next와 동일한 이름으로요!
그렇지만 Nuxt 생태계에서는 pinia 라는 상태관리 모듈을 일반적으로 사용합니다.
일단, 기본제공되는 상태관리 함수는 각각 어떻게 사용하는지 같이 알아보도록 하죠.
🟢 Nuxt4
<script setup lang="ts">
// useState (Nuxt의 내장 상태 관리)
const counter = useState('counter', () => 0)
const user = useState('user', () => null)
// 서버와 클라이언트 간 자동 동기화
// Pinia 통합도 매끄러움
</script>
🔵 Next.js
// React의 기본 상태 관리 사용
const [counter, setCounter] = useState(0)
// 전역 상태는 Context API, Zustand, Redux 등 사용
// 서버 상태는 별도 관리 필요
Nuxt4에서는 SSR 친화적인 내장 상태함수를 제공하지만, Next는 CSR기반인 react 생태계의 상태관리 라이브러리에 의존하는 것이 차이점이라고 하겠습니다.
4. Fetch() / useFetch()
이제부터 소개할 API들은 이름이 조금씩 다르지만 기능적으로는 유사하게 동작합니다. 우선 데이터 조회 함수는 아래와 같이 기본적으로 사용할 수 있습니다.
🟢 Nuxt4
<script setup lang="ts">
// 자동 타입 추론, 캐싱, 에러 핸들링 내장
const { data, pending, error, refresh } = await useFetch('/api/users')
// 옵션이 풍부함
const { data } = await useFetch('/api/posts', {
key: 'posts',
server: true,
lazy: true,
transform: (data) => data.map(post => ({ ...post, formatted: true })),
onRequest({ request, options }) {
// 요청 전 처리
},
onResponse({ response }) {
// 응답 후 처리
}
})
</script>
🔵 Next.js
const data = await fetch('/api/users', {
cache: 'force-cache', // 'no-store', 'reload' 등
next: {
revalidate: 60,
tags: ['users']
}
})
여기서도, Nuxt4가 조금더 풍부한 기능을 제공하고 있습니다. 그렇기때문에, Next.js는 내장 fetch()함수와 외부라이브러리의 조합으로 구성하는 것이 일반적입니다.
5. Image Optimize
프론트앤드를 다루다보면, 필수적으로 이미지의 최적화를 진행해야 합니다. 두 프레임워크 모두 기본적인 이미지 최적화가 가능한 컴포넌트를 제공해줍니다. 한번 살펴보도록 하죠!
🟢 Nuxt4
<template>
// NuxtImg, NuxtPicture 컴포넌트
<NuxtImg
src="/hero.jpg"
width="800"
height="600"
format="webp"
quality="80"
loading="lazy"
placeholder="blur"
sizes="sm:100vw md:50vw lg:400px"
/>
</template>
<script setup lang="ts">
// 프로그래밍 방식
const img = useImage()
const optimizedSrc = img('/hero.jpg', { width: 400, quality: 80 })
</script>
🔵 Next.js
import Image from 'next/image'
<Image
src="/hero.jpg"
width={800}
height={600}
alt="Hero image"
quality={80}
loading="lazy"
placeholder="blur"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
6.useCookie(), cookies
당연하지만 브라우저에 저장되는 쿠키 관리 또한 유사하게 제공하고 있습니다.
이 또한 어떻게 사용되는지 비교해보면서 살펴보도록 하죠!
🟢 Nuxt4
<script setup lang="ts">
// useCookie
const counter = useCookie('counter', {
default: () => 0,
serializer: {
read: (value: string) => {
return value ? parseInt(value) : 0
},
write: (value: number) => {
return value.toString()
}
},
maxAge: 60 * 60 * 24 * 7 // 1주일
})
// 반응형으로 자동 동기화
counter.value++ // 자동으로 쿠키에 저장됨
</script>
🔵 Next.js
// cookies() 함수 (App Router)
import { cookies } from 'next/headers'
const cookieStore = cookies()
const theme = cookieStore.get('theme')
cookieStore.set('theme', 'dark')
// 클라이언트에서는 document.cookie 직접 사용하거나
// js-cookie 같은 라이브러리 사용
import Cookies from 'js-cookie'
Cookies.set('theme', 'dark', { expires: 7 })
7. SEO / Head
메타데이터 관리 또한 두 프레임워크 모두 유사하게 제공하고 있습니다.
🟢 Nuxt4
<script setup lang="ts">
// useHead - 반응형 메타데이터
useHead({
title: computed(() => `${post.value?.title} - My Blog`),
meta: [
{ name: 'description', content: () => post.value?.excerpt },
{ property: 'og:image', content: () => post.value?.image }
],
link: [
{ rel: 'canonical', href: () => `https://example.com${route.path}` }
]
})
// useSeoMeta - SEO 특화
useSeoMeta({
title: 'My Amazing Site',
ogTitle: 'My Amazing Site',
description: 'This is my amazing site, let me tell you all about it.',
ogDescription: 'This is my amazing site, let me tell you all about it.',
ogImage: 'https://example.com/image.png',
twitterCard: 'summary_large_image',
})
</script>
🔵 Next.js
// Metadata API (App Router)
export const metadata: Metadata = {
title: {
template: '%s | My Blog',
default: 'My Blog'
},
description: 'This is my blog',
openGraph: {
title: 'My Blog',
description: 'This is my blog',
images: ['https://example.com/og.jpg'],
},
twitter: {
card: 'summary_large_image',
},
}
// 동적 메타데이터
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.id)
return {
title: post.title,
description: post.excerpt,
}
}
Nuxt4에서는 컴포저블 함수로써, 손쉽게 모든 페이지에서 각각 Head를 설정할 수 있으며, Next 에서는 정적인 export 방식으로 명확한 타입 안정성을 가진다는 점이 특징이 되겠습니다.
마무리
이처럼 오늘은 간단하게 Nuxt4와 Next에서 제공하는 API를 비교하고 사용방법도 확인해보았습니다.
이름과 기능까지 동일한 내장함수부터 시작해서, 각각 프레임워크에서 사용되는 차이점까지 존재하는 것을 알 수 있었습니다.
Nuxt4에서 제공하는 기본 함수들이 더욱 유용하게 사용할 수 있다고 다시금 생각하게 되었습니다.
앞으로의 프론트앤드 기술의 나아가는 방향은 조금은 명확한 것 같습니다. 어느정도 각각의 프레임워크들에서 제공해주는 기능들은 유사(혹은 거의 동일)하게 수렴할 것이라고 판단됩니다.
이렇게 판단하는 근거는, 이 도구들을 사용하는 개발자들은 어쨌든 인간이 사용하는 것이고 해당 도구를 사용하면서 불편하거나 필요하다고 생각하는 기능은 거의 동일하다고 생각하기 때문입니다!
(외계인은 사용하지 않을테니까요!)
다음 글에서는 Nuxt4와 Next의 본격 성능 비교를 해볼 수 있는 자리를 가져보도록 할게요!
그럼 다음에 봬요!!

