Revert "WebView2 Fixed Version 호환성 문제 해결 - NuGet 패키지 버전 업데이트 및 환경 설정 개선"
This reverts commit a13306115b.
This commit is contained in:
@@ -46,3 +46,305 @@ UniMarc.csproj
|
||||
## Webview2 Fixed Version 다운로드 주소
|
||||
https://msedge.sf.dl.delivery.mp.microsoft.com/filestreamingservice/files/759b508a-00bb-4724-9b87-2703c8417737/Microsoft.WebView2.FixedVersionRuntime.139.0.3405.86.x86.cab
|
||||
|
||||
## WebView2 Selenium 스타일 DOM 조작 가이드
|
||||
|
||||
### 🔧 기본 DOM 조작 메서드들 (NamguLibrarySearcher 기본 클래스에 구현됨)
|
||||
|
||||
#### 1. 요소 대기 및 존재 확인
|
||||
```csharp
|
||||
// DOM 요소가 준비될 때까지 대기 (Selenium의 WebDriverWait와 유사)
|
||||
await WaitForElementReady("#clickAll", 10000); // 10초 타임아웃
|
||||
await WaitForElementReady("input[name='query']", 5000); // 5초 타임아웃
|
||||
```
|
||||
|
||||
#### 2. 요소 상태 확인
|
||||
```csharp
|
||||
// 체크박스가 체크되어 있는지 확인 (Selenium의 element.IsSelected와 유사)
|
||||
bool isChecked = await IsElementChecked("#clickAll");
|
||||
bool isLibMAChecked = await IsElementChecked("#libMA");
|
||||
```
|
||||
|
||||
#### 3. 요소 클릭
|
||||
```csharp
|
||||
// 요소 클릭 (Selenium의 element.Click()와 유사)
|
||||
await ClickElement("#clickAll"); // 전체 선택 클릭
|
||||
await ClickElement("#libMA"); // 문화정보도서관 클릭
|
||||
await ClickElement("button[type='submit']"); // 검색 버튼 클릭
|
||||
```
|
||||
|
||||
#### 4. 값 입력 및 설정
|
||||
```csharp
|
||||
// 입력창에 값 설정 (Selenium의 element.SendKeys()와 유사)
|
||||
await SetElementValue("input[name='query']", "검색어");
|
||||
await SetElementValue("#search_txt", "도서명");
|
||||
// 이벤트도 자동으로 발생시킴 (input, change 이벤트)
|
||||
```
|
||||
|
||||
#### 5. 텍스트 가져오기
|
||||
```csharp
|
||||
// 요소의 텍스트 내용 가져오기 (Selenium의 element.Text와 유사)
|
||||
string resultText = await GetElementText(".search-result span");
|
||||
string bookCount = await GetElementText("span:contains('전체')");
|
||||
```
|
||||
|
||||
### 🚀 실제 사용 예제
|
||||
|
||||
#### 도서관 선택 (남구통합도서관 예제)
|
||||
```csharp
|
||||
protected override async Task<bool> SelectLibraryWebView2()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. DOM 요소 존재 확인 및 대기
|
||||
await WaitForElementReady("#clickAll");
|
||||
await WaitForElementReady("#libMA");
|
||||
|
||||
// 2. 전체 선택 해제 (단계별 실행)
|
||||
bool isClickAllChecked = await IsElementChecked("#clickAll");
|
||||
if (isClickAllChecked)
|
||||
{
|
||||
await ClickElement("#clickAll");
|
||||
Console.WriteLine("전체 선택 해제됨");
|
||||
}
|
||||
|
||||
// 3. 특정 도서관 선택
|
||||
bool libMAChecked = await ClickElement("#libMA");
|
||||
Console.WriteLine($"문화정보도서관 선택: {libMAChecked}");
|
||||
|
||||
return libMAChecked;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"도서관 선택 오류: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 검색 실행 (단계별)
|
||||
```csharp
|
||||
private async Task PerformSearchWebView2(string searchTerm)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. 검색창 찾기 및 대기 (여러 선택자 시도)
|
||||
string[] searchSelectors = {
|
||||
"input[name='query']",
|
||||
"input[id='query']",
|
||||
"input[type='text']"
|
||||
};
|
||||
|
||||
string searchInputSelector = null;
|
||||
foreach (var selector in searchSelectors)
|
||||
{
|
||||
try
|
||||
{
|
||||
await WaitForElementReady(selector, 3000);
|
||||
searchInputSelector = selector;
|
||||
break;
|
||||
}
|
||||
catch { continue; }
|
||||
}
|
||||
|
||||
// 2. 검색어 입력
|
||||
await SetElementValue(searchInputSelector, searchTerm);
|
||||
|
||||
// 3. 검색 버튼 클릭 (여러 선택자 시도)
|
||||
string[] buttonSelectors = {
|
||||
"button[type='submit']",
|
||||
"input[type='submit']",
|
||||
".search-btn",
|
||||
".btn-search"
|
||||
};
|
||||
|
||||
bool searchExecuted = false;
|
||||
foreach (var selector in buttonSelectors)
|
||||
{
|
||||
if (await ClickElement(selector))
|
||||
{
|
||||
searchExecuted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 검색 버튼이 없으면 Enter 키로 검색
|
||||
if (!searchExecuted)
|
||||
{
|
||||
string enterScript = $@"
|
||||
const input = document.querySelector('{searchInputSelector}');
|
||||
if (input) {{
|
||||
input.dispatchEvent(new KeyboardEvent('keydown', {{
|
||||
key: 'Enter', keyCode: 13, bubbles: true
|
||||
}}));
|
||||
return true;
|
||||
}}
|
||||
return false;
|
||||
";
|
||||
await _webView2.CoreWebView2.ExecuteScriptAsync(enterScript);
|
||||
}
|
||||
|
||||
Console.WriteLine("✅ 검색 실행 완료");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ 검색 실행 오류: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 🎯 CSS 선택자 참고
|
||||
|
||||
#### 남구통합도서관 주요 선택자들
|
||||
```css
|
||||
/* 도서관 선택 체크박스 */
|
||||
#clickAll /* 전체 선택 */
|
||||
#libMA /* 문화정보도서관 */
|
||||
#libMB /* 푸른길도서관 */
|
||||
#libMC /* 청소년도서관 */
|
||||
#libSW /* 효천어울림도서관 */
|
||||
#libSQ /* 스마트도서관 */
|
||||
|
||||
/* 검색 관련 */
|
||||
input[name='query'] /* 검색창 */
|
||||
button[type='submit'] /* 검색 버튼 */
|
||||
|
||||
/* 결과 관련 */
|
||||
.search-result /* 검색 결과 영역 */
|
||||
span:contains('전체') /* 검색 결과 수량 */
|
||||
```
|
||||
|
||||
### ⚠️ 주의사항
|
||||
|
||||
1. **페이지 로딩 대기**: 항상 `WaitForElementReady()`로 요소가 준비될 때까지 대기
|
||||
2. **에러 처리**: try-catch로 각 단계별 예외 처리
|
||||
3. **로깅**: `Console.WriteLine()`으로 상세한 실행 로그 남기기
|
||||
4. **타임아웃**: 적절한 타임아웃 설정 (기본 10초)
|
||||
5. **다중 선택자**: 여러 CSS 선택자를 배열로 준비하여 순차적으로 시도
|
||||
|
||||
### 🔄 Selenium에서 WebView2로 이주 가이드
|
||||
|
||||
| Selenium 코드 | WebView2 대응 코드 |
|
||||
|---------------|-------------------|
|
||||
| `WebDriverWait.Until()` | `await WaitForElementReady()` |
|
||||
| `element.Click()` | `await ClickElement()` |
|
||||
| `element.SendKeys()` | `await SetElementValue()` |
|
||||
| `element.Text` | `await GetElementText()` |
|
||||
| `element.IsSelected` | `await IsElementChecked()` |
|
||||
| `element.Clear()` | SetElementValue로 빈 문자열 설정 |
|
||||
|
||||
### 💡 성능 및 안정성 팁
|
||||
|
||||
- **WebView2가 Selenium보다 빠름**: 네이티브 성능
|
||||
- **고정 버전 사용**: Chrome 버전 호환성 문제 없음
|
||||
- **단계별 실행**: 각 작업을 개별 메서드로 분리하여 디버깅 용이
|
||||
- **상세 로깅**: 각 단계마다 성공/실패 상태 출력
|
||||
- **단순한 JavaScript 사용**: 복잡한 JSON보다는 단순한 문자열 반환 권장
|
||||
|
||||
## 🚨 **WebView2 JavaScript 실행 시 주의사항 (중요 교훈)**
|
||||
|
||||
### **문제: RuntimeBinderException과 null 반환**
|
||||
|
||||
**❌ 피해야 할 패턴:**
|
||||
```csharp
|
||||
// 복잡한 JSON 반환 스크립트 - 불안정함
|
||||
string script = $@"
|
||||
try {{
|
||||
const element = document.querySelector('{selector}');
|
||||
return JSON.stringify({{ success: true, isChecked: element.checked }});
|
||||
}} catch (e) {{
|
||||
return JSON.stringify({{ success: false, error: e.message }});
|
||||
}}
|
||||
";
|
||||
|
||||
string result = await webView2.CoreWebView2.ExecuteScriptAsync(script);
|
||||
dynamic resultObj = JsonConvert.DeserializeObject(result); // RuntimeBinderException 위험!
|
||||
|
||||
if (resultObj.success == true) // null 참조 오류 발생 가능
|
||||
```
|
||||
|
||||
**✅ 권장하는 안전한 패턴:**
|
||||
```csharp
|
||||
// 단순한 문자열 반환 - 안정적임
|
||||
string result = await webView2.CoreWebView2.ExecuteScriptAsync($@"
|
||||
try {{
|
||||
var element = document.querySelector('{selector}');
|
||||
if (element && element.checked) {{
|
||||
element.click();
|
||||
return element.checked ? 'failed' : 'success';
|
||||
}}
|
||||
return element ? 'already_unchecked' : 'not_found';
|
||||
}} catch (e) {{
|
||||
return 'error: ' + e.message;
|
||||
}}
|
||||
");
|
||||
|
||||
// 안전한 문자열 비교
|
||||
if (result != null && result.Contains("success"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### **핵심 교훈:**
|
||||
|
||||
1. **단순함이 최고**: WebView2에서는 복잡한 JSON.stringify()보다 단순한 문자열 반환이 훨씬 안정적
|
||||
2. **Dynamic 타입 위험**: `dynamic` 객체는 null 체크 없이 사용하면 RuntimeBinderException 발생
|
||||
3. **직접적인 JavaScript**: 중간 JSON 변환 없이 직접적인 DOM 조작이 더 확실함
|
||||
4. **단계별 진단**: 복잡한 로직은 여러 개의 간단한 스크립트로 분할하여 실행
|
||||
|
||||
### **권장 디버깅 패턴:**
|
||||
```csharp
|
||||
// 1단계: 기본 JavaScript 실행 확인
|
||||
string test1 = await webView2.ExecuteScriptAsync("1+1");
|
||||
Console.WriteLine($"기본 연산: {test1}");
|
||||
|
||||
// 2단계: DOM 접근 확인
|
||||
string test2 = await webView2.ExecuteScriptAsync("document.title");
|
||||
Console.WriteLine($"DOM 접근: {test2}");
|
||||
|
||||
// 3단계: 요소 존재 확인
|
||||
string test3 = await webView2.ExecuteScriptAsync($"document.querySelector('{selector}') !== null");
|
||||
Console.WriteLine($"요소 존재: {test3}");
|
||||
|
||||
// 4단계: 실제 작업 수행
|
||||
string result = await webView2.ExecuteScriptAsync($"document.querySelector('{selector}').click(); 'clicked'");
|
||||
Console.WriteLine($"작업 결과: {result}");
|
||||
```
|
||||
|
||||
**이 패턴을 사용하면 WebView2 JavaScript 실행에서 99%의 문제를 예방할 수 있습니다!**
|
||||
|
||||
## 🧹 **WebView2 메모리 누수 방지 (중요!)**
|
||||
|
||||
**문제**: WebView2는 메모리 누수로 인해 프로그램이 갑자기 종료될 수 있습니다.
|
||||
|
||||
**해결책**: 검색 전후로 메모리 정리를 수행하세요.
|
||||
|
||||
```csharp
|
||||
// 검색 시작 전
|
||||
await CleanupWebView2Memory();
|
||||
|
||||
// 검색 작업 수행...
|
||||
|
||||
// 검색 완료 후 (finally 블록에서)
|
||||
finally {
|
||||
await CleanupWebView2Memory();
|
||||
}
|
||||
|
||||
private async Task CleanupWebView2Memory()
|
||||
{
|
||||
try {
|
||||
await _webView2.CoreWebView2.ExecuteScriptAsync(@"
|
||||
if (window.gc) window.gc(); null;
|
||||
");
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
} catch { /* 무시 */ }
|
||||
}
|
||||
```
|
||||
|
||||
**주의사항**:
|
||||
- WebView2는 장시간 사용 시 메모리 누수 발생 가능
|
||||
- 각 검색 작업 후 반드시 정리 수행
|
||||
- Chrome WebDriver보다 메모리 관리가 까다로움
|
||||
|
||||
|
||||
Reference in New Issue
Block a user