testing-library/react의 act는 언제 써야할까

이미 act로 감싸져 호출되는 경우가 대부분이라 내가 직접 쓸 일이 없다.

FlyingSquirrel
5 min readFeb 4, 2022
act

TL;DR;

  • (과거) act(() => {}) 어쩌구하는 에러만 보면 act()를 감쌌던 과거. 그래도 안되면 async await 붙이고도 안되서 고통받음
  • testing-library/reactuserEvent 등의 이벤트 함수들이나 render()같은 함수들은 act()로 감싸놔서 내가 act()를 쓸 일은 거의 없다. (👉 코드)

일단 act()로 감싸버렸는데..

일단 act로 바리바리 감싸고 보자 (출처: giphy, dev.to)

5개월 전쯤 지금 회사에 입사했을 때, 내가 맡고 있는 서비스에는 테스트코드가 없어서 요즘에는 환경 설정해놓고 테스트 코드를 하나씩 추가하고 있습니다.

반복하는 실수 중에 하나가 일단 act() 함수로 감싸게 되는 것인데, Common mistake with React Testing Library 라는 글을 읽고 act에 대해 정리해보려고 합니다.

불필요하게 act()로 감싸면 안된다는 내용 (출처: 갓켄트씨 블로그)
불필요하게 act()로 감싸면 안된다는 내용 (출처: 갓켄트씨 블로그)

what they don’t know is that render and fireEvent are already wrapped in act!
사람들은 renderfireEvent가 이미 act로 감싸져있다는걸 모릅니다!

act()함수는 어떤 일을 할까?

act() 는 함수를 인자로 받는데, 이 함수를 실행시켜서 가상의 DOM(jsdom)에 적용하는 역할을 합니다. act()함수가 호출된 뒤, DOM에 반영되었다고 가정하고 그 다음 코드를 쓸 수 있게 되서 React가 브라우저에서 실행될 때와 최대한 비슷한 환경에서 테스트할 수 있습니다.

React에서 제공해주는 act를 그냥 사용하면 써야할 코드가 많아질 수 있어서, testing-library/react에서 act를 랩핑해서 좀 더 편하게 사용해줄 수 있도록 돕는 역할을 합니다.

혹시라도 act의 구현체를 직접 탐험해보고 싶은 분들은 아래 repo들을 보시면 됩니다.

test('테스트', () => {
act(() => {
/* DOM에 반영하고 싶은 코드들 */
(예. ReactDOM.render(<Counter />, container);)
});
/* `DOM에 반영하고 싶은 코드들`이 DOM에 반영되었다고 가정하고 테스트할 코드들 */
});

여러 복잡한 로직을 빼고 가장 기본적인 로직만 살펴보면 act는 받은 callback을 실행한 후 thenable이라는 객체를 return합니다.

그런데 이 thenable이라는 객체를 어디다 써먹을 수 있는지는 아직 모르겠습니다 🧐 😇

사실 내가 act()를 쓸 필요 없다.

왜냐면 이미 act()로 감싸져있기 때문입니다.

userEvent.click 같은 이벤트 함수들 jsdom에 반영되기 위해서 testing-library/domeventWrapper() 메소드 함수가 호출되게 되는데, testing-library/reacteventWrapper()오버라이딩해서 콜백함수들(예. userEvent.click())가 act()함수로 감싸지도록 해놨습니다.

import { configure as configureDTL } from '@testing-library/dom';
import act from './act-compat';
// DOM 설정을 오버라이딩함
configureDTL({
eventWrapper: cb => { // cb는 userEvent.click() 같은 함수
let result;
act(() => {
result = cb();
});
return result;
},
})

그래서 내가 굳이 act()userEvent를 감싸줄 필요가 없습니다.


act(() => {
userEvent.click(screen.getByText('컴포넌트'));
});

userEvent.click(screen.getByText('컴포넌트'));

render함수도 이미testing-library/react에서 act로 감싸서 DOM에 렌더링을 해주고 있어서 내가 직접 act로 감싸지 않아도 됩니다.

function render(ui, { hydrate = false, ...다른옵션생략 }) {
(...생략)
act(() => {
if (hydrate) {
ReactDOM.hydrate(wrapUiIfNeeded(ui), container)
} else {
ReactDOM.render(wrapUiIfNeeded(ui), container)
}
})
(...생략)
}

act는 중첩되게 호출될 수 있고, act 구현체를 보면 actScopeDepth라는 변수로 중첩을 stack으로 관리하고 있지만, 불필요한 act를 사용하면서 의도치 않은 동작이 일어날 수 있으니 켄트님이 사용할 필요 없다고 한 것 같습니다 🧐

--

--