testing-library/react의 act는 언제 써야할까
이미 act로 감싸져 호출되는 경우가 대부분이라 내가 직접 쓸 일이 없다.
TL;DR;
- (과거) act(() => {}) 어쩌구하는 에러만 보면 act()를 감쌌던 과거. 그래도 안되면 async await 붙이고도 안되서 고통받음
testing-library/react
가userEvent
등의 이벤트 함수들이나render()
같은 함수들은act()
로 감싸놔서 내가act()
를 쓸 일은 거의 없다. (👉 코드)
일단 act()로 감싸버렸는데..
5개월 전쯤 지금 회사에 입사했을 때, 내가 맡고 있는 서비스에는 테스트코드가 없어서 요즘에는 환경 설정해놓고 테스트 코드를 하나씩 추가하고 있습니다.
반복하는 실수 중에 하나가 일단 act()
함수로 감싸게 되는 것인데, Common mistake with React Testing Library 라는 글을 읽고 act에 대해 정리해보려고 합니다.
what they don’t know is that
render
andfireEvent
are already wrapped inact
!
사람들은render
와fireEvent
가 이미 act로 감싸져있다는걸 모릅니다!
act()
함수는 어떤 일을 할까?
act()
는 함수를 인자로 받는데, 이 함수를 실행시켜서 가상의 DOM(jsdom)에 적용하는 역할을 합니다. act()
함수가 호출된 뒤, DOM에 반영되었다고 가정하고 그 다음 코드를 쓸 수 있게 되서 React가 브라우저에서 실행될 때와 최대한 비슷한 환경에서 테스트할 수 있습니다.
React에서 제공해주는 act를 그냥 사용하면 써야할 코드가 많아질 수 있어서, testing-library/react에서 act를 랩핑해서 좀 더 편하게 사용해줄 수 있도록 돕는 역할을 합니다.
혹시라도 act의 구현체를 직접 탐험해보고 싶은 분들은 아래 repo들을 보시면 됩니다.
- React (여기서 act가 태어났다)
- testing-library/dom (DOM 설정 함수 configure()를 제공한다)
- react-testing-library (configure()함수를 이용 여러 이벤트를 act()함수로 랩핑하는 한 새로운 함수를 오버라이딩한다)
test('테스트', () => {
act(() => {
/* DOM에 반영하고 싶은 코드들 */
(예. ReactDOM.render(<Counter />, container);)
}); /* `DOM에 반영하고 싶은 코드들`이 DOM에 반영되었다고 가정하고 테스트할 코드들 */
});
여러 복잡한 로직을 빼고 가장 기본적인 로직만 살펴보면 act
는 받은 callback을 실행한 후 thenable
이라는 객체를 return합니다.
그런데 이 thenable
이라는 객체를 어디다 써먹을 수 있는지는 아직 모르겠습니다 🧐 😇
사실 내가 act()를 쓸 필요 없다.
왜냐면 이미 act()
로 감싸져있기 때문입니다.
userEvent.click
같은 이벤트 함수들 jsdom
에 반영되기 위해서 testing-library/dom
의 eventWrapper()
메소드 함수가 호출되게 되는데, testing-library/react
는 eventWrapper()
오버라이딩해서 콜백함수들(예. 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를 사용하면서 의도치 않은 동작이 일어날 수 있으니 켄트님이 사용할 필요 없다고 한 것 같습니다 🧐