ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 🧩 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 렌더링 부담을 줄이고, 사용자 인터랙션 경험을 개선할 수 있지 않을까?
    (다만, 실무에서라면 개인적으로는 더 많이 알아보고 사용 경험들도 쌓아서 적용하는 편이 좋을지도?)
     

    참고 자료 🐦

Designed by Tistory.