<Activity>

2026. 9. 14. 19:35·리액트/레퍼런스 (react 19ver)

좋습니다. 이번 페이지의 핵심은 <Activity>가 “보이지 않게 숨기되, 상태와 DOM은 보존하고, Effects는 정리하는” 새로운 종류의 UI 경계라는 점입니다.

React 19.3 공식 문서 기준으로 <Activity>는 자식 UI를 숨겼다가 다시 보여줄 때 이전 state와 DOM 상태를 복원할 수 있게 해줍니다. 동시에 hidden 상태에서는 Effects를 cleanup하고, 보이지 않는 UI의 렌더링 우선순위도 낮춥니다. (React)

1. 기본 형태

import { Activity } from 'react';

function App() {
  return (
    <Activity mode="visible">
      <Sidebar />
    </Activity>
  );
}

mode는 두 가지입니다.

"visible"
"hidden"

생략하면 기본값은 "visible"입니다. (React)

보통은 조건에 따라 이렇게 씁니다.

<Activity mode={isShowingSidebar ? 'visible' : 'hidden'}>
  <Sidebar />
</Activity>

2. 그냥 조건부 렌더링하면 안 되나?

기존에는 보통 이렇게 했습니다.

{isShowingSidebar && <Sidebar />}

isShowingSidebar가 false가 되면:

Sidebar unmount

됩니다.

그리고 다시 true가 되면:

새로운 Sidebar mount

입니다.

따라서 Sidebar 내부 state는 사라집니다.

예를 들어:

function Sidebar() {
  const [isOpen, setIsOpen] = useState(false);

  // ...
}

사용자가 메뉴를 펼쳐놓고 Sidebar를 숨겼다가 다시 열면:

isOpen = false

부터 다시 시작합니다.


3. Activity를 사용하면 state를 보존한다

이걸:

<Activity mode={isShowingSidebar ? 'visible' : 'hidden'}>
  <Sidebar />
</Activity>

로 바꾸면 Sidebar를 숨겨도 내부 state를 React가 보존합니다. 다시 visible이 되면 그 state가 복구됩니다. (React)

예를 들어:

Sidebar state

isOverviewOpen = true

인 상태에서:

visible
   ↓
hidden
   ↓
visible

이 되어도:

isOverviewOpen = true

가 유지됩니다.

이게 Activity의 첫 번째 중요한 목적입니다.


4. 그런데 단순히 CSS로 display: none 하는 것과는 다르다

여기가 가장 중요합니다.

이렇게 직접 CSS만 바꾼다고 생각해봅시다.

<div style={{ display: hidden ? 'none' : 'block' }}>
  <Sidebar />
</div>

이 경우 컴포넌트는 계속 mount되어 있습니다.

따라서 내부 Effect도 계속 살아 있습니다.

예를 들어:

useEffect(() => {
  const connection = connect();

  return () => {
    connection.disconnect();
  };
}, []);

CSS로만 숨기면:

display: none

하지만 connection은 계속 살아 있음

입니다.

Activity는 다릅니다.

hidden이 되면 React가 자식을 시각적으로 숨기면서 Effects를 cleanup합니다. 다시 visible이 되면 Effects를 다시 생성합니다. (React)

즉:

<Activity mode="hidden">

DOM/state
→ 보존

Effects
→ cleanup

입니다.

이 조합이 Activity의 핵심입니다.


5. Activity를 한 문장으로 이해하기

Activity는 unmount와 display: none의 중간쯤에 있습니다.

                 state    DOM    Effects
-----------------------------------------
Unmount           ❌      ❌      cleanup
CSS display:none   ✅      ✅      계속 실행
Activity hidden    ✅      ✅      cleanup

이 표가 이번 페이지에서 가장 중요한 mental model입니다.

정확히 말하면 hidden Activity의 자식은 개념적으로 “state를 저장한 채 unmount된 것처럼” 생각하는 것이 좋다고 공식 문서도 설명합니다. (React)


6. hidden에서는 실제로 어떻게 숨기는가?

React는 Activity가 hidden이 되면 자식을:

display: none;

방식으로 시각적으로 숨깁니다. (React)

따라서 DOM 자체는 남아 있습니다.

이게 중요한 이유는 React state뿐 아니라 브라우저 DOM 자체가 가지고 있던 임시 상태(ephemeral DOM state)도 유지할 수 있기 때문입니다.


7. uncontrolled DOM state도 보존할 수 있다

공식 문서의 좋은 예제가 <textarea>입니다.

function Contact() {
  return (
    <textarea />
  );
}

여기에는 React state가 없습니다.

즉 controlled input이 아니라 브라우저 DOM이 직접 값을 가지고 있습니다.

사용자가:

"내가 작성 중인 메시지..."

를 입력했다고 합시다.

일반적인 조건부 렌더링으로:

{activeTab === 'contact' && <Contact />}

사용하면 탭을 바꿀 때 <textarea>가 unmount됩니다.

그러면 DOM도 사라지므로 작성 중인 내용 역시 사라집니다. (React)


8. Activity는 DOM도 보존한다

다음처럼 만들면:

<Activity mode={activeTab === 'contact' ? 'visible' : 'hidden'}>
  <Contact />
</Activity>

Contact 탭이 hidden이어도 <textarea> DOM이 보존됩니다.

그래서:

Contact 탭
→ "Hello React" 입력

Home 탭
→ Contact hidden

Contact 탭
→ "Hello React" 그대로

가 가능합니다. (React)

여기서 중요한 포인트는:

React state가 아니라 DOM 자체의 상태도 보존할 수 있다.

입니다.


9. hidden이어도 컴포넌트는 완전히 멈추지 않는다

이 부분도 매우 중요합니다.

Activity가 hidden일 때 자식들은 새 props 등에 반응해서 여전히 re-render될 수 있습니다.

다만 visible 콘텐츠보다 낮은 우선순위로 렌더링됩니다. (React)

즉:

hidden
≠ React tree에서 완전히 제거

입니다.

개념적으로:

visible UI
→ 중요한 foreground 작업

hidden Activity
→ background 작업

으로 취급됩니다.

그래서 공식 문서는 Activity를 일종의 background activity를 렌더링하는 메커니즘이라고 설명합니다. (React)


10. 이걸 이용하면 pre-rendering도 가능하다

Activity의 두 번째 큰 사용 사례입니다.

아직 사용자에게 보여주지 않을 UI도:

<Activity mode="hidden">
  <Posts />
</Activity>

처럼 미리 렌더링할 수 있습니다.

처음에는 보이지 않지만 React는 낮은 우선순위로 Posts를 준비할 수 있습니다. (React)

그러면 사용자가 나중에 Posts 탭을 클릭했을 때:

그제서야 렌더링 시작

이 아니라:

이미 어느 정도 준비되어 있음

이 될 수 있습니다.


11. 탭 UI에서 특히 유용하다

예를 들어:

function App() {
  const [activeTab, setActiveTab] = useState('home');

  return (
    <>
      <TabButton onClick={() => setActiveTab('home')}>
        Home
      </TabButton>

      <TabButton onClick={() => setActiveTab('posts')}>
        Posts
      </TabButton>

      <Suspense fallback={<h1>Loading...</h1>}>
        <Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}>
          <Home />
        </Activity>

        <Activity mode={activeTab === 'posts' ? 'visible' : 'hidden'}>
          <Posts />
        </Activity>
      </Suspense>
    </>
  );
}

라고 해봅시다.

처음:

Home visible
Posts hidden

입니다.

하지만 Posts가 hidden이어도 React가 낮은 우선순위로 렌더링을 준비할 수 있습니다.

그래서 사용자가:

Posts 클릭

했을 때 loading 시간이 줄어들 가능성이 있습니다. (React)


12. Suspense와 Activity의 관계

방금 예제에서 <Suspense>가 같이 나왔죠.

두 개는 역할이 다릅니다.

Activity
→ 보여줄지 숨길지
→ state/DOM 보존
→ hidden UI를 background에서 준비

Suspense
→ 아직 준비되지 않은 UI가 있을 때
→ fallback과 reveal 시점 관리

예를 들어 Posts가 Suspense-enabled data를 읽는다면:

<Activity mode="hidden">
  <Posts />
</Activity>

상태에서도 data/code를 미리 준비할 수 있습니다.

나중에 visible로 바뀌면 이미 준비가 끝나 있어서 빠르게 나타날 수 있습니다.


13. 하지만 useEffect fetch는 pre-render되지 않는다

지난 Suspense 페이지와 연결되는 중요한 제한입니다.

function Posts() {
  useEffect(() => {
    fetchPosts();
  }, []);
}

이런 방식이라면 hidden Activity에서 Effect가 실행되지 않습니다.

왜냐하면 Activity는 hidden 상태에서 Effects를 mount하지 않기 때문입니다. (React)

따라서:

hidden pre-render
→ component render 가능
→ Effect는 실행하지 않음

입니다.

공식 문서는 pre-rendering 중 data를 미리 가져오려면 Suspense를 활성화시키는 데이터 소스, 예를 들어 use(Promise)로 읽는 데이터를 사용해야 한다고 명시합니다. (React)

이건 아주 중요한 차이입니다.


14. 왜 hidden 상태에서 Effect를 없애는가?

Activity를 사용해서 UI를 숨겼는데:

useEffect(() => {
  const socket = connectToChat();

  return () => {
    socket.disconnect();
  };
}, []);

이 socket이 계속 살아 있다면 숨겨진 UI가 불필요한 작업을 계속 수행합니다.

예를 들어:

숨겨진 채팅 탭
→ socket 계속 메시지 수신
→ state update 계속
→ CPU/네트워크 사용

이럴 수 있습니다.

Activity는 hidden이 되면 Effect cleanup을 호출하기 때문에:

숨겨짐
→ subscription 종료

다시 visible
→ Effect 재실행
→ subscription 다시 연결

됩니다. (React)


15. 우리가 StrictMode에서 배운 내용과 연결된다

이전 페이지에서 배웠죠.

올바른 Effect:

useEffect(() => {
  const connection = connect();

  return () => {
    connection.disconnect();
  };
}, []);

StrictMode가:

setup
cleanup
setup

을 일부러 실행했던 이유가 바로 이런 상황에 강한 Effect를 만들기 위해서입니다.

Activity에서는 실제로:

visible
→ Effect setup

hidden
→ Effect cleanup

visible
→ Effect setup

이 발생합니다.

그래서:

StrictMode에서 잘 작성된 Effect는 Activity에서도 잘 동작할 가능성이 높다.

고 연결해서 이해할 수 있습니다.

공식 문서 역시 Activity 관련 Effect 문제를 일찍 발견하려면 StrictMode를 권장합니다. (React)


16. 중요한 함정: DOM이 남아 있기 때문에 생기는 side effect

여기서 상당히 흥미로운 문제가 있습니다.

Activity가 hidden이 되면 Effects는 cleanup하지만 DOM은 제거하지 않습니다.

그래서 DOM 자체가 side effect를 가지고 있으면 문제가 생길 수 있습니다.

대표적인 예가:

<video>
<audio>
<iframe>

입니다. (React)


17. <video> 예제

다음처럼 단순하게:

function Video() {
  return (
    <video
      src="video.mp4"
      controls
    />
  );
}

라고 합시다.

일반적인 unmount라면:

<video> DOM 제거
→ 재생 종료

됩니다.

하지만 Activity:

<Activity mode="hidden">
  <Video />
</Activity>

이면 DOM 자체는 남아 있습니다.

단지:

display: none;

상태입니다.

그래서 영상/소리가 계속 재생될 수 있습니다. (React)


18. 이런 DOM side effect는 직접 cleanup해야 한다

공식 문서에서는 이런 식으로 해결합니다.

function VideoTab() {
  const ref = useRef(null);

  useLayoutEffect(() => {
    const video = ref.current;

    return () => {
      video.pause();
    };
  }, []);

  return (
    <video
      ref={ref}
      controls
      src="..."
    />
  );
}

Activity가 hidden될 때 Effect cleanup이 실행되므로:

video.pause();

도 실행됩니다. (React)


19. 왜 useEffect가 아니라 useLayoutEffect일까?

공식 문서에서 이 부분을 굳이 useLayoutEffect로 작성한 이유가 있습니다.

영상 일시 정지는 UI가 실제로 숨겨지는 시점과 밀접한 작업입니다.

일반 useEffect cleanup은 Suspense나 View Transition 같은 상황에서 다소 지연될 수 있습니다.

그래서:

UI가 숨겨질 때 즉시 DOM 상태를 맞춰야 함

같은 작업에는 useLayoutEffect cleanup이 더 적절할 수 있습니다. (React)

보충 설명

그렇다고 Activity를 쓸 때 모든 cleanup을 useLayoutEffect로 바꾸는 것은 아닙니다.

네트워크 연결 같은 일반적인 외부 시스템 동기화라면 여전히:

useEffect(...)

가 적절합니다.

DOM의 시각적 상태와 동기적으로 맞춰야 하는 작업에서 useLayoutEffect가 필요할 수 있다는 뜻입니다.


20. hidden Activity는 개념적으로 unmounted라고 생각해라

공식 문서에서 아주 중요한 사고방식을 제공합니다.

hidden Activity는 DOM/state를 물리적으로 보존하지만, 컴포넌트의 side effect 관점에서는:

unmounted라고 생각하는 것이 좋다.

즉 hidden일 때:

Effect가 돌고 있을 것이다 ❌

subscription이 살아 있을 것이다 ❌

DOM은 존재할 수 있다 ✅

state는 보존된다 ✅

입니다. (React)

이걸 기억하면 많은 혼란이 사라집니다.


21. Effects가 실행되지 않아서 문제가 생긴다면?

다음처럼 작성했다고 합시다.

useEffect(() => {
  someGlobalState.isOpen = true;
}, []);

그리고 다른 코드가:

이 Effect가 mount되면 항상 true가 된다

고 가정하고 있다면 Activity hidden에서는 깨질 수 있습니다.

왜냐하면 hidden 상태에서는 Effect가 cleanup되기 때문입니다.

공식 문서는 side effect의 정리 로직은 반드시 Effect의 cleanup 함수에 넣으라고 권장합니다. (React)

즉:

useEffect(() => {
  subscribe();

  return () => {
    unsubscribe();
  };
}, []);

처럼 lifecycle을 대칭적으로 만들어야 합니다.


22. Activity와 단순 조건부 렌더링의 선택 기준

이제 실전 판단 기준을 만들어봅시다.

그냥 unmount하는 게 맞는 경우

{show && <Modal />}

다시 보여줄 가능성이 낮거나:

state를 보존할 이유 없음
DOM을 보존할 이유 없음

이라면 조건부 렌더링이 가장 단순합니다.


Activity가 좋은 경우

<Activity mode={show ? 'visible' : 'hidden'}>

다시 보여줄 가능성이 높고:

내부 state 보존하고 싶음
DOM state 보존하고 싶음
미리 렌더링하고 싶음
다만 숨겨졌을 때 Effect는 멈추고 싶음

이라면 Activity가 적합합니다.

대표적으로:

탭
사이드바
다단계 폼
일시적으로 숨기는 패널
뒤로 돌아갈 가능성이 높은 화면

같은 UI가 떠오릅니다.


23. Activity와 CSS 숨김의 선택 기준

CSS:

<div hidden={!show}>

또는:

style={{ display: 'none' }}

는:

state 보존
DOM 보존
Effect 계속 실행

입니다.

Activity는:

state 보존
DOM 보존
Effect cleanup

입니다.

따라서:

숨겨져 있는 동안에도 Effect가 계속 살아 있어야 하는가?

가 중요한 선택 기준이 됩니다.


24. Activity와 Suspense 비교

두 컴포넌트 이름이 비슷한 “경계” 역할을 하기 때문에 구분해봅시다.

<Suspense>

는:

자식이 아직 준비되지 않았을 때 어떻게 할 것인가?

입니다.

<Activity>

는:

이미 존재하는 UI를 현재 보여줄 것인가 숨길 것인가?

입니다.

그리고 둘을 조합하면:

hidden Activity
→ 미래 UI 미리 준비

Suspense
→ 그 준비 과정에서 async resource 관리

같은 패턴을 만들 수 있습니다.


25. 초기 렌더부터 hidden이어도 렌더링한다

이것도 중요한 포인트입니다.

<Activity mode="hidden">
  <SlowComponent />
</Activity>

라고 처음부터 hidden이면:

안 보이니까 아예 렌더링하지 않겠지?

라고 생각할 수 있습니다.

아닙니다.

React는 자식을 낮은 우선순위로 렌더링할 수 있습니다. 다만 Effects는 mount하지 않습니다. (React)

그래서 이것이 pre-rendering 기능으로 사용될 수 있습니다.


26. Activity와 hydration

공식 문서에서는 Activity가 page load 성능에도 도움을 줄 수 있다고 설명합니다.

React의 서버 렌더링에는 Selective Hydration이라는 최적화가 있습니다.

전체 HTML을 한꺼번에 hydration하지 않고, Suspense boundary 등을 기준으로 필요한 부분부터 interactive하게 만들 수 있습니다. (React)

Activity는 이런 React의 백그라운드 렌더링/준비 모델과 함께 동작해서:

사용자에게 필요한 foreground UI 먼저
↓
숨겨진 UI는 낮은 우선순위로 준비

같은 전략을 가능하게 합니다.

이 부분은 SSR/framework를 사용할 때 특히 의미가 큽니다.


27. ViewTransition과도 연동된다

현재 공식 문서에는 Activity가 ViewTransition 안에 있을 때의 동작도 나옵니다.

Activity가 startTransition에 의해:

hidden → visible

이 되면 ViewTransition의 enter animation,

visible → hidden

이 되면 exit animation을 활성화할 수 있습니다. (React)

지금은 이걸 깊게 공부할 필요는 없습니다.

ViewTransition 페이지에서 다시 자세히 보면 됩니다.


28. 조금 특이한 caveat: 텍스트만 반환하는 경우

다음 컴포넌트를 봅시다.

function Greeting() {
  return 'Hello';
}

그리고:

<Activity mode="hidden">
  <Greeting />
</Activity>

라고 하면 hidden text를 위한 DOM element가 따로 존재하지 않습니다.

즉 display: none을 적용할 실제 요소가 없습니다.

그래서 hidden 상태에서는 해당 text가 DOM에 출력되지 않습니다. visible이 되면 다시 출력됩니다. (React)

일반적인 앱에서 자주 마주칠 문제는 아니지만 Activity가 실제 DOM을 어떻게 숨기는지 이해하는 데는 좋은 예외입니다.


29. Activity의 핵심 모델

이번 페이지는 이 그림 하나로 정리할 수 있습니다.

                visible
                   │
                   │ mode="hidden"
                   ▼
              ┌─────────┐
              │ Activity│
              │ hidden  │
              └─────────┘

React state   → 보존
DOM           → 보존 + display:none
Effects       → cleanup
Rendering     → 계속 가능하지만 낮은 우선순위


                   │
                   │ mode="visible"
                   ▼

React state   → 이전 값 복원
DOM           → 다시 표시
Effects       → 다시 setup

30. 실무에서 판단하는 질문

Activity를 써야 할지 고민되면 다음 질문 하나가 가장 좋습니다.

“이 UI는 잠깐 사라졌다가 곧 다시 돌아올 가능성이 높고, 돌아왔을 때 사용자가 이전 상태 그대로 보기를 원하는가?”

그렇다면 Activity를 고려할 가치가 있습니다.

예를 들어:

탭 A에서 입력하다
탭 B로 이동
다시 탭 A

에서 이전 입력 상태가 남아 있으면 자연스럽다면 Activity가 잘 맞습니다.

반대로:

로그인 완료
로그인 폼은 다시 쓸 필요 없음

같은 경우라면 그냥 unmount하는 것이 자연스럽습니다.


핵심 정리

<Activity>는 UI를 숨기면서 state와 DOM을 보존하고, hidden 상태에서는 Effects를 cleanup하는 React 컴포넌트입니다. (React)

가장 중요한 차이는 이것입니다.

Unmount
→ state ❌ DOM ❌ Effects cleanup

CSS display:none
→ state ✅ DOM ✅ Effects 계속 실행

Activity hidden
→ state ✅ DOM ✅ Effects cleanup

그리고 hidden Activity의 자식은 완전히 사라지는 것이 아니라 낮은 우선순위로 계속 렌더링될 수 있기 때문에 pre-rendering에도 사용할 수 있습니다. (React)

다만 useEffect 안에서 시작하는 데이터 fetching은 hidden 상태에서 실행되지 않으므로 Activity를 통한 pre-rendering의 이점을 얻으려면 Suspense와 통합된 데이터 소스가 필요합니다. (React)

또 <video>, <audio>, <iframe>처럼 DOM 자체가 side effect를 갖는 경우에는 DOM이 남아 있다는 점을 특히 주의해야 합니다. (React)

이해도 테스트

정답은 바로 알려주지 않겠습니다.

  1. 아래 두 코드의 가장 중요한 차이는 무엇일까요?
{show && <Sidebar />}
<Activity mode={show ? 'visible' : 'hidden'}>
  <Sidebar />
</Activity>
  1. Activity가 hidden일 때 다음 세 가지는 각각 어떻게 되나요?
React state
DOM
Effect
  1. <textarea>가 uncontrolled일 때 Activity를 사용하면 탭을 이동했다 돌아와도 입력값이 유지될 수 있는 이유는 무엇인가요?
  2. 다음 코드의 fetchPosts()는 Activity가 hidden인 동안 실행될까요? 이유도 설명해보세요.
useEffect(() => {
  fetchPosts();
}, []);
  1. <video>를 Activity로 숨겼는데 소리가 계속 재생될 수 있는 이유는 무엇이며, 어떤 방식으로 해결해야 할까요?

'리액트 > 레퍼런스 (react 19ver)' 카테고리의 다른 글

cache  (0) 2026.09.15
addTransitionType  (0) 2026.09.15
act  (0) 2026.09.15
<ViewTransition>  (0) 2026.09.15
<Suspense>  (1) 2026.09.14
'리액트/레퍼런스 (react 19ver)' 카테고리의 다른 글
  • addTransitionType
  • act
  • <ViewTransition>
  • <Suspense>
배불뚱이
배불뚱이
  • 배불뚱이
    FE 개발자의 개발 기록
    배불뚱이
  • 전체
    오늘
    어제
    • 분류 전체보기 (24)
      • 리액트 (24)
        • 공식문서 강의 (19ver) (17)
        • 레퍼런스 (react 19ver) (7)
        • 레퍼런스 (react-dom 19ver) (0)
  • 공지사항

  • 인기 글

  • 태그

    component
    공식문서
    레퍼런스
    Hook
    react
  • hELLO· Designed By정상우.v4.10.6
배불뚱이
<Activity>
상단으로

티스토리툴바