메인 컨텐츠로 건너뛰기

동기식에서 비동기식으로

V8의 변경 사항으로 인해 WebdriverIO 팀은 2023년 4월까지 동기식 명령 실행을 폐지할 것을 발표했습니다. 팀은 전환을 최대한 쉽게 만들기 위해 열심히 노력해 왔습니다. 이 가이드에서는 테스트 스위트를 동기식에서 비동기식으로 점진적으로 마이그레이션하는 방법을 설명합니다. 예제 프로젝트로 Cucumber Boilerplate를 사용하지만 접근 방식은 다른 모든 프로젝트에서도 동일합니다.

JavaScript의 프로미스​

WebdriverIO에서 동기식 실행이 인기 있었던 이유는 프로미스 처리의 복잡성을 제거했기 때문입니다. 특히 이러한 개념이 이런 방식으로 존재하지 않는 다른 언어에서 온 경우, 처음에는 혼란스러울 수 있습니다. 그러나 프로미스는 비동기 코드를 다루는 매우 강력한 도구이며, 오늘날의 JavaScript는 실제로 이를 쉽게 다룰 수 있게 합니다. 프로미스를 사용해 본 적이 없다면, 여기서 설명하기에는 범위를 벗어나기 때문에 MDN 참조 가이드를 확인하는 것이 좋습니다.

비동기 전환​

WebdriverIO 테스트러너는 동일한 테스트 스위트 내에서 비동기 및 동기 실행을 모두 처리할 수 있습니다. 이는 테스트와 PageObject를 자신의 속도에 맞게 단계별로 천천히 마이그레이션할 수 있음을 의미합니다. 예를 들어, Cucumber Boilerplate는 프로젝트에 복사할 수 있는 대규모 스텝 정의 세트를 정의했습니다. 한 번에 하나의 스텝 정의 또는 하나의 파일을 마이그레이션할 수 있습니다.

팁

WebdriverIO는 동기 코드를 거의 완전 자동으로 비동기 코드로 변환할 수 있는 codemod를 제공합니다. 문서에 설명된 대로 codemod를 먼저 실행하고 필요한 경우 수동 마이그레이션을 위해 이 가이드를 사용하세요.

많은 경우, 필요한 것은 WebdriverIO 명령을 호출하는 함수를 async로 만들고 모든 명령 앞에 await를 추가하는 것뿐입니다. 보일러플레이트 프로젝트에서 변환할 첫 번째 파일 clearInputField.ts를 살펴보면, 다음과 같이 변환합니다:

export default (selector: Selector) => {
$(selector).clearValue();
};

에서:

export default async (selector: Selector) => {
await $(selector).clearValue();
};

이게 전부입니다. 모든 재작성 예제가 포함된 전체 커밋은 여기에서 확인할 수 있습니다:

커밋:​

정보

이 전환은 TypeScript 사용 여부와 관계없이 독립적입니다. TypeScript를 사용하는 경우 tsconfig.json의 types 속성을 webdriverio/sync에서 @wdio/globals/types로 변경하세요. 또한 컴파일 대상이 최소한 ES2018로 설정되어 있는지 확인하세요.

특별한 케이스​

물론 더 많은 주의가 필요한 특별한 경우가 항상 있습니다.

ForEach 루프​

엘리먼트를 반복하는 등의 forEach 루프가 있는 경우, 반복자 콜백이 비동기 방식으로 제대로 처리되는지 확인해야 합니다. 예:

const elems = $$('div')
elems.forEach((elem) => {
elem.click()
})

forEach에 전달하는 함수는 반복자 함수입니다. 동기식 세계에서는 다음으로 넘어가기 전에 모든 요소를 클릭할 것입니다. 이를 비동기 코드로, 변환하면 모든 반복자 함수가 실행을 완료할 때까지 기다리도록 해야 합니다. async/await를 추가하면 이러한 반복자 함수는 우리가 해결해야 하는 프로미스를 반환합니다. 이제 forEach는 반복자 함수의 결과, 즉 우리가 기다려야 하는 프로미스를 반환하지 않기 때문에 더 이상 요소를 반복하는 데 이상적이지 않습니다. 따라서 해당 프로미스를 반환하는 map으로 forEach를 대체해야 합니다. map과 find, every, reduce 등 배열의 다른 모든 반복자 메서드는 반복자 함수 내의 프로미스를 존중하고 비동기 컨텍스트에서 사용하기 단순화되도록 구현되어 있습니다. 위의 예는 다음과 같이 변환됩니다:

const elems = await $$('div')
await elems.forEach((elem) => {
return elem.click()
})

예를 들어 모든 <h3 /> 요소를 가져와 텍스트 내용을 얻으려면 다음을 실행할 수 있습니다:

await browser.url('https://webdriver.io')

const h3Texts = await browser.$$('h3').map((img) => img.getText())
console.log(h3Texts);
/**
* 반환값:
* [
* 'Extendable',
* 'Compatible',
* 'Feature Rich',
* 'Who is using WebdriverIO?',
* 'Support for Modern Web and Mobile Frameworks',
* 'Google Lighthouse Integration',
* 'Watch Talks about WebdriverIO',
* 'Get Started With WebdriverIO within Minutes'
* ]
*/

이것이 너무 복잡해 보인다면 간단한 for 루프를 사용하는 것을 고려해 볼 수 있습니다. 예:

const elems = await $$('div')
for (const elem of elems) {
await elem.click()
}

WebdriverIO 어서션​

WebdriverIO 어서션 헬퍼 expect-webdriverio를 사용하는 경우 모든 expect 호출 앞에 await를 설정해야 합니다. 예:

expect($('input')).toHaveAttributeContaining('class', 'form')

다음과 같이 변환해야 합니다:

await expect($('input')).toHaveAttributeContaining('class', 'form')

동기식 PageObject 메서드와 비동기 테스트​

테스트 스위트에서 동기식으로 PageObject를 작성했다면, 더 이상 비동기 테스트에서 사용할 수 없습니다. 동기 및 비동기 테스트 모두에서 PageObject 메서드를 사용해야 하는 경우, 메서드를 복제하고 두 환경 모두에 제공하는 것을 권장합니다. 예:

class MyPageObject extends Page {
/**
* 요소 정의
*/
get btnStart () { return $('button=Start') }
get loadedPage () { return $('#finish') }

someMethod () {
// 동기 코드
}

someMethodAsync () {
// MyPageObject.someMethod()의 비동기 버전
}
}

마이그레이션을 완료하면 동기식 PageObject 메서드를 제거하고 이름을 정리할 수 있습니다.

PageObject 메서드의 두 가지 다른 버전을 유지하는 것이 마음에 들지 않는다면, 전체 PageObject를 비동기로 마이그레이션하고 동기 환경에서 browser.call을 사용하여 메서드를 실행할 수도 있습니다. 예:

// 이전:
// MyPageObject.someMethod()
// 이후:
browser.call(() => MyPageObject.someMethod())

call 명령은 다음 명령으로 이동하기 전에 비동기 someMethod가 해결되도록 합니다.

결론​

결과적인 재작성 PR에서 볼 수 있듯이 이 재작성의 복잡성은 상당히 간단합니다. 한 번에 하나의 스텝 정의를 재작성할 수 있다는 것을 기억하세요. WebdriverIO는 단일 프레임워크 내에서 동기 및 비동기 실행을 완벽하게 처리할 수 있습니다.

Welcome! How can I help?

WebdriverIO AI Copilot