Nuxt 3와 4에서 useState와 ref 사용 기준
Nuxt에서 반응형 값을 만들 때 Vue의 ref와 Nuxt의 useState를 모두 사용할 수 있다.
두 함수 모두 Ref를 반환하지만 상태가 살아 있는 범위와 SSR 결과를 브라우저로 전달하는 방식이 다르다.
한 컴포넌트 안에서만 쓰는 값은 ref가 기본 선택이다.
여러 컴포넌트가 같은 값을 공유하거나 서버에서 만든 값을 hydration 뒤에도 유지해야 한다면 useState가 맞다.
이 글은 2026년 8월 현재 Nuxt 3.21과 Nuxt 4.5 문서를 기준으로 한다.
Nuxt 3는 2026년 7월 31일 지원이 종료됐으므로 새 프로젝트라면 Nuxt 4를 기준으로 잡는 편이 안전하다.
ref의 컴포넌트 로컬 상태
ref는 Vue의 반응형 값을 만드는 API다.
컴포넌트의 <script setup>에서 호출하면 각 컴포넌트 인스턴스가 자기 값을 가진다.
<script setup lang="ts">
const isOpen = ref(false)
function toggle() {
isOpen.value = !isOpen.value
}
</script>
<template>
<button type="button" @click="toggle">
{{ isOpen ? '닫기' : '열기' }}
</button>
</template>메뉴의 열림 여부, 입력 중인 문자열, 특정 컴포넌트의 탭처럼 다른 곳에서 알 필요가 없는 UI 상태에 잘 맞는다.
컴포넌트가 사라지면 해당 인스턴스의 상태도 함께 정리되므로 수명도 이해하기 쉽다.
같은 composable 안에서 ref를 만들어 반환해도 호출할 때마다 새 ref가 만들어지는 것이 일반적이다.
호출한 모든 컴포넌트가 같은 값을 보기를 기대한다면 별도 공유 구조가 필요하다.
useState의 key 기반 공유
useState는 Nuxt가 제공하는 SSR 친화적인 공유 상태 composable이다.
같은 Nuxt 앱 안에서 같은 key를 사용한 호출은 같은 ref를 돌려준다.
// app/composables/useTheme.ts
export const useTheme = () => {
return useState<'light' | 'dark'>('theme', () => 'light')
}<script setup lang="ts">
const theme = useTheme()
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
</script>다른 컴포넌트에서 useTheme()을 다시 호출해도 theme key의 상태를 함께 사용한다.
문자열 key는 앱 전체의 이름표이므로 기능 이름을 포함해 충돌하지 않게 정하는 편이 좋다.
key를 생략하면 Nuxt가 호출 파일과 줄 위치를 바탕으로 key를 만든다.
여러 위치에서 의도적으로 공유할 상태라면 자동 key에 기대지 않고 명시적인 key를 사용하는 편이 코드의 의도를 분명하게 만든다.
SSR과 hydration의 차이
Nuxt의 초기 화면 코드는 서버 렌더링과 브라우저 hydration에서 실행될 수 있다.
실행할 때마다 결과가 달라지는 값을 일반 ref로 만들고 화면에 출력하면 서버 HTML과 브라우저의 첫 값이 달라질 수 있다.
<script setup lang="ts">
const randomNumber = ref(Math.round(Math.random() * 1000))
</script>
<template>
<p>{{ randomNumber }}</p>
</template>useState의 초기화 함수로 만든 값은 서버의 Nuxt payload에 들어가 브라우저로 전달된다.
브라우저는 같은 key의 상태를 복원하므로 hydration에서 초기화 함수를 다시 계산한 다른 값을 곧바로 표시하지 않는다.
<script setup lang="ts">
const randomNumber = useState('random-number', () => {
return Math.round(Math.random() * 1000)
})
</script>
<template>
<p>{{ randomNumber }}</p>
</template>그렇다고 SSR에서 사용하는 모든 ref를 useState로 바꿀 필요는 없다.
초깃값이 서버와 브라우저에서 같고 다른 컴포넌트와 공유하지 않는 로컬 상태라면 ref가 더 작은 범위를 정확히 표현한다.
서버에서 조회한 데이터와 요청 상태는 useState에 직접 넣기보다 useFetch나 useAsyncData가 맞는 경우가 많다.
초기 조회와 이벤트 요청의 구분은 Nuxt 3와 4에서 useFetch와 $fetch 차이에서 이어서 볼 수 있다.
서버 요청 격리와 직렬화
SSR 서버의 module 최상위에서 ref를 만들어 export하면 하나의 서버 프로세스를 사용하는 여러 요청이 같은 상태를 볼 수 있다.
사용자별 값이 섞이거나 상태가 계속 남아 메모리를 차지할 수 있으므로 전역 ref 패턴은 피해야 한다.
// 피해야 하는 서버 공유 상태
export const currentUser = ref<{ id: string } | null>(null)
// 요청별로 격리되는 Nuxt 상태
export const useCurrentUser = () => {
return useState<{ id: string } | null>('current-user', () => null)
}서버에서 useState의 상태는 요청 단위로 격리되고, 클라이언트에서는 현재 Nuxt 앱 안에서 key로 공유된다.
다만 payload로 브라우저에 전달되므로 비밀 키나 서버 전용 정보는 useState에 넣으면 안 된다.
상태 값은 직렬화할 수 있는 데이터로 제한하는 편이 안전하다.
함수, Symbol, class instance처럼 그대로 직렬화할 수 없는 값을 넣으면 오류가 나거나 기대한 형태로 복원되지 않을 수 있다.
API 비밀 값과 공개 값을 나누는 기준은 Nuxt 3와 4에서 runtimeConfig로 환경변수 관리하기에서 정리했다.
초기화와 선택 기준
useState의 초기화 함수는 해당 key가 아직 만들어지지 않았을 때만 기본값을 제공한다.
비동기 데이터가 필요하다면 초기화 함수 자체를 async로 만들기보다 callOnce와 함께 명시적으로 채우는 방식을 검토한다.
<script setup lang="ts">
const settings = useState<{ locale: string }>('settings')
await callOnce(async () => {
settings.value = await $fetch('/api/settings')
})
</script>큰 객체에서 깊은 반응성이 필요 없다면 Nuxt 4의 useState 초기화 함수에서 shallowRef를 반환해 반응형 비용을 줄일 수 있다.
로그아웃이나 테스트 초기화처럼 저장한 상태를 지울 때는 clearNuxtState를 사용할 수 있다.
Nuxt 4.4 이후에는 clearNuxtState의 reset 동작이 compatibility 설정에 따라 달라질 수 있으므로 설치 버전의 문서를 확인해야 한다.
공유 상태가 많고 action, plugin, 개발 도구 같은 구조가 필요해지면 useState를 계속 늘리기보다 Pinia 같은 store 도입을 검토한다.
선택 기준은 상태의 범위에서 시작한다.
컴포넌트 로컬 UI 상태는 ref, key로 공유하고 SSR payload에 보존할 작은 상태는 useState, 복잡한 도메인 상태는 별도 store가 자연스럽다.
참고 자료





