- 에러 발생 환경: vue3, vue/test-utils, jest
- 에러 상황: 직접 DOM 접근 시 요소가 없다는 에러 발생
- Vue 컴포넌트를 테스트할 때 DOM에 직접 접근하는 경우가 있다. Vue는 보통
ref
로 DOM에 접근할 수 있으나,document.querySelector
나document.getElementById
도 사용이 가능하다. - 이런 경우 Jest와 vue/test-utils 환경에서는 컴포넌트가 jsdom의 가상 DOM에만 렌더링되므로 실제
document.body
에 요소가 없어서 에러가 발생한다. - 이번 시간에는 에러 발생 원인과
attachTo
옵션을 비롯한 추가 해결 방법을 간단히 살펴보고자 한다.
1. DOM 접근 에러의 근본 원인
(1) jsdom 환경과 Virtual DOM
- 테스트 환경에서 주로 사용하는 jsdom은 실제 브라우저의 DOM을 모방한다.
- 빠른 테스트 실행과 독립적인 환경 구성이 가능하나, 실제 브라우저처럼 모든 DOM API와 이벤트를 지원하지 않는다.
- vue/test-utils의 기본 마운트 방식은 컴포넌트를 jsdom의 가상 DOM에만 렌더링하므로 실제
document.body
에 포함되지 않는다.
(2) 직접 DOM 접근의 한계
- 컴포넌트 내부에서
ref
를 사용하면 Vue가 제공하는 반응형 시스템과 컴포넌트 라이프사이클에 따라 안전하게 DOM에 접근할 수 있다. - 하지만
document.getElementById
나document.querySelector
로 접근하면 jsdom의 한계로 인해 해당 요소가 없어서 에러가 발생한다. - 예를 들어, 아래 코드는 컴포넌트가 jsdom의 가상 DOM에만 존재하여 요소를 찾지 못하고 실패한다.
import { mount } from '@vue/test-utils'
import MyComponent from '@/components/MyComponent.vue'
test('getElementById로 요소에 접근하기', () => {
// MyComponent 내부에서 document.getElementById()로 요소를 찾으려고 할 때
const element = document.getElementById('my-element')
expect(element).not.toBeNull() // jsdom에 실제 요소가 없으므로 에러 발생
})
2. 해결 방법: attachTo 옵션 활용하기
- vue/test-utils는 mount 함수의 옵션으로
attachTo
를 제공한다. (공식문서) - 이 옵션을 사용하면 컴포넌트를 실제 DOM, 보통
document.body
에 마운트할 수 있다. - 결과적으로 테스트 시 실제 DOM에 요소가 생성되어
document.getElementById
나document.querySelector
호출이 정상적으로 동작한다.
import { mount } from '@vue/test-utils'
import MyComponent from '@/components/MyComponent.vue'
test('getElementById로 요소에 접근하기', () => {
// 컴포넌트를 실제 document.body에 마운트하여 DOM 요소를 생성한다.
const wrapper = mount(MyComponent, {
attachTo: document.body
})
// 컴포넌트 내부의 특정 요소를 document.getElementById로 찾아 테스트 진행
const element = document.getElementById('my-element')
expect(element).not.toBeNull()
expect(element.textContent).toBe('예상되는 텍스트')
// 테스트 후 wrapper.unmount()를 호출해 메모리 누수를 방지한다.
wrapper.unmount()
})
attachTo
옵션을 활용하면 컴포넌트가 실제document.body
에 마운트되어 DOM 접근 시 에러를 피할 수 있다.- 단, 테스트 종료 후 반드시
wrapper.unmount()
로 컴포넌트를 해제해야 한다. 그렇지 않으면 여러 테스트 실행 시 메모리 누수가 발생할 수 있다.
3. 추가 해결 방법
추가로, 상황에 따라 다음 방법들을 고려할 수 있다!
(1) 테스트 전용 DOM 컨테이너 생성
- 테스트 시작 시
document.body
에 임의의 컨테이너 요소를 생성하고, 그 컨테이너에 컴포넌트를 마운트하는 방법이다.attachTo
옵션과 유사하지만, 테스트 환경을 보다 세밀하게 제어할 수 있다. - 한 예로, 모달 컴포넌트는 보통 특정 DOM 계층(예:
#modal-root
)에 마운트되어 동작하도록 설계된다. - 이 경우, 전용 컨테이너를 생성하면 실제 운영 환경과 유사하게 테스트할 수 있다.
import { mount } from '@vue/test-utils'
import ModalComponent from '@/components/ModalComponent.vue'
test('모달 컴포넌트가 커스텀 컨테이너에 정상적으로 마운트되어 동작하는지 확인한다.', () => {
// 테스트 전용 컨테이너 생성
const container = document.createElement('div')
container.id = 'modal-root'
document.body.appendChild(container)
// ModalComponent를 커스텀 컨테이너에 마운트한다.
const wrapper = mount(ModalComponent, {
attachTo: container
})
// 예시: 모달이 활성화되면 'modal-active' 클래스가 추가되어야 한다.
expect(wrapper.find('.modal-active').exists()).toBe(true)
// 모달 내부에 특정 텍스트가 렌더링되는지 확인
expect(wrapper.text()).toContain('모달 내용')
// 테스트 종료 후 정리
wrapper.unmount()
document.body.removeChild(container)
})
(2) DOM API 모킹(Mock)하기
- 직접 DOM 접근 대신 테스트 코드에서 DOM 관련 메서드를 모킹해 원하는 값을 반환하도록 설정할 수 있다.
- 이 방법은 실제 DOM이 필요하지 않은 경우 유용하나, 실제 동작과 차이가 있을 수 있으므로 주의해야 한다.
test('getElementById 모킹 테스트', () => {
const dummyElement = document.createElement('div')
dummyElement.textContent = '예상되는 텍스트'
// getElementById를 모킹해 항상 dummyElement를 반환하도록 설정한다.
jest.spyOn(document, 'getElementById').mockReturnValue(dummyElement)
const element = document.getElementById('my-element')
expect(element).toBe(dummyElement)
// 테스트 후 모킹한 메서드를 복원한다.
document.getElementById.mockRestore()
})
(3) 테스트 전략 재검토
- 가능하면 직접 DOM 접근 대신 Vue의
ref
나 reactive 방식을 활용하는 것이 좋다. - 이 방법은 테스트 환경의 한계로 인한 문제를 근본적으로 회피할 수 있으며, Vue의 컴포넌트 라이프사이클에 맞춘 안정적인 DOM 접근을 보장한다.
4. 정리 및 결론
테스트 코드 작성 시 Vue 컴포넌트 내에서 직접 DOM API를 호출하는 경우 jsdom의 가상 DOM과 실제 DOM 간 차이로 인해 에러가 발생한다. attachTo
옵션을 사용하면 컴포넌트를 실제 document.body
나 커스텀 컨테이너에 마운트해 문제를 효과적으로 해결할 수 있다.
또한, DOM API 모킹이나 테스트 전략 재검토를 통해 상황에 맞는 대안을 마련할 수 있다. 테스트 종료 후 반드시 wrapper.unmount()
나 컨테이너 정리를 통해 메모리 누수를 방지해야 한다. 이번 포스트가 Vue 테스트 환경에서 DOM 접근 문제 해결에 도움이 되길 바란다.
반응형
'error log' 카테고리의 다른 글
[Vitest] Error: You must set an element with Modal.setAppElement(el) to make this accessible (2) | 2024.07.14 |
---|---|
[github Actions] job별로 시간 제한 걸기, timeout-minutes (0) | 2022.11.21 |
댓글