좋습니다. 이번 페이지의 act는 React 컴포넌트 테스트에서 “업데이트가 다 처리된 뒤 검증하도록” 만드는 테스트 헬퍼입니다.
앱 코드에서 쓰는 API가 아니라 테스트 코드용 API라는 점부터 잡으면 됩니다. 공식 문서의 표현대로 act는 pending React updates를 적용한 후 assertion할 수 있게 도와줍니다. (React)
1. 오늘의 핵심
기본 형태는 이겁니다.
import { act } from 'react';
await act(async () => {
// render
// click
// state update
// async update
});
// 여기서 assertion
expect(...);
핵심은:
사용자 한 번의 상호작용에 의해 발생할 수 있는 React 업데이트들을
act안에서 처리한 뒤, 결과를 검사한다.
입니다.
2. 왜 act가 필요한가?
예를 들어 이런 컴포넌트가 있다고 해봅시다.
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return (
<>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>
Increment
</button>
</>
);
}
테스트에서 버튼을 클릭했다고 바로:
button.click();
expect(label.textContent).toBe('1');
이라고 검사하면 문제가 생길 수 있습니다.
왜냐하면 React 업데이트에는:
event
↓
setState
↓
render 예약
↓
render
↓
commit
↓
Effect 실행
같은 과정이 있기 때문입니다.
테스트 코드가 React보다 먼저 다음 줄로 넘어갈 수도 있습니다.
그래서 우리가 원하는 건:
상호작용
↓
React 관련 update 전부 처리
↓
DOM/Effect 반영
↓
그 다음 assertion
입니다.
act가 이 경계를 만들어줍니다. (React)
3. 이름이 왜 act인가?
공식 문서는 act라는 이름이 테스트에서 흔히 사용하는:
Arrange
Act
Assert
패턴의 Act에서 왔다고 설명합니다. (React)
예를 들어:
it('increments count', async () => {
// Arrange
// 테스트 환경 준비
// Act
await act(async () => {
button.click();
});
// Assert
expect(label.textContent).toBe('1');
});
하지만 여기서 React의 act는 단순히 이름만 “Act”인 게 아니라:
React가 해당 상호작용으로 발생한 업데이트를 안정적으로 끝낼 수 있는 경계
를 제공합니다.
4. 렌더링도 act 안에서 해야 한다
공식 문서의 첫 번째 대표적인 사용 예입니다.
import { act } from 'react';
import { createRoot } from 'react-dom/client';
it('renders a counter', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
await act(async () => {
createRoot(container).render(<Counter />);
});
expect(
container.querySelector('p').textContent
).toBe('0');
expect(document.title).toBe('Count: 0');
});
왜 최초 렌더까지 act가 필요할까요?
렌더만 일어나는 게 아니기 때문입니다.
root.render()
↓
render
↓
commit
↓
useEffect
가 있을 수 있습니다.
act가 없다면:
root.render(<Counter />);
expect(document.title).toBe('Count: 0');
시점에 Effect가 아직 실행되지 않았을 가능성을 테스트 코드가 직접 고려해야 합니다.
act를 사용하면 React에게:
"이 렌더와 관련된 작업을 처리한 뒤 내가 다음 assertion으로 넘어가겠다."
라고 말하는 셈입니다. (React)
5. 이벤트도 act 안에서 처리한다
공식 문서의 두 번째 대표적인 용도입니다.
await act(async () => {
button.dispatchEvent(
new MouseEvent('click', { bubbles: true })
);
});
expect(label.textContent).toBe('1');
expect(document.title).toBe('Count: 1');
여기서 click이 발생하면:
click
↓
handleClick
↓
setCount
↓
render
↓
commit
↓
useEffect
가 이어집니다.
act는 이 사용자 상호작용 단위에 관련된 업데이트를 처리한 후 밖으로 빠져나옵니다. (React)
6. “React 업데이트를 flush한다”는 게 무슨 뜻인가?
act 설명에서 자주 나오는 단어가 flush입니다.
대략 다음 의미라고 보면 됩니다.
예약만 되어 있는 React 작업
↓
실제로 처리
↓
DOM에 반영
예를 들어:
setCount(1);
했다고 항상 바로 DOM이:
<p>1</p>
이 되는 것은 아닙니다.
React가 update를 schedule하고 처리하는 과정이 있습니다.
act는 내부적으로 act queue를 사용하고, 그 안에서 발생한 업데이트를 처리해서 테스트가 중간 상태가 아니라 안정된 결과를 보도록 돕습니다. (React)
7. 왜 항상 await act(...)를 쓰라고 하나?
공식 문서가 꽤 강하게 권장하는 부분입니다.
이렇게 동기 형태도 사용할 수 있는 경우가 있습니다.
act(() => {
root.render(<App />);
});
하지만 공식 문서는 앞으로는 다음 형태를 사용하라고 합니다.
await act(async () => {
root.render(<App />);
});
React가 내부적으로 업데이트를 스케줄링하는 방식 때문에 어떤 경우에 동기 act가 안전한지 예측하기 어렵기 때문입니다.
그리고 공식 문서는 sync act를 미래에 deprecated하고 제거할 예정이라고 명시합니다. (React)
따라서 실무 mental model은 간단합니다.
act를 쓴다
→ 기본적으로 await act(async () => ...)
로 생각하세요.
8. 왜 async가 중요한가?
예를 들어 interaction이 Promise를 건넌다고 해봅시다.
function Profile() {
const [name, setName] = useState(null);
async function handleClick() {
const user = await fetchUser();
setName(user.name);
}
return (
<>
<button onClick={handleClick}>Load</button>
<p>{name}</p>
</>
);
}
사용자가 클릭하면:
click
↓
fetchUser()
↓
await
↓
Promise resolved
↓
setName()
↓
render
즉 async boundary를 건넙니다.
await act(async () => ...)는 이런 경우 비동기 경계를 넘어서 예약되는 React update까지 처리할 수 있게 설계되어 있습니다. (React)
9. act 자체가 데이터를 기다려주는 마법은 아니다
여기서는 한 가지를 구분하면 좋습니다.
act는:
React update를 안정적으로 처리하게 해주는 도구
입니다.
무조건 모든 네트워크 요청을 알아서 mock하거나 해결해주는 API는 아닙니다.
예를 들어 테스트에서는 여전히:
API mock
Promise resolution
timer 제어
같은 테스트 환경 설정이 필요할 수 있습니다.
그 결과 발생하는 React update를 처리하는 경계가 act인 겁니다.
10. act callback 안에서 발생한 update는 queue로 모인다
공식 문서에서 설명하는 핵심 내부 모델입니다.
await act(async () => {
// A update
// B update
// C update
});
여기서 발생하는 React update들은 내부 act queue로 들어가고 React가 해당 작업을 처리합니다. async boundary를 넘어 발생한 예약 작업도 처리할 수 있습니다. (React)
개념적으로:
act 시작
│
├─ update A
├─ update B
└─ async → update C
│
▼
act queue
│
▼
React가 처리
│
▼
act 종료
│
▼
assert
라고 이해하면 충분합니다.
11. act는 값을 반환하는 API가 아니다
공식 문서에 따르면 act는 의미 있는 반환값을 제공하지 않습니다. (React)
즉 이런 사고방식이 아닙니다.
const result = await act(...);
해서 결과를 꺼내는 도구가 아닙니다.
목적은:
React update가 처리되는 시점을 제어
하는 것입니다.
12. 직접 DOM event를 dispatch할 때 주의점
공식 문서에는 이런 예제가 나옵니다.
button.dispatchEvent(
new MouseEvent('click', {
bubbles: true,
})
);
여기서 bubbles: true가 중요합니다.
React의 이벤트 시스템과 정상적으로 연결하려면 이벤트가 DOM에서 bubbling되어야 하기 때문입니다.
또 공식 문서는 직접 DOM event를 dispatch하려면 테스트 container를 실제 document에 붙여야 한다고 설명합니다. (React)
그래서:
const container = document.createElement('div');
document.body.appendChild(container);
가 나옵니다.
13. 그런데 실무에서 act를 직접 자주 써야 하나?
대부분의 경우 아닙니다.
공식 문서 역시 act()를 직접 쓰면 꽤 장황하기 때문에 React Testing Library 같은 라이브러리를 사용하는 것을 권장합니다. 이런 라이브러리의 helper들은 일반적으로 내부에서 act로 감싸져 있습니다. (React)
예를 들어 React Testing Library라면 보통:
render(<Counter />);
await user.click(
screen.getByRole('button', { name: 'Increment' })
);
expect(
screen.getByText('1')
).toBeInTheDocument();
처럼 작성합니다.
직접:
await act(...)
를 매번 넣지 않아도 됩니다.
보충 설명
이건 좋은 테스트 철학과도 연결됩니다.
저수준으로:
createRoot
querySelector
dispatchEvent
act
를 직접 다루기보다,
사용자가 실제 앱을 사용하는 방식에 더 가까운:
render
getByRole
user.click
같은 고수준 API를 사용하는 게 테스트 유지보수에 유리한 경우가 많습니다.
14. 그러면 언제 act를 직접 알 필요가 있는가?
React Testing Library를 쓰더라도 act를 이해해야 하는 이유가 있습니다.
테스트하다 보면 이런 경고를 보게 됩니다.
An update to SomeComponent inside a test
was not wrapped in act(...)
이 메시지의 의미는 대략:
"테스트 중 React state update가 발생했는데, 그 update가 끝난 뒤 assertion한다는 보장이 없다."
입니다.
즉 단순히:
act 좀 넣으라는 귀찮은 경고
가 아니라,
테스트가 React update lifecycle을 제대로 기다리지 않고 있을 가능성
을 알려주는 신호입니다.
15. 흔한 act 경고 상황
예를 들어:
function User() {
const [name, setName] = useState('');
useEffect(() => {
fetchUser().then(user => {
setName(user.name);
});
}, []);
return <p>{name}</p>;
}
테스트:
render(<User />);
expect(screen.getByText('Alice')).toBeInTheDocument();
라고 하면 비동기 update가 아직 끝나지 않았을 수 있습니다.
이럴 때 React Testing Library에서는 직접 act를 넣기보다:
expect(
await screen.findByText('Alice')
).toBeInTheDocument();
처럼 비동기 UI 결과를 기다리는 API를 사용하는 경우가 더 자연스럽습니다.
보충 설명
act 경고를 볼 때 무조건:
act(() => {
...
});
를 억지로 감싸는 것보다:
“내 테스트가 어떤 사용자 상호작용 또는 비동기 UI 변화를 기다리지 않고 있는가?”
를 먼저 생각하는 게 좋습니다.
16. Testing Library와 act의 관계
관계를 이렇게 이해하면 깔끔합니다.
React act
= 저수준 primitive
React Testing Library
= act를 활용해 만든 고수준 testing helper
즉 Testing Library를 사용한다고 act 개념이 사라지는 게 아닙니다.
내부에서 대신 처리해주는 겁니다.
공식 문서도 Testing Library의 helper들이 act로 감싸져 있기 때문에 boilerplate를 줄일 수 있다고 안내합니다. (React)
17. React 19에서는 import 위치도 바뀌었다
React 19 기준으로 중요한 변경점입니다.
현재는:
import { act } from 'react';
를 사용합니다.
예전 코드에서는:
import { act } from 'react-dom/test-utils';
를 볼 수 있는데, React 19에서는 이 방식이 deprecated되었습니다. React 19 업그레이드 가이드도 act를 react에서 import하도록 변경하라고 안내합니다. (React)
즉 현재 기준:
// ✅
import { act } from 'react';
로 기억하세요.
18. 테스트 환경 설정
직접 act를 사용하는 테스트 환경이라면:
global.IS_REACT_ACT_ENVIRONMENT = true;
가 필요합니다. 그렇지 않으면:
The current testing environment is not configured
to support act(...)
같은 경고를 받을 수 있습니다. (React)
React Testing Library 같은 테스트 환경은 보통 이 설정을 대신 처리해줍니다.
따라서 직접 설정할 일은 주로:
직접 React 테스트 환경 구축
custom test runner 사용
React 저수준 테스트 작성
같은 상황에서 생깁니다.
19. act를 React의 Render/Commit 흐름과 연결해보자
우리가 예전에 배운:
Trigger
↓
Render
↓
Commit
을 기억해봅시다.
테스트가:
setState();
expect(...);
처럼 되어 있으면 테스트 코드 관점에서는:
Trigger
↓
??? React가 처리 중
↓
Assert가 먼저 실행될 수도 있음
이라는 문제가 생깁니다.
act를 사용하면:
act {
Trigger
↓
Render
↓
Commit
↓
Effects 등 관련 update
}
↓
Assert
라는 형태로 테스트를 작성할 수 있습니다.
따라서 act는 새로운 React lifecycle 개념이 아니라, 기존 React lifecycle을 테스트 코드가 기다리도록 만드는 도구입니다.
20. act와 batching은 같은 개념인가?
비슷해 보일 수 있지만 구분해야 합니다.
act 안의 업데이트가 모여 처리될 수 있지만:
act의 주목적은 성능을 위한 batching이 아닙니다.
목적은 테스트에서 관찰 가능한 React 결과가 안정된 시점까지 update를 처리하는 것입니다.
즉:
batching
→ update 처리 효율
act
→ 테스트 synchronization
이라고 생각하는 게 좋습니다.
21. act와 flushSync도 다르다
flushSync를 이미 보았거나 앞으로 볼 수 있습니다.
둘 다 “update를 처리한다”는 느낌 때문에 혼동하기 쉽습니다.
act
await act(async () => {
...
});
테스트 전용입니다.
목적:
interaction에 관련된 React 작업을 처리하고
assertion 가능한 상태로 만들기
flushSync
flushSync(() => {
setState(...);
});
일반 앱 코드에서도 사용하는 탈출구이며, 특정 업데이트를 동기적으로 DOM에 flush하도록 강제합니다.
따라서 둘은 용도가 완전히 다릅니다.
22. “사용자 상호작용 단위”로 감싼다는 관점이 중요하다
공식 문서는 rendering, user events, data fetching 같은 작업을 UI interaction의 unit으로 봅니다. (React)
그래서 이런 코드보다:
await act(async () => {
button.dispatchEvent(...);
});
await act(async () => {
// 아무 관련 없는 세부 작업
});
await act(async () => {
// 또 다른 내부 구현 세부 작업
});
사용자가 하는 하나의 행동과 그 결과를 하나의 단위로 생각하는 것이 좋습니다.
await act(async () => {
// 사용자 행동
});
// 사용자가 보게 되는 결과 검증
테스트 역시 구현 세부사항보다는 사용자의 행동 → UI 결과를 중심으로 작성하는 것이 좋습니다.
23. 흔한 실수 1: await를 빼먹는다
act(async () => {
...
});
expect(...);
보다는:
await act(async () => {
...
});
expect(...);
로 작성합니다.
공식 문서도 async act + await 사용을 권장합니다. (React)
24. 흔한 실수 2: 모든 assertion까지 act 안에 넣는다
이렇게 생각할 수 있습니다.
await act(async () => {
button.click();
expect(...);
});
하지만 mental model상 보통은:
await act(async () => {
// update를 발생시키는 행동
});
// 처리된 결과 검증
expect(...);
로 분리해서 이해하는 게 좋습니다.
act의 역할은 assertion을 수행하는 게 아니라 assertion 가능한 상태까지 React를 진행시키는 것이기 때문입니다.
25. 흔한 실수 3: 경고를 없애기 위해 무조건 감싼다
가장 조심해야 할 패턴입니다.
테스트에서 act warning이 나오면:
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
});
같은 식으로 무작정 해결하려고 할 수 있습니다.
하지만 먼저:
어떤 state update가 늦게 발생하는가?
테스트가 어떤 UI 변화를 기다려야 하는가?
사용자 interaction helper를 제대로 await했는가?
를 확인해야 합니다.
특히 Testing Library를 사용하고 있다면 act를 직접 추가하는 것보다:
await user.click(...)
await screen.findBy...
await waitFor(...)
같은 테스트 의도에 맞는 helper가 더 적절한 경우가 많습니다.
26. 이번 페이지에서 직접 코드를 많이 외울 필요는 없다
실무에서 React Testing Library를 사용한다면 여러분이 실제로 작성하는 코드는 대부분:
render(<App />);
await user.click(button);
expect(...);
일 가능성이 높습니다.
따라서 act에서 중요한 건 문법보다 왜 존재하는지를 이해하는 것입니다.
React 업데이트는 테스트 코드의 한 줄 실행과 항상 1:1로 즉시 완료되는 것이 아니다.
그래서:
테스트에서 사용자 행동으로 발생하는 React 작업들이 끝난 뒤 결과를 관찰해야 한다.
이를 보장하기 위한 primitive가:
act()
입니다.
핵심 정리
act를 한 문장으로 설명하면:
act는 테스트에서 렌더링이나 사용자 상호작용으로 발생한 React 업데이트를 처리한 후 assertion하도록 만드는 테스트 동기화 helper입니다. (React)
현재 권장 형태는:
import { act } from 'react';
await act(async () => {
// render or interaction
});
입니다.
특히 기억해야 할 것은:
render
event
async state update
Effect
↓
act
↓
React가 관련 update 처리
↓
assertion
입니다.
그리고 현재 React에서는 sync act보다 await act(async () => ...)를 사용해야 하며, sync 버전은 미래에 제거될 예정입니다. (React)
또 실무에서는 React Testing Library 같은 도구가 대부분의 act 처리를 대신 해주기 때문에 직접 호출하는 경우는 상대적으로 적습니다. 하지만 act warning을 이해하려면 이 개념을 알아두는 것이 중요합니다. (React)
이해도 테스트
정답은 바로 알려주지 않겠습니다.
setState()직후 바로 assertion하면 테스트가 불안정해질 수 있는 이유는 무엇일까요?act의 가장 중요한 역할을 batching이 아니라 테스트 synchronization이라고 표현하는 이유는 무엇인가요?- 왜 현재 React에서는 다음보다
act(() => {
...
});
다음을 권장할까요?
await act(async () => {
...
});
- React Testing Library를 사용하면
act를 직접 거의 쓰지 않는데도,act를 이해해야 하는 이유는 무엇일까요? act(...)warning이 발생했을 때 무조건 코드를act로 감싸기 전에 먼저 어떤 문제를 의심해봐야 할까요?
'리액트 > 레퍼런스 (react 19ver)' 카테고리의 다른 글
| cache (0) | 2026.09.15 |
|---|---|
| addTransitionType (0) | 2026.09.15 |
| <ViewTransition> (0) | 2026.09.15 |
| <Activity> (0) | 2026.09.14 |
| <Suspense> (1) | 2026.09.14 |