act๋Š” ํ…Œ์ŠคํŠธ ํ—ฌํผHelper๋กœ, ๋Œ€๊ธฐ ์ค‘์ธ React ์—…๋ฐ์ดํŠธ๋ฅผ ๋ชจ๋‘ ์ ์šฉํ•œ ๋’ค ๋‹จ์–ธAssertํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋„์›€์„ ์ค๋‹ˆ๋‹ค.

await act(async actFn)

์ปดํฌ๋„ŒํŠธ๋ฅผ ๋‹จ์–ธAssertionํ•  ์ˆ˜ ์žˆ๋„๋ก ์ค€๋น„ํ•˜๋ ค๋ฉด await act() ํ˜ธ์ถœ ์•ˆ์— ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•˜๊ณ  ์—…๋ฐ์ดํŠธํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ๊ฐ์‹ธ์„ธ์š”. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ž‘๋™ํ•˜๋Š” ์‹ค์ œ React ๋ฐฉ์‹๊ณผ ๋” ์œ ์‚ฌํ•˜๊ฒŒ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค!

act()๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋‹ค์†Œ ์žฅํ™ฉํ•˜๋‹ค๊ณ  ๋А๊ปด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ˜๋ณต๋˜๋Š” ์ฝ”๋“œ๋ฅผ ์ค„์ด๊ณ  ์‹ถ๋‹ค๋ฉด React Testing Library์ฒ˜๋Ÿผ ๋‚ด๋ถ€์ ์œผ๋กœ act()๋กœ ๊ฐ์‹ผ ํ—ฌํผ๋ฅผ ์ œ๊ณตํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์Šต๋‹ˆ๋‹ค.


๋ ˆํผ๋Ÿฐ์Šค

await act(async actFn)

UI ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ ๋ Œ๋”๋ง, ์‚ฌ์šฉ์ž ์ด๋ฒคํŠธ, ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ ๋“ฑ์€ ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค์™€์˜ ์ƒํ˜ธ์ž‘์šฉ โ€œ๋‹จ์œ„โ€๋กœ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. React๋Š” act()๋ผ๋Š” ํ—ฌํผ๋ฅผ ์ œ๊ณตํ•˜๋Š”๋ฐ ์ด๋Š” ์ด โ€œ๋‹จ์œ„โ€์™€ ๊ด€๋ จ๋œ ๋ชจ๋“  ์—…๋ฐ์ดํŠธ๊ฐ€ DOM์— ์ ์šฉ๋˜๊ธฐ ์ „๊นŒ์ง€ ๋‹จ์–ธ์ด ์‹คํ–‰๋˜์ง€ ์•Š๋„๋ก ๋ณด์žฅํ•ด ์ค๋‹ˆ๋‹ค.

act ๋ผ๋Š” ์ด๋ฆ„์€ Arrange-Act-Assert ํŒจํ„ด์—์„œ ๋”ฐ์˜จ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

it ('renders with button disabled', async () => {
await act(async () => {
root.render(<TestComponent />)
});
expect(container.querySelector('button')).toBeDisabled();
});

์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค!

act๋Š” await์™€ async ํ•จ์ˆ˜์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. ๋™๊ธฐ ๋ฒ„์ „๋„ ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ ๋™์ž‘ํ•˜์ง€๋งŒ React๊ฐ€ ๋‚ด๋ถ€์ ์œผ๋กœ ์—…๋ฐ์ดํŠธ๋ฅผ ์˜ˆ์•ฝํ•˜๋Š” ๋ฐฉ์‹ ๋•Œ๋ฌธ์— ์–ธ์ œ ๋™๊ธฐ ๋ฒ„์ „์„ ์จ๋„ ๋˜๋Š”์ง€ ์˜ˆ์ธกํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.

์•ž์œผ๋กœ ๋™๊ธฐ ๋ฒ„์ „์€ ๋” ์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š์„ ์˜ˆ์ •์ด๋ฉฐ ์ œ๊ฑฐ๋  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.

๋งค๊ฐœ๋ณ€์ˆ˜

  • async actFn: ํ…Œ์ŠคํŠธํ•  ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•˜๊ฑฐ๋‚˜ ์ƒํ˜ธ์ž‘์šฉ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋น„๋™๊ธฐ ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. actFn ๋‚ด๋ถ€์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์—…๋ฐ์ดํŠธ๋Š” ๋‚ด๋ถ€ act ํ์— ์ถ”๊ฐ€๋˜๋ฉฐ ๋ชจ๋‘ ๋ชจ์•„์„œ DOM์— ์ ์šฉ๋ฉ๋‹ˆ๋‹ค. ๋น„๋™๊ธฐ ํ•จ์ˆ˜์ด๊ธฐ ๋•Œ๋ฌธ์— React๋Š” ๋น„๋™๊ธฐ ๊ฒฝ๊ณ„๋ฅผ ๋„˜๋Š” ์ฝ”๋“œ๋„ ์‹คํ–‰ํ•˜๊ณ  ์˜ˆ์•ฝ๋œ ์—…๋ฐ์ดํŠธ๋„ ํ•จ๊ป˜ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๋ฐ˜ํ™˜๊ฐ’

act๋Š” ์•„๋ฌด ๊ฐ’๋„ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉ๋ฒ•

์ปดํฌ๋„ŒํŠธ๋ฅผ ํ…Œ์ŠคํŠธํ•  ๋•Œ act๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ถœ๋ ฅ ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ๋‹จ์–ธ์„ ๋” ์•ˆ์ „ํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ์‹œ๋กœ Counter๋ผ๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๊ณ  ์•„๋ž˜ ์‚ฌ์šฉ ์˜ˆ์‹œ๋Š” ์ด๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prev => prev + 1);
}

useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);

return (
<div>
<p>You clicked {count} times</p>
<button onClick={handleClick}>
Click me
</button>
</div>
)
}

ํ…Œ์ŠคํŠธ์—์„œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•˜๋Š” ๋ฐฉ๋ฒ•

์ปดํฌ๋„ŒํŠธ์˜ ๋ Œ๋”๋ง ๊ฒฐ๊ณผ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋ ค๋ฉด ๋ Œ๋”๋ง ์ฝ”๋“œ๋ฅผ act()๋กœ ๊ฐ์‹ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

import {act} from 'react';
import ReactDOMClient from 'react-dom/client';
import Counter from './Counter';

it('can render and update a counter', async () => {
container = document.createElement('div');
document.body.appendChild(container);

// โœ… ์ปดํฌ๋„ŒํŠธ๋ฅผ act() ์•ˆ์—์„œ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.
await act(() => {
ReactDOMClient.createRoot(container).render(<Counter />);
});

const button = container.querySelector('button');
const label = container.querySelector('p');
expect(label.textContent).toBe('You clicked 0 times');
expect(document.title).toBe('You clicked 0 times');
});

์œ„ ์˜ˆ์‹œ์—์„œ๋Š” ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋งŒ๋“ค๊ณ  ๋ฌธ์„œ์— ์ถ”๊ฐ€ํ•œ ๋’ค Counter ์ปดํฌ๋„ŒํŠธ๋ฅผ act() ์•ˆ์—์„œ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง๋˜๊ณ  ํšจ๊ณผ๊ฐ€ ์ ์šฉ๋œ ํ›„์— ๋‹จ์–ธ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

act๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ชจ๋“  ์—…๋ฐ์ดํŠธ๊ฐ€ ์ ์šฉ๋œ ๋’ค ๋‹จ์–ธ์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ์—์„œ ์ด๋ฒคํŠธ ๋””์ŠคํŒจ์นญํ•˜๋Š” ๋ฐฉ๋ฒ•

์ด๋ฒคํŠธ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋ ค๋ฉด ์ด๋ฒคํŠธ๋ฅผ act()๋กœ ๊ฐ์‹ธ์„ธ์š”.

import {act} from 'react';
import ReactDOMClient from 'react-dom/client';
import Counter from './Counter';

it.only('can render and update a counter', async () => {
const container = document.createElement('div');
document.body.appendChild(container);

await act( async () => {
ReactDOMClient.createRoot(container).render(<Counter />);
});

// โœ… ์ด๋ฒคํŠธ ๋””์ŠคํŒจ์น˜๋ฅผ act() ์•ˆ์—์„œ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
await act(async () => {
button.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});

const button = container.querySelector('button');
const label = container.querySelector('p');
expect(label.textContent).toBe('You clicked 1 times');
expect(document.title).toBe('You clicked 1 times');
});

์œ„ ์˜ˆ์‹œ์—์„œ๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋จผ์ € act๋กœ ๊ฐ์‹ธ ๋ Œ๋”๋งํ•˜๊ณ , ์ด๋ฒคํŠธ ๋””์ŠคํŒจ์น˜๋„ act()๋กœ ๊ฐ์Œ‰๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ•ด๋‹น ์ด๋ฒคํŠธ๋กœ ์ธํ•œ ๋ชจ๋“  ์—…๋ฐ์ดํŠธ๊ฐ€ ์ ์šฉ๋œ ๋’ค ๋‹จ์–ธ์ด ์ˆ˜ํ–‰๋ฉ๋‹ˆ๋‹ค.

์ฃผ์˜ํ•˜์„ธ์š”!

DOM ์ด๋ฒคํŠธ๋ฅผ ๋””์ŠคํŒจ์น˜ํ•  ๋•Œ๋Š” DOM ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ๋ฌธ์„œ์— ์ถ”๊ฐ€๋˜์–ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋ณต๋˜๋Š” ์„ค์ • ์ฝ”๋“œ๋ฅผ ์ค„์ด๊ณ  ์‹ถ๋‹ค๋ฉด React Testing Library๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๋„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”.

๋ฌธ์ œ ํ•ด๊ฒฐ

โ€œThe current testing environment is not configured to support act(โ€ฆ)โ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ

act๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ global.IS_REACT_ACT_ENVIRONMENT=true๋ฅผ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ์„ค์ •์€ act๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ํ™˜๊ฒฝ์—์„œ๋งŒ ์‚ฌ์šฉ๋˜๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

์ด ์ „์—ญ ์„ค์ •์ด ์—†์œผ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

Console
Warning: The current testing environment is not configured to support act(โ€ฆ)

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด React ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ์ „์—ญ ์„ค์ • ํŒŒ์ผ์— ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”.

global.IS_REACT_ACT_ENVIRONMENT=true

์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค!

React Testing Library๊ฐ™์€ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ๋Š” IS_REACT_ACT_ENVIRONMENT๊ฐ€ ์ด๋ฏธ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.