-
🧩 React 19의 <Activity>란?React 2025. 11. 10. 00:11
UI 요소를 언마운트하지 않고 숨기면서 상태와 DOM을 보존하는 방법
웹 애플리케이션을 개발하다 보면 다음과 같은 상황을 마주칠 때가 있다.
- 사용자가 잠시 사이드바를 닫고 나중에 다시 열지 않을까 싶을 때
- 탭 전환 시 입력 폼의 값이나 스크롤 위치를 유지하고 싶을 때
- 숨겨진 구성요소는 렌더링 우선순위를 낮추고, 보이게 되는 순간 빠르게 표시되기를 원할 때
이런 경우 기존에는 조건부 렌더링을 사용해 해당 컴포넌트를 마운트/언마운트 했지만, 언마운트되는 순간 내부 상태와 DOM이 사라지는 문제가 있다.
React 19에서는 이런 문제를 해결하기 위해 <Activity> 경계(boundary)를 도입하여, 숨김 상태에서도 내부 상태와 DOM을 유지하면서, 보이는 순간 다시 활성화할 수 있게하는 기능을 추가해줬다 !
❓ <Activity>는 무엇인가?
<Activity>는 자식 컴포넌트를 감싸서 다음과 같은 동작을 가능하게 하는 컴포넌트다.
- mode 속성으로 'visible' 또는 'hidden'을 지정
- mode="hidden"이면 자식 컴포넌트는 display: none 처리되어 시각적으로 사라짐
- 하지만 컴포넌트 내부 상태(state), DOM 노드는 보존됨
- 숨겨진 동안 그 자식의 Effects(부수 효과)는 정리(clean-up)됨
- 다시 mode="visible"로 변경되면 이전 상태가 복원됨
요약하면 -->
<Activity>는 “언마운트 대신 숨김” 전략을 사용하며, 상태 유지 + DOM 유지 + 렌더 우선순위 조정이 가능한 경계이다.
왜 이 기능이 필요한가?
✅ 상태 유지
탭 전환이나 사이드바 등에서 사용자가 입력한 내용이나 펼쳐둔 메뉴 상태를 잃지 않고 유지할 수 있다.
기존에 {isShowing && <Component />} 방식으로 처리했다면, 해당 컴포넌트는 언마운트되어 상태가 초기화되었을 수 있다.✅ DOM 보존
특히 <textarea>, <video>, <canvas> 같은 요소가 있는 화면에서 DOM 노드가 제거되면 리렌더 시점에 초기화되거나 재설정되는 문제가 생긴다.
<Activity>는 DOM 노드가 있는 상태로 숨김 처리하므로 이런 문제를 줄일 수 있다.✅ 렌더/하이드레이션 우선순위 조정
React는 초기 하이드레이션(hydration)이나 렌더링 시 일부 부분을 우선적으로 처리하고 나머지를 나중에 처리할 수 있는 Selective Hydration 전략을 지원한다.
<Activity> 경계를 적절히 사용하면 렌더링 진입 시 바로 보여져야 하는 UI와 나중에 보여져도 되는 UI를 구분할 수 있어 초기 반응성을 개선할 수 있다.
실무 예제 코드
예제 1 – 사이드바 숨김/표시 + 상태 유지 🎍
// Sidebar.tsx import { useState } from 'react'; export default function Sidebar() { const [isExpanded, setIsExpanded] = useState(false); return ( <aside style={{ width: isExpanded ? 300 : 100, transition: 'width 0.3s' }}> <button onClick={() => setIsExpanded(prev => !prev)}> {isExpanded ? 'Collapse' : 'Expand'} </button> {isExpanded && <nav>/* 메뉴 항목 나열 */</nav>} </aside> ); }// App.tsx import { useState } from 'react'; import { Activity } from 'react'; import Sidebar from './Sidebar'; export default function App() { const [showSidebar, setShowSidebar] = useState(true); return ( <> <button onClick={() => setShowSidebar(prev => !prev)}> Toggle Sidebar </button> <Activity mode={showSidebar ? 'visible' : 'hidden'}> <Sidebar /> </Activity> <main> <h1>Main Content</h1> </main> </> ); }위 코드에서 Sidebar를 숨겼다가 다시 표시해도 내부 isExpanded 상태가 유지된다 👍
예제 2 – 탭 전환 + 입력 폼 상태 유지 🥷
// Home.tsx export default function Home() { return <div>Home Content</div>; } // Contact.tsx import { useState } from 'react'; export default function Contact() { const [message, setMessage] = useState(''); return ( <div> <h2>Contact Us</h2> <textarea value={message} onChange={e => setMessage(e.target.value)} /> <p>Message length: {message.length}</p> </div> ); }// App.tsx import { useState } from 'react'; import { Activity } from 'react'; import Home from './Home'; import Contact from './Contact'; export default function App() { const [tab, setTab] = useState<'home' | 'contact'>('home'); return ( <> <button onClick={() => setTab('home')}>Home</button> <button onClick={() => setTab('contact')}>Contact</button> <Activity mode={tab === 'home' ? 'visible' : 'hidden'}> <Home /> </Activity> <Activity mode={tab === 'contact' ? 'visible' : 'hidden'}> <Contact /> </Activity> </> ); }위 구조를 사용하면 Contact 탭에서 메시지를 입력하다가 Home 탭으로 갔다가 다시 Contact 탭으로 돌아와도 입력했던 메시지가 사라지지 않는다.
😆 주의사항 및 체크포인트
- <Activity> 경계 안의 자식 컴포넌트가 DOM 없는 텍스트만 반환하는 경우, 숨김 상태에서 아무것도 렌더링되지 않을 수 있다.
- 예 -->
<Activity mode="hidden"><() => 'Hello' /></Activity>는 DOM을 갖지 않기 때문에 아무 출력이 없을 수 있다.
- 예 -->
- 숨김 상태에서 Effects(ex. useEffect)가 마운트되지 않고 정리(clean-up)된다.
즉, 숨김 상태의 자식은 다시 보일 때까지 Effect가 재실행될 수 있음을 염두에 두어야 함 - <video>, <audio>, <iframe> 등 내부에 제어 로직이나 외부 영향이 있는 요소가 있는 경우 숨김 상태에서도 계속 동작하는 경우가 있으므로 manual cleanup이 필요함
(예: 영상 일시정지) - 아직까지 이 기능은 실험적(experimental)이며, React 버전이나 설정에 따라 호환성 이슈가 있을 수 있으므로 실제 도입 전에 버전 및 문서 확인을 꼭 하자 👍
실무에서는 UI 일부를 언마운트하지 않고 숨기면서 다시 복원하는 요구사항이 많다.
다양한 탭, 드롭다운, 사이드바, 입력 폼 등에서 사용자 경험을 해치지 않고 상태를 유지하고 싶을 때, <Activity>는 매우 유용한 도구가 될 수 있을 것 같다.
다만 이 기능을 사용할 때는 숨김 상태의 동작, Effect의 재실행 여부, DOM 존재 여부, 버전 호환성 등을 반드시 고려해야 한다.
장기적으로는 <Activity>를 적절히 활용해 UI 렌더링 부담을 줄이고, 사용자 인터랙션 경험을 개선할 수 있지 않을까?
(다만, 실무에서라면 개인적으로는 더 많이 알아보고 사용 경험들도 쌓아서 적용하는 편이 좋을지도?)
참고 자료 🐦
- React 공식 문서 – <Activity>: https://react.dev/reference/react/Activity
- React Labs 블로그 – View Transitions, Activity, and more: https://react.dev/blog/2025/04/23/react-labs-view-transitions-activity-and-more
- LogRocket 튜토리얼 – React View Transitions & Activity API: https://blog.logrocket.com/react-view-transitions-activity-api/
'React' 카테고리의 다른 글
중첩 래퍼(Wrapper) 컴포넌트가 많은 UI 구조에서의 이벤트 처리 전략 고민해보기 🧐 (0) 2025.11.02 리액트에서 @loadable/component로 코드 스플리팅 하기 (1) 2025.08.18 React에서 우클릭 메뉴를 커스텀하게 만들기 (onContextMenu 이벤트를 활용한 간단한 메뉴 구현) 😘 (3) 2025.07.27 Controlled 컴포넌트와 UnControlled 컴포넌트 😊 공통 컴포넌트 고민해보기 (1) 2025.07.13 🧩 Rspack 기반 Module Federation 튜토리얼 (React 기반 MFA) (0) 2025.06.19