안녕하세요. J4J입니다.
이번 포스팅은 React 19.2에서 새로 추가된 Activity, useEffectEvent, View Transitions를 Next.js 16 App Router 환경에 통합해 활용하는 방법을 살펴보는 시간을 가져보려고 합니다.
세 API 각각이 무엇을 해결하는지는 별도로 다루지 않으니, 기본 개념이 궁금하신 분들은 지난 포스팅을 먼저 참고해 주시길 바랍니다.
React 19.2: Activity·useEffectEvent 정식 지원, View Transitions 실험 단계
안녕하세요. J4J입니다. 이번 포스팅은 React 19.2에서 새롭게 추가된 Activity, useEffectEvent와 함께, 아직 실험 단계에 있는 View Transitions까지 살펴보는 시간을 가져보려고 합니다.
jforj.tistory.com
Next.js 16 App Router는 별도 설치 없이 세 API를 모두 사용할 수 있다
순수 React 프로젝트에서 View Transitions를 사용하려면 react, react-dom을 Canary 채널로 별도 설치해야 했습니다.
// ❌ 순수 React 프로젝트에서 View Transitions를 쓰려면 필요했던 절차
// terminal
$ npm install react@canary react-dom@canary
이렇게 설치해야 했던 이유는 View Transitions가 React 19.2 시점에도 정식(stable) 채널에는 포함되지 않고 Canary 채널에서만 제공되기 때문입니다.
하지만 Next.js 16의 App Router는 이미 React Canary 릴리스를 기반으로 동작하고 있어서, react@canary를 직접 설치하지 않아도 View Transitions를 포함한 React 19.2 신기능을 곧바로 사용할 수 있습니다.
// ✅ Next.js 16 프로젝트는 별도 설치 없이 바로 import
import { Activity, useEffectEvent, ViewTransition } from 'react'
그러므로 Next.js 16 App Router 프로젝트라면 react, react-dom 버전을 16과 함께 올리는 것만으로 세 API를 모두 사용할 수 있고, 순수 React 프로젝트처럼 Canary 채널을 별도로 관리할 필요가 없습니다.
저의 경우 이 사실을 처음 확인했을 때, Next.js 프로젝트에서도 Canary 설치 절차를 별도로 거쳐야 할 것으로 예상했지만 실제로는 버전만 맞추면 되는 수준이라 확인 과정이 한결 수월했습니다.
다음으로는 Activity를 App Router 환경에서 어떻게 활용하는지 살펴보겠습니다.
Activity로 App Router 페이지의 상태를 유지하기
Activity의 기본 동작 방식(상태 보존, Effect 정리)은 순수 React 환경과 동일하지만, App Router에서는 어디에 'use client'를 선언해야 하는지가 추가로 중요해집니다.
App 폴더에 작성되는 컴포넌트는 기본적으로 Server Component로 동작하는데, 탭 전환처럼 클릭에 반응해 mode를 바꾸는 상태(state)는 Server Component에서 다룰 수 없습니다.
그러므로 tab 상태를 관리하며 Activity의 mode를 전환하는 컴포넌트는 파일 상단에 'use client'를 선언해야 합니다.
// app/activity/page.tsx
'use client'
import { Activity, useState } from 'react'
import NoteEditor from './NoteEditor'
export default function ActivityPage() {
const [tab, setTab] = useState<'note' | 'other'>('note')
return (
<div className="flex flex-col gap-4 p-6">
<div className="flex gap-2">
<button
className="cursor-pointer rounded border border-gray-300 px-3 py-1.5 text-sm hover:bg-gray-50"
onClick={() => setTab('note')}
>
메모
</button>
<button
className="cursor-pointer rounded border border-gray-300 px-3 py-1.5 text-sm hover:bg-gray-50"
onClick={() => setTab('other')}
>
다른 화면
</button>
</div>
<Activity mode={tab === 'note' ? 'visible' : 'hidden'}>
<NoteEditor />
</Activity>
<Activity mode={tab === 'other' ? 'visible' : 'hidden'}>
<p className="text-sm text-gray-500">다른 화면입니다.</p>
</Activity>
</div>
)
}
// app/activity/NoteEditor.tsx
'use client'
import { useEffect, useState } from 'react'
export default function NoteEditor() {
const [text, setText] = useState('')
useEffect(() => {
console.log('자동 저장 타이머를 구독합니다.')
const id = setInterval(() => {
console.log('자동 저장을 실행합니다.')
}, 3000)
return () => {
console.log('자동 저장 타이머를 구독 해제합니다.')
clearInterval(id)
}
}, [])
return (
<textarea
className="w-full rounded border border-gray-300 p-2 text-sm"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="메모를 입력하세요"
/>
)
}
메모 탭에 텍스트를 입력한 뒤 다른 화면 탭으로 전환했다가 다시 메모 탭으로 돌아와도, 입력했던 값이 그대로 남아 있는 것을 확인할 수 있습니다.
그리고 다른 화면 탭으로 전환하는 순간 콘솔에 "자동 저장 타이머를 구독 해제합니다" 로그가 남고, 다시 메모 탭으로 돌아오면 "자동 저장 타이머를 구독합니다" 로그가 다시 남는 것도 함께 확인할 수 있습니다.
다음으로는 useEffectEvent를 App Router의 Client Component 안에서 사용할 때 유의할 점을 살펴보겠습니다.
useEffectEvent, Client Component 경계를 벗어나면 사용할 수 없다
useEffectEvent도 사용법 자체는 순수 React 환경과 동일합니다.
다만 useEffectEvent는 Effect와 함께 동작하는 훅이고, Effect는 브라우저에서만 실행되는 client 전용 개념이기 때문에 Server Component에서는 애초에 호출할 수 없습니다.
// ❌ 'use client' 없이 useEffectEvent를 호출하는 경우
// app/effect-event/page.tsx
import { useEffect, useEffectEvent, useState } from 'react'
export default function EffectEventPage() {
const [step, setStep] = useState(1)
const onTick = useEffectEvent(() => {
console.log(step)
})
// ...
}
이렇게 작성하면 useState, useEffect와 마찬가지로 useEffectEvent도 Server Component 안에서는 사용할 수 없다는 빌드 에러가 발생합니다.
// ✅ 파일 최상단에 'use client'를 선언한 경우
// app/effect-event/page.tsx
'use client'
import { useEffect, useEffectEvent, useState } from 'react'
export default function EffectEventPage() {
const [count, setCount] = useState(0)
const [step, setStep] = useState(1)
const [intervalMs, setIntervalMs] = useState(1000)
const onTick = useEffectEvent(() => {
setCount((prev) => prev + step)
})
useEffect(() => {
const id = setInterval(() => {
onTick()
}, intervalMs)
return () => clearInterval(id)
}, [intervalMs])
return (
<div className="flex flex-col gap-4 p-6">
<p>카운트: {count}</p>
<div className="flex gap-2">
<button
className="cursor-pointer rounded border border-gray-300 px-3 py-1.5 text-sm hover:bg-gray-50"
onClick={() => setStep((prev) => (prev === 1 ? 5 : 1))}
>
증가량 {step === 1 ? '5로 변경' : '1로 변경'}
</button>
<button
className="cursor-pointer rounded border border-gray-300 px-3 py-1.5 text-sm hover:bg-gray-50"
onClick={() => setIntervalMs((prev) => (prev === 1000 ? 2000 : 1000))}
>
간격 {intervalMs === 1000 ? '2000ms로 변경' : '1000ms로 변경'}
</button>
</div>
</div>
)
}
그러므로 App Router에서 useEffectEvent를 사용하는 컴포넌트는 항상 'use client'가 선언된 파일 안에 있어야 하며, Server Component에서 곧바로 호출할 수 없다는 제약을 먼저 확인하는 것이 좋습니다.
증가량 변경 버튼을 눌러도 간격은 그대로 유지되고, 간격 변경 버튼을 눌렀을 때만 새로운 간격으로 다시 구독하는 것도 순수 React 환경과 동일하게 확인할 수 있습니다.
다음으로는 이번 포스팅의 핵심인 View Transitions를 next/link 네비게이션에 통합하는 방법을 살펴보겠습니다.
View Transitions로 페이지 이동에 애니메이션 적용하기
순수 React 환경에서는 ViewTransition을 활성화하려면 setState 호출을 startTransition으로 직접 감싸야 했습니다.
하지만 Next.js App Router에서는 라우트 이동 자체가 이미 하나의 Transition으로 처리되기 때문에, next/link로 이동하는 것만으로 ViewTransition 애니메이션이 자동으로 활성화됩니다.
별도로 startTransition을 호출하는 코드를 작성할 필요가 없다는 뜻입니다.
목록 페이지에서 상세 페이지로 이동할 때, 같은 항목이라는 것을 시각적으로 이어 보여주는 방법을 예로 들어보겠습니다.
목록의 카드와 상세 페이지의 카드를 같은 name을 가진 ViewTransition으로 감싸면, React가 두 페이지에서 이름이 같은 요소를 찾아 크기와 위치를 자동으로 이어 애니메이션합니다.
// app/view-transition/page.tsx
import Link from 'next/link'
import { ViewTransition } from 'react'
const colors = [
{ id: 1, name: '산호', hex: '#f97362' },
{ id: 2, name: '민트', hex: '#3ecf8e' },
{ id: 3, name: '라벤더', hex: '#8b7bf0' },
{ id: 4, name: '레몬', hex: '#f5c445' },
]
export default function ViewTransitionPage() {
return (
<div className="flex flex-col gap-4 p-6">
<h1 className="text-lg font-medium">색상 목록</h1>
<div className="grid grid-cols-2 gap-3">
{colors.map((color) => (
<Link key={color.id} href={`/view-transition/detail?id=${color.id}`}>
<ViewTransition name={`color-${color.id}`}>
<div
className="flex h-24 items-center justify-center rounded text-sm text-white"
style={{ backgroundColor: color.hex }}
>
{color.name}
</div>
</ViewTransition>
</Link>
))}
</div>
</div>
)
}
// app/view-transition/detail/page.tsx
import Link from 'next/link'
import { ViewTransition } from 'react'
const colors = [
{ id: 1, name: '산호', hex: '#f97362' },
{ id: 2, name: '민트', hex: '#3ecf8e' },
{ id: 3, name: '라벤더', hex: '#8b7bf0' },
{ id: 4, name: '레몬', hex: '#f5c445' },
]
interface Props {
searchParams: Promise<{ id?: string }>
}
export default async function ViewTransitionDetailPage(props: Props) {
const { id } = await props.searchParams
const color = colors.find((item) => item.id === Number(id)) ?? colors[0]
return (
<div className="flex flex-col gap-4 p-6">
<Link href="/view-transition" className="text-sm text-blue-600">
← 목록으로
</Link>
<ViewTransition name={`color-${color.id}`}>
<div
className="flex h-64 items-center justify-center rounded text-2xl text-white"
style={{ backgroundColor: color.hex }}
>
{color.name}
</div>
</ViewTransition>
</div>
)
}
목록에서 카드를 클릭하면 클릭한 색상 카드가 그 자리에서 사라지고 새로운 카드가 나타나는 대신, 같은 카드가 상세 페이지의 큰 영역으로 자연스럽게 커지면서 이동하는 애니메이션을 확인할 수 있습니다.
뒤로가기로 목록에 돌아오면 반대로 축소되며 원래 카드 위치로 되돌아가는 것도 함께 확인할 수 있습니다.
이 애니메이션은 Chromium 125 이상, 최신 Safari, Firefox처럼 View Transitions API의 최신 기능을 지원하는 브라우저에서만 재생되며, 지원하지 않는 브라우저에서는 애니메이션 없이 페이지 이동만 정상적으로 동작합니다.
그러므로 별도의 폴백 코드를 작성하지 않아도 오래된 브라우저에서 기능 자체가 깨지지는 않습니다.
이번 글에서는 두 페이지 사이의 카드 전환 하나만 다뤘지만, Next.js 공식 문서에서는 Suspense fallback과 연결한 로딩 전환, 이동 방향에 따라 다른 애니메이션을 적용하는 방법 같은 패턴도 추가로 안내하고 있으니 더 다양한 패턴이 궁금하시다면 참고하시면 좋을 것 같습니다.
Next.js 공식 문서 - Designing view transitions
Guides: View transitions | Next.js
Learn how to use view transitions to communicate meaning during navigation, loading, and content changes in a Next.js app.
nextjs.org
다음으로는 세 API를 실무 App Router 프로젝트에 도입할 때 고려할 점을 정리해 보겠습니다.
App Router 프로젝트에 도입할 때 고려할 점
지금까지 살펴본 내용을 정리하면 다음과 같습니다.
- 세 API 모두 Next.js 16 App Router에서는 별도 설치 없이 바로 사용 가능 — react, react-dom을 16과 함께 올리는 것만으로 충분함
- Activity, useEffectEvent — 상태나 Effect를 다루는 컴포넌트는 항상 'use client'가 선언된 파일 안에 있어야 함
- View Transitions — next/link로 이동하는 라우트 전환은 이미 Transition으로 처리되어 startTransition 없이 자동으로 애니메이션이 활성화됨
개인적으로 가장 체감이 큰 부분은 View Transitions라고 생각합니다.
순수 React 프로젝트에서는 Canary 설치와 startTransition 처리를 직접 챙겨야 했지만, App Router에서는 두 가지 모두 신경 쓸 필요 없이 name prop만 맞춰주면 되기 때문에 실무 페이지 전환에 바로 적용해 볼 여지가 커 보입니다.
다만 Activity와 useEffectEvent는 실험 단계가 아닌 정식 기능이므로 지금 진행 중인 프로젝트에도 바로 도입해 볼 수 있지만, View Transitions는 여전히 Canary 채널의 실험적 기능이라는 점은 순수 React 환경과 동일하게 유의해야 합니다.
이상으로 Next.js 16 App Router에서 Activity, useEffectEvent, View Transitions를 통합해 활용하는 방법에 대해 간단하게 알아보는 시간이었습니다.
읽어주셔서 감사합니다.
'SPA > Next' 카테고리의 다른 글
| Next.js 16 next/image 기본값 변경, quality 75부터 SSRF 방어까지 점검 가이드 (0) | 2026.08.12 |
|---|---|
| Next.js 16, Layout Deduplication과 Incremental Prefetching 동작 원리 (0) | 2026.08.06 |
| React Compiler와 Next.js 16: useMemo/useCallback이 사라지는 이유 (0) | 2026.08.02 |
| Turbopack, Next.js 16 기본 번들러 전환과 webpack 커스텀 설정 마이그레이션 (0) | 2026.08.01 |
| Next.js 16 proxy.ts, middleware.ts를 대체하는 네트워크 경계 재정의 (1) | 2026.07.27 |
댓글