Activity는 React 19.2의 컴포넌트로, UI를 언마운트하지 않고 display: none으로 숨기면서 state와 DOM(Document Object Model)을 보존한다. 숨기는 동안 Effect는 정리(cleanup)된다. 예전 이름은 Offscreen이다.
<Activity mode={isVisible ? 'visible' : 'hidden'}>
<Sidebar />
</Activity>Next.js 16에서의 쓰임
Cache Components(cacheComponents: true)를 켜면, 클라이언트 이동(<Link>, router.push) 때 이전 페이지를 언마운트하지 않고 Activity로 숨긴다. 뒤로 가면 입력하던 폼, 스크롤, 펼친 details가 그대로 돌아온다. 문서 기준으로 최근 3개 라우트까지 보존하고, 그보다 오래된 것은 지운다. 새로고침 같은 하드 내비게이션(Hard Navigation)은 평소처럼 새로 마운트한다.
함정: 같은 id가 여러 개 생긴다
숨겨진 이전 페이지의 DOM이 문서에 남아 있으니, 페이지 안에서 렌더하는 같은 id가 동시에 여러 개 존재할 수 있다. 레이아웃은 공유되므로 하나뿐이다.
| 사용처 | 동작 | 위험 |
|---|---|---|
getElementById·querySelector('#x') | 첫 번째 매칭만 반환 | 숨겨진 이전 페이지 요소를 잡을 수 있음 |
CSS(Cascading Style Sheets) #x | 전부에 적용 | 대체로 무해 |
href="#x" 앵커 | 첫 번째로 스크롤 | 엉뚱한 위치 |
DOM 순서는 보장되지 않는다. 숨겨진 요소를 잡으면 렌더 박스가 없어서 scrollIntoView가 조용히 아무 일도 하지 않는다. 전역 id 조회 대신 ref나 현재 컴포넌트의 컨테이너 기준으로 찾는다. React의 useId는 인스턴스마다 다른 id를 준다.
무엇을 보존할지 고르기
보존이 늘 좋은 것은 아니다. 다시 열 때 닫혀 있어야 하는 드롭다운처럼 숨겨질 때 초기화할 상태는 정리 함수에서 되돌린다. 숨기기 직전 낡은 화면이 한 프레임 보이지 않게 하려면 useLayoutEffect의 정리 함수를 쓴다. 다시 마운트하지 않으니 마운트 시점 초기화 Effect가 다시 돌지 않는다는 점도 기억한다.
예전에는 "라우트가 바뀌면 언마운트되니 로컬 state가 초기화된다"에 기대던 코드가 많다. 장바구니에 담은 뒤 열린 시트, 확인 다이얼로그처럼 일시적인 상호작용이 그렇다. Cache Components를 켜면 이런 UI가 열린 채로 돌아와 E2E(End-to-End) 테스트가 간헐적으로 막히는 식으로 드러난다. 공식 문서가 권하는 처리는 다음과 같다.
- 숨겨질 때 닫기:
useLayoutEffect(() => () => setOpen(false), []). 다시 보일 때 닫는 것보다 낫다. 보이는 순간 한 프레임이라도 열린 화면이 나오지 않는다 - URL에서 파생하기: 열림 여부를
?edit=true같은 검색 파라미터로 둔다. 다른 페이지에 갔다 오면 파라미터가 없으니 자연히 닫히고, 열 때마다 값이 바뀌어 초기화 Effect도 다시 돈다 - 링크를 누를 때 닫기:
<Link onNavigate>콜백에서 바로 닫는다 <video>·<audio>·<iframe>처럼 DOM 자체에 부수 효과가 있는 요소는 숨겨져도 재생이 이어질 수 있으니 정리 함수에서 멈춘다- 숨긴 동안에도 새 props가 오면 낮은 우선순위로 다시 렌더된다. Effect만 정리될 뿐 렌더는 계속된다
- Effect는 숨김 → 표시 때마다 다시 실행된다. 첫 마운트와 재노출을 구분하려면 정리되지 않는 ref에 "이미 마운트됨"을 기록한다
테스트
숨겨진 페이지도 display: none으로 문서에 남으니, E2E 도구에서 같은 요소가 둘 잡히거나 숨겨진 요소를 기다리다 시간 초과가 난다. Playwright라면 접근성 트리 기준이라 숨겨진 요소를 거르는 getByRole·getByLabel을 쓰고, CSS 선택자를 써야 하면 .filter({ visible: true })로 거른다. 점진적 이전 중이라 보존이 곤란하면 useRouter().bfcacheId를 <Fragment key={bfcacheId}>로 감싼 서브트리의 key로 쓴다. push·replace 이동(<Link> 클릭 포함)에서는 새로 마운트되고, 뒤로·앞으로 가기에서는 상태가 복원된다. 문서는 이를 주로 이전용 도구로 보고, 새 코드에는 위의 패턴별 초기화를 권한다(Next.js 16.3 기준).
관련: 부분 사전 렌더링(PPR), 재조정(Reconciliation)(state가 유지되는 조건), Next.js 16 변경점.
출처: Next.js - Preserving UI state across navigations · Next.js - useRouter: bfcacheId · React - Activity