인터넷 익스플로러와 연동하기 - 1

정성태

프린트 이메일 전송

연+재+순+서
1회 2004.1 | 스마트 클라이언트 탐험하기
2회 2004. 2 | 인터넷 익스플로러와 연동하기
3회 | 스마트 클라이언트 배포하기

연+재+가+이+드
운영체제 | IIS가 설치된 윈도우(필자의 경우 윈도우 2003 서버)
개발도구 | 비주얼 스튜디오 닷넷 2003, 닷넷 프레임워크 v1.1.4322
기초지식 | COM, C#, ASP.NET 기초
응용분야 | 현재로서는 기업 내부의 인트라넷 환경에서 액티브X 컨트롤 대체. 닷넷 프레임워크가 일반화되면 외부 웹 사이트에서 액티브X 컨트롤 대체

=====================================================================================

스마트 클라이언트는 제작하기 쉽고 유지/보수가 간편하긴 하지만 기존 액티브X 컨트롤들은 그 제작 방법에 있어서 다양한 IE와 연동 기법을 사용해 왔다. 스마트 클라이언트도 그와 동일한 기능을 기본적으로 구현할 수 있어야만 실제 현업에서 사용할 수 있는데, 비록 닷넷 프레임워크가 COM 환경과 연동을 고려해 제작되긴 했지만, COM 자체로 동작되는 것은 아니기 때문에 기존 액티브X 컨트롤의 기능을 스마트 클라이언트로 포팅하는 것이 그다지 만만하진 않다. 이번 호에서는 바로 스마트 클라이언트가 어떻게 IE와 연동할 수 있는지를 소개 한다.

COM으로 액티브X를 개발해 본 독자라면 대개의 경우 일정한 순서로 공부를 했을 것이다. 우선 기본적인 메쏘드와 속성을 구현했을 것이고 그 다음 약간 어려운 이벤트 연결을 공부했을 것이다. 그렇게 COM에 익숙해지고 나면, 웹에서 액티브X 컨트롤 제작을 해보게 되고 결국 IE(Internet Explorer)와 연동을 익히게 된다. 마찬가지로 우리는 지난 호에서 기본적인 기능을 할 수 있는 스마트 클라이언트를 개발해 보았다. 스크립트 상에서 스마트 클라이언트의 속성, 메쏘드, 이벤트를 다룰 수 있는 방법을 소개했으며, 그에 따른 보안 관련된 설정을 알아보았다. 순서대로 이제 우리는 스마트 클라이언트로 IE와 연동하는 방법을 알아내야 한다.
닷넷 프레임워크는 일종의 ‘프로그래밍 운영환경’에 지나지 않기 때문에 IE와 연동이 그다지 매끄럽게 이뤄지지는 않는다. 아마도 그 부분은 닷넷에서의 변화만으로는 부족하고 IE 자체도 변화돼야 가능할 텐데, 그 부분에 대해서는 차기 IE 버전에서나 기대해 볼 수밖에 없다. 어쨌든 우리는 현재의 IE 환경에서 구현을 해야만 한다. 그러다 보니 이번 연재에서도 역시 COM에 대한 지식을 기본 전제로 하게 되나, 그렇다고 해서 코드가 비주얼 C++로 된 것은 아니니 닷넷 언어만 아는 독자라도 unmanaged 환경과 interop을 어떻게 할 수 있는 지에 대한 많은 지식을 얻게 될 것이다.
아직 공식적으로 마이크로소프트(이하 MS)에 의해 제공되는 IE와 연동에 대한 예제 코드는 나와 있지 않은 상태이기 때문에 여기서 보여준 코드가 ‘표준’적인 접근 방식이라고 말할 수는 없다. 단지 필자가 시행착오로 알아낸 것들이기 때문에 이외에도 얼마든지 다양한 방법으로 구현 가능할 수도 있다는 것이다. 각자의 개발 경험만큼 여전히 다른 식으로 ‘IE와 연동’을 이끌어 낼 수 있으며 오히려 더 좋은 방법들이 여러분 머리속에 있을 지도 모르겠다. 이 기사를 보면서 그 가능성을 제한하지 말고 자신이 생각하는 IE와 연동 기법을 구현해 보기 바라며 또한 그 방법을 널리 알려주기 바란다. 지난 호 기사를 읽지 않은 독자는 반드시 읽어보고 이번 호 기사를 따라해 볼 것을 권한다. 왜냐하면, 이번 호에서의 실습은 지난 호에서 했던 설정사항을 그대로 이어받아 진행되기 때문이다. 만약 그러한 고려를 하지 않은 사용자라면 이번 호에서 개발하는 스마트 클라이언트의 동작이 안 될 수도 있음을 감안하기 바란다.

스크립트와 연동
액티브X를 제작하면서 HTML에 포함된 스크립트 함수를 호출하는 기능을 만들어 본 적이 있을 것이다. 이것을 스마트 클라이언트로 구현해 보자. COM을 잘 아는 독자라면 그다지 어렵지 않은 작업이겠지만, 그렇지 않은 독자들은 감이 잘 잡히지 않을 것이다. 예제를 위한 스마트 클라이언트는 지난 호에 작업했던 TreeControl을 확장해 나가는 방식으로 하겠다.
스마트 클라이언트에서 HTML 내의 스크립트 함수를 호출하는 것은 어떤 의미에서 ‘이벤트’를 발생시키는 것과 동일한 기능을 한다. 사실 이벤트를 구현하기 위해서는 이벤트 소스를 구현해야 하는 등의 다소 복잡한 부가 코드가 필요하지만 스크립트 함수를 직접 호출하는 것은 그보다 좀더 간단한 코드로 구현할 수 있다. 실제로 필자의 경우에는 이벤트를 구현하는 것보다 스크립트 함수를 직접 호출하는 것을 선호한다. 우선 코딩에 앞서 이론적인 사항부터 살펴보자.
IE는 HTML 내부의 태그뿐만 아니라 JScript 함수까지도 IDispatch 인터페이스로 다룰 수 있게 하고 있다. 이해를 돕기 위해 다음의 JScript 함수를 보자. 여기서 test_func 함수 자체가 IDispatch 인터페이스인 것이다.

function test_func()
{
alert( ‘일반 함수입니다.’ );
}

좀더 정확히 살펴보면 test_func 함수는 IDispatchEx 인터페이스를 구현하고 있다. IDispatchEx에 대해서 다소 낯설어 할 독자들이 있을 텐데, 설령 그럴지라도 대개의 경우 모르는 상태로 일반적으로 사용한 것이 바로 IDispatchEx 인터페이스이다. 이것은 기존 IDispatch 구현을 포함하면서 멤버에 대한 동적 추가/삭제를 지원하고 있다. IDispatch의 동작 원리 자체가 IDispatchEx로 기능 확장을 위한 배려가 되어 있다고도 볼 수 있다.
예를 들어 IDispatch를 구현한 COM 개체의 ‘TestFunc’ 함수를 호출한다고 가정하자. 그러면 스크립트에서 TestFunc을 obj.TestFunc()이라고 호출하게 되고, 이는 액티브 스크립팅 엔진 내부에서 obj가 구현한 IDispatch 개체의 GetIDsOfNames 메쏘드를 호출해 ‘TestFunc’ 이름에 해당하는 DISPID를 반환받게 되고, DISPID를 인자로 해서 IDispatch::Invoke 메쏘드를 호출해 주는 것이다. 즉 IDispatch::GetIDsOfNames에 의한 이름과 DISPID 쌍으로 메쏘드 호출이 이뤄지므로 멤버를 추가하기 위해 단지 맵핑 테이블 구조에 다른 이름과 DISPID를 추가해 주고, DISPID와 호출될 메쏘드 함수의 포인터를 관리하기만 하면 되는 것이다. 여러분이 이러한 IDispatchEx를 언제 사용했는지를 잠깐 살펴보자.

<input type=button id=”_btn_Test” value=”버튼을 누르세요”>

앞의 코드를 보면 원래의 <INPUT /> 요소에는 newField나 newFunc 멤버는 존재하지 않는다. 하지만 INPUT 요소가 IDispatchEx를 구현하고 있기 때문에 앞과 같이 멤버 변수와 멤버 메쏘드를 실행시에 동적으로 추가하게 된 것이다. 앞의 코드는 COM 개체에 대해 전혀 모르는 웹 프로그래머들도 자주 사용해 보았을 것이다. 그렇게 우린 이미 암암리에 IDispatchEx와 충분히 친해져 있었던 것이다. HTML 요소에서는 앞과 같은 코드를 많이 사용해 봤을 텐데, 정작 스크립트 함수까지도 IDispatchEx라는 것을 아는 개발자들은 많지 않다. 원리를 알았으니 스크립트 함수도 IDispatchEx를 구현했다는 것을 직접 예를 들어 증명해 보자.

function window.onload()
{
testFunc.newField = “새로운 멤버 변수 추가”;
testFunc.newFunc = newAddedFunc;
testFunc();
}

function testFunc()
{
alert( testFunc.newField );
testFunc.newFunc( “새로운 멤버 함수 추가” );
}

function newAddedFunc( outputText )
{
alert( outputText );
}

앞의 코드에서 설명한 IDispatchEx를 이해하고 스크립트 함수가 그 IDispatchEx로 구현되었다는 것을 안다면 지금의 예제가 그리 낯설지 않을 것이다. 보는 바와 같이 스크립트 함수인 testFunc 역시 HTML 요소와 똑같이 동적인 멤버 추가가 가능하다는 것을 알 수 있다.
이야기의 주제를 다시 스마트 클라이언트로 돌려 보자. 결국 스마트 클라이언트에서 스크립트 함수를 호출하는 것은 IDispatch 인터페이스 포인터의 Invoke를 호출하는 것과 다를 것이 없다. 예를 위해 이전에 제작해 두었던 TreeControl에 스크립트 함수를 호출할 수 있는 코드를 추가해 보자. 우선 호출할 스크립트 함수의 IDispatch 인터페이스를 보관할 속성이 필요하다. TreeEvent.cs 파일의 ITreeControlCOMIncoming 인터페이스에 다음과 같은 속성을 추가한다.

object _NodeClicked { set; get; }

TreeControls.cs 파일에서는 앞의 인터페이스에 구현된 프로퍼티를 구현해 줘야 한다.

object _NodeClicked = null;
public object _NodeClicked // 공용 프로퍼티로 정의
{
get { return _nodeClicked; }
set { _nodeClicked = value;
}

// 기존에 정의된 TreeView::AfterSelect 이벤트 핸들러를 다음과 같이 수정
private void _treeView_AfterSelect( ... )
{
if ( _nodeClicked != null )
{
// IDispatch::Invoke 메쏘드를 호출하는 것과 동일한 호출 역할
Type type = _nodeClicked.GetType();
type.InvokeMember( “”, BindingFlags.InvokeMethod, null, _nodeClicked, null );
}
}

IDispatch::Invoke 메쏘드를 직접 호출해 주던 것과 달리 Type 클래스의 InvokeMember 메쏘드를 호출한 것만 제외한다면 비주얼 C++ 코드와 비교해 패턴 자체는 크게 변한 것이 없다. 스마트 클라이언트 측에서는 내부적으로 앞과 같이 구현해 주면 된다. 이어서 다음의 코드에서 보는 것처럼 HTML 스크립트에서 호출될 스크립트 함수를 대입해 주고, 해당 스크립트 함수 안에 코드를 구현해 주면 된다.

Form1._Control1._NodeClicked = treeControl_nodeClicked;
function treeControl_nodeClicked()
{
// 트리 컨트롤에 선택된 노드를 가져와 처리
}

이제 컴파일하고 웹 브라우저에서 테스트를 해보자. 트리 컨트롤에서 노드를 클릭하면 앞에서 구현한 treeControl_nodeClicked 스크립트 함수가 실행되는 것을 확인할 수 있다. 결국 이전에 말했던 것처럼 결과적으로는 이벤트 함수와 역할이 동일할 뿐만 아니라, 지난 호의 이벤트 구현을 위해 소요되었던 코드와 비교해 볼 때 구현도 간단해졌다(여전히 보안 설정은 ‘Unmanaged 코드를 호출 가능’하도록 설정해 둬야 한다).
참고로 필자가 겪은 재미있는 오류를 하나 설명하고자 한다. HTML에 구현된 스크립트 함수를 CLR(Common Language Runtime)에서는 InvokeMember로 호출하게 되는데, 만약 그 스크립트 함수 내부에서 오류가 발생하면 흔히 본 ‘스크립트 오류 메시지’ 창이 뜨지 않고 ‘닷넷 프레임워크의 예외 메시지’ 창이 뜨게 된다. 예외 내용은 System.Reflection.TargetInvocationException이니 혹시나 이 예외가 발생했다고 해서 CLR 코드를 의심하지 말고 스크립트 함수를 살펴보기 바란다.

인터넷 익스플로러와 연동하기 - 2

IWebBrowser2 인터페이스 얻기
이번 연재의 하이라이트가 아닐까 생각된다. 필자가 처음 스마트 클라이언트를 제작하고서 가장 먼저 해결하고자 했던 것이 바로 자신을 호스팅하고 있는 IWebBrowser2 인터페이스를 얻어내는 것이다. 아마도 이미 개발된 기존 액티브X 중에서도 많이 활용하고 있는 부분이 아닐까 싶다. 액티브X 컨트롤로 구현할 때의 얘기를 해보면, WebBrowser 개체는 자신이 호스팅하고 있는 모든 컨트롤에 대해서 CreateInstance로 개체를 생성한 후 IUnknown::QueryInterface를 통해 IObjectWithSite 인터페이스 포인터를 얻어낸다. 만약 그 인터페이스가 구현되어 있다면 개체의 Site IUnknown 인터페이스 포인터를 인자로 해서 IObjectWithSite::SetSite 메쏘드를 호출해 주고, 결과적으로 액티브X 컨트롤은 넘겨받은 Site 인터페이스 포인터를 통해 자신을 호스팅하고 있는 IWebBrowser2 인터페이스 포인터를 얻어낼 수 있게 되는 것이다.
액티브X에서 구현처럼 처음에 스마트 클라이언트 역시 IObjectWithSite 인터페이스를 구현해 단순히 클래스에서 상속을 받아주면 되지 않을까 싶었는데 성공하지 못했다. 아직 필자가 RCW/CCW의 운영에 대한 지식이 미흡해서 밝히지 못했음을 미리 말해둔다. ‘이달의 디스켓’에 실린 소스에서는 IObjectWithSite 인터페이스 코드를 실어 뒀으니 독자들도 도전해 보기 바란다.
가장 표준적일 수 있는 IObjectWitheSite::SetSite 인터페이스를 통한 방법이 미흡한 실력으로 인해 구현할 수 없지만, 프로그래밍 세계에서는 어떤 문제 해결을 위한 방법이 오직 하나만 있는 것이 아님을 독자들도 잘 알고 있을 것이다. 그렇다면 과연 뭐가 남아 있는가? 이렇게 되면 역시 예전의 unmanaged 환경에서 제공되던 길을 찾아야 한다. 나름대로 여러 가지 방법이 있을 수 있겠지만, 필자가 발견한 방법은 Microsoft Active Accessibility SDK(이하, MSAA)에서 제공해 주는 ObjectFromLresult 함수를 이용한 방법이다.
이것은 해당 응용 프로그램이 고유하게 정의한 윈도우 메시지를 통해 프로세스 간에도 사용할 수 있도록 완전히 마샬링된 인터페이스 포인터를 얻을 수 있는 방법을 제공해 준다. 그렇다고 해서 모든 액티브X 컨트롤이 기본으로 제공하는 것은 아니고, 응용 프로그램 개발자가 알아서 내부에 구현해 줘야 한다. 다행히도 웹 브라우저 컨트롤의 경우 이 방법을 제공하고 있는데, 아마도 이 기능을 넣어둔 MS 개발자조차도 닷넷 환경의 스마트 클라이언트에서 유용하게 쓰일 것이라고는 예측하지 못했을 것 같다.
접근 방법은 정해졌으니 하나씩 구현을 해보자. 우선 해야 할 것은 IWebBrowser2 개체의 윈도우 핸들을 알아내는 것이다. 이는 스마트 클라이언트가 윈도우이고 그의 부모 윈도우가 WebBrowser라는 것으로 쉽게 구할 수 있다. <화면 1>은 Spy ++ 유틸리티를 이용해 웹 브라우저 안에 활성화된 스마트 클라이언트의 위치를 확인해 본 것이다.
지난 호에 만들어 두었던 스마트 클라이언트인 TreeControl의 경우 윈도우 폼 위에 TreeView 컨트롤을 얹어 놓은 형태이다. <화면 1>을 보면 강조된 ‘WindowsForms10.Window8.app9’가 바로 윈도우 폼이고, 하위의 ‘WindowsForms10.SysTreeView32.app9’가 TreeView 컨트롤이다. 우리가 구해야 할 IWebBrowser2 인터페이스 포인터를 담고 있는 윈도우는 스마트 클라이언트의 부모 윈도우인 ‘Internet Explorer_Server’이기 때문에 Win32 API에서 제공하고 있는 GetParent API로 구할 수 있다. 일단 HWND를 구하고 나면 그 이후의 구현은 MS에서 제공해 주는 문서대로 구현하면 된다. 해당 문서에 대해서는 참고자료에서 확인할 수 있으니 참조하고, 스마트 클라이언트에서 사용하기 위해 C# 언어로 포팅된 코드는 다음과 같다.

한 가지 더 언급하자면 정상적인 컴파일을 위해 PIA(Primary Interop Assembly) 모듈로 제공되는 ‘Microsoft.mshtml’을 참조 추가해야 한다. 주의해야 할 것은 이 모듈은 ‘닷넷 프레임워크 재배포 모듈’이 설치된 컴퓨터에는 포함되어 있지 않고, VS.NET을 설치한 경우에만 GAC(Global Assembly Cache)에 등록된다는 것이다. 이것으로 인해 문제가 발생하게 되는데, VS.NET 2003 IDE에서 참조를 추가하는 경우 기본적으로 GAC에 등록된 모듈에 대해 로컬 복사가 ‘False’여서 이렇게 되면 클라이언트에서 활성화되는 스마트 클라이언트의 경우 해당 DLL을 찾을 수 없으므로 동작 자체가 되지 않는다. 따라서 반드시 참조로 추가한 다음 해당 모듈의 속성창에서 로컬 복사 속성을 ‘True’로 바꿔야 한다.

// 자신의 윈도우 핸들을 구하고 부모인 WebBrowser 윈도우 핸들을 구한다.
IntPtr pHandle = this.Handle;
pHandle = GetParent( pHandle );

// WebBrowser 컨트롤에서 정의된 특별한 메시지를 얻어낸다.
uint nMsg = RegisterWindowMessage( “WM_HTML_GETOBJECT” );

// ObjectFromLresult의 인자로 전달돼야 할 lRes 값을 구하고
uint lRes = 0;
SendMessageTimeout( pHandle, nMsg, 0, 0, 2, 1000, ref lRes ); // SMTO_ABORTIFHUNG : 2

// MsHTML.h/ComDef.h에서 IHTMLDocument2의 GUID를 얻을 수 있다.
Guid htmlDocumentGuid = new Guid( “332C4425-26CB-11D0-B483-00C04FD90119” );
// MSAA SDK에서 제공하는 함수를 통해
// 마샬링된 IHTMLDocument2 인터페이스 포인터를 구한다.
ObjectFromLresult( lRes, ref htmlDocumentGuid, 0, out _htmlDocument);

Type t = _htmlDocument.GetType();
string title = (string)t.InvokeMember( “title”, BindingFlags.GetProperty, null, _htmlDocument, null );

// mshtml.HTMLDocument docObj = (mshtml.HTMLDocument)obj;
// string title2 = docObj.title;

앞의 코드에서 GetParent, RegisterWindowMessage, Send MessageTimeout, ObjectFromLresult 메쏘드는 Win32 API로서 PInvoke로 호출된다(해당 DllImport 선언과 관계된 소스는 ‘이달의 디스켓’에 실려 있다). 일단은 앞의 과정을 통해 IHTML Document2 인터페이스까지 얻을 수 있다.
앞의 코드를 자세히 보면 맨 마지막 라인에 주석 처리가 된 것에 대해서 의문을 제기할 것이다. 주석 처리된 라인의 위에 보면 IHTMLDocument2::title을 구하기 위해 복잡하게 Type.Invoke Member를 호출한 것을 볼 수 있는데, 굳이 그렇게 한 것에는 ‘보안’과 관계된 설정 사항 때문이다. 이쯤에서 고백하는데, 필자 나름대로 스마트 클라이언트를 개발하면서 이해할 수 없는 현상을 많이 접해 보았다. 앞의 코드 역시 필자에게는 아직 그런 ‘이해할 수 없는 현상’ 중의 하나로 남아 있다.
주석 처리된 코드에서 보는 것처럼 만약 mshtml.HTML Document로 형변환을 하게 되면 지난 호에 설정했던 ‘Ineternet SmartClient_Zone’에 ‘Full Trust’를 줘야 한다. 가능한 최소한의 권한만을 스마트 클라이언트에 주는 것이 바람직하기 때문에 필자로서는 ‘Full Trust’를 사용하지 않는 다른 방법을 찾았고 그것이 Type.InvokeMember였다. 원칙적으로 이해하려고 든다면 mshtml.HTMLDocument를 사용하는 것에는 지난 호에 살펴본 ‘Unmanaged Code 호출 가능’ 권한만으로도 충분하다고 볼 수 있다. 그런데 왜 ‘Full Trust’를 필요로 하게 되었는지에 관해서는 필자 자신도 어떻게 설명할 길이 없다. Full Trust를 스마트 클라이언트에 허용해 mshtml.HTMLDocument를 사용할 것인지, 아니면 제한된 ‘SmartClientSet’을 허용하고 프로그램을 다소 복잡하게 할 것인지는 독자의 선택에 달려 있다. 물론, 필자는 이 연재 전체에 걸쳐서 SmartClientSet을 선택한 것을 기본 전제로 코드를 구현했음을 알아주기 바란다.
앞의 과정을 통해 일단 object 형식의 IHTMLDocument2 인터페이스를 구하게 되었고, Untyped 상태에서 해당 객체의 메쏘드/속성을 호출하는 방법을 알아보았다. 경우에 따라 스마트 클라이언트 제작에 웹 브라우저와 연동을 고려해 볼 때 이 정도 수준에서도 끝낼 수 있다. 사실 액티브X 컨트롤을 제작하던 때를 생각해 보면, IWebBrowser2 인터페이스를 구하려는 목적에는 크게 두 가지를 떠 올릴 수 있을 것이다. 첫 번째는 IWebBrowser2::Document를 통해 컨트롤을 호스팅하고 있는 HTML 문서를 접근하고자 하는 경우이고, 두 번째는 웹 브라우저의 이벤트를 받고 싶은 경우이다. 대개의 경우 첫 번째 목적이라면 ObjectFromLresult 함수의 특이한 능력 덕분에 굳이 IWebBrowser2 인터페이스까지 구할 필요 없이 앞의 코드까지만 구현하면 된다. 하지만 웹 브라우저의 이벤트를 받고자 한다면 결국 IWebBrowser2 인터페이스까지 얻어야 가능하다.
웹 브라우저로부터 이벤트를 받는 것을 목표로 힘들겠지만 한 단계 더 나아가서 IWebBrowser2 인터페이스까지 구해보자. IE에서 액티브X 컨트롤을 만들어 본 독자들은 IWebBrowser2::Document를 통해 IHTMLDocument2 인터페이스를 쉽게 얻었던 기억이 있을 것이다. 하지만 그 역으로 변환하는 것을 해 본 독자들은 많지 않을 것 같다. 일단 그 방법에 관한 C++로 된 소스는 참고자료를 참고하고, 여기서는 C# 버전으로 포팅된 코드로 살펴보자. mshtml.HTML Document를 다루기 위해 Microsoft.mshtml 모듈을 참조한 것과는 달리 IWebBrowser2 인터페이스를 다루기 위한 참조(ShDocVw.dll, “Microsoft Internet Controls”)를 추가할 필요는 없다.

// IHTMLDocument2로부터 IWebBrowser2 인터페이스 얻기

// managed 개체로부터 unmanaged IUnknown 포인터를 반환
IntPtr docPtr = Marshal.GetIUnknownForObject( _htmlDocument );

// IOleCommandTarget GUID는 DocObj.h/ComDef.h에서 구함
Guid ocmTargetGuid = new Guid( “B722BCCB-4E68-101B-A2BC-00AA00404770” );
IntPtr ocmPtr;
// IUnknown::QueryInterface를 Marshal 클래스에 정의된 QueryInterface를 이용
// IHTMLDocument2로부터 IOleCommandTarget 인터페이스를 가져오기
Marshal.QueryInterface( docPtr, ref ocmTargetGuid, out ocmPtr );
Marshal.Release( docPtr );

// IServiceProvider의 GUID는 ServProv.h에서 구함
Guid svpGuid = new Guid( “6d5140c1-7436-11ce-8034-00aa006009fa” );
IntPtr svpPtr;
// IOleCommandTarget으로부터 IServiceProvider 인터페이스를 가져오기
Marshal.QueryInterface( ocmPtr, ref svpGuid, out svpPtr );
Marshal.Release( ocmPtr );

// unmanaged IServiceProvider 개체를 managed 개체로 변환
SmartClient.IServiceProvider svpObject =
(SmartClient.IServiceProvider)Marshal.GetObjectForIUnknown( svpPtr );

// SID_SWebBrowserApp와 IID_IWebBrowser2 GUID는 ExDisp.h에서 구함
Guid sidWBA = new Guid( “0002DF05-0000-0000-C000-000000000046” );
Guid webBrowser2 = new Guid( “D30C1661-CDAF-11d0-8A3E-00C04FC9E26E” );
// IServiceProvider::QueryService를 통해 IWebBrowser2 인터페이스 포인터를 반환
object WBObject = svpObject.QueryService( ref sidWBA, ref webBrowser2 );
Marshal.Release( svpPtr );

// SHDocVw.WebBrowser로 형변환
// 이후 IWebBrowser2 개체와 동일하게 처리
_webBr = (IWebBrowser2)WBObject;

앞의 코드를 보면 unmanaged COM 개체들을 C++로 다루던 것을 managed 환경의 C#에서 다루는 것이 쉽지만은 않아 보인다. 앞의 내용에서 핵심 작업은 System.Runtime.InteropServices. Marshal 클래스로 어떻게 unmanaged 환경에서 COM 작업을 대체하느냐 하는 것인데, 보는 것처럼 C/C+보다 약간 부가적인 코드를 필요로 해도 C#으로도 동일하게 포팅할 수 있다. COM 개체와 상호운용을 가능케 하는 Marshal 클래스에는 앞의 코드에서 보여준 메쏘드 이외에도 unmanaged와 managed 사이의 다리 역할을 해주는 static 함수들이 상당수 포함되어 있으니 시간이 날 때 틈틈이 살펴보는 것도 좋겠다.
웬만큼 COM을 경험한 독자들의 경우에는 앞의 코드를 하나씩 뜯어보면 모르는 부분은 없을텐데, 그래도 IServiceProvider에 대해서는 짚고 넘어가야겠다. 그 앞의 코드에서 본 것처럼 IDispatch를 상속받은 인터페이스의 메쏘드 호출은 Type.InvokeMember를 통해 가능하다는 것을 알았다. 하지만 IUnknown으로부터 상속받은 IServiceProvider 같은 경우에는 사정이 달라진다. 즉 DISPID가 없는 경우에는 직접 vtable에 기반한 메쏘드 호출을 해야 하기 때문이다. 다행히도 C#의 interface 구문으로 다음과 같이 C/C++에서와 같은 interface를 만들 수 있게 해주고 있다. 더군다나 IService Provider 같은 경우에는 단순하게 IUnknown 인터페이스에서 단 하나의 QueryService 메쏘드만을 추가한 인터페이스이기 때문에 그다지 어렵지 않게 구현할 수 있다.

[Guid(“6d5140c1-7436-11ce-8034-00aa006009fa”)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] // IUnknown 기본 구현
[ComImport()]
public interface IServiceProvider
{
[return: MarshalAs(UnmanagedType.IUnknown)]
object QueryService( ref Guid guidService, ref Guid riid );
}

물론 앞의 코드에 구현된 인터페이스를 직접 상속받아서 사용할 일은 없다. unmanaged 환경에서 구한 인터페이스 포인터를 managed 개체로 변환한 후에 스텁(stub)만 구현해 둔 인터페이스로 ‘형변환’을 해주어 사용하는 목적으로 쓰일 것이기 때문이다. 사실 IServiceProvider까지는 구현이 간단한데, 그와 동일하게 여러분은 IWebBrowser2에 대해서도 앞의 코드와 같은 형태의 인터페이스를 직접 코딩해 줘야 한다. 만약 이벤트까지 생각한다면 DWebBrowser Events2 인터페이스까지도 만들어야 한다. 그에 대해 어떤 독자들은 왜 SHDocVw.dll을 참조한 RCW(Runtime Callable Wrapper)를 사용하지 않느냐고 말할지도 모르겠다.
이론상으로는 분명 SHDocVw.dll을 참조 추가하는 것으로 끝나야 정상이다. 그러나 현실은 그렇지 않다. 앞에서 IWebBrowser2를 IServiceProvider처럼 인터페이스 정의로 하지 않고 직접 SHDocVw.dll을 참조해서 형변환하여 쓰게 되면 스마트 클라이언트가 실행시에 오류를 발생하고 만다. 필자는 그런 이해할 수 없는 현상 앞에서 많은 시간을 소비했고, 결국 찾아낸 것이 C# 인터페이스를 별도로 정의하는 것이었다. 참고로 IWebBrowser2 인터페이스와 DWebBrowserEvents 인터페이스 정의는 ‘이달의 디스켓’으로 제공하니 참조하기 바란다.

인터넷 익스플로러와 연동하기 -3

정성태

프린트 이메일 전송

DWebBrowserEvents 이벤트 연결
결국 우여곡절 끝에 IWebBrowser2 인터페이스까지 얻게 되었다. 필자가 C#으로 새롭게 정의한 IWebBrowser2 인터페이스로 형변환된 개체는 SHDocVw.IWebBrowser2로 형변환한 것과 동일하게 사용할 수 있다. SHDocVw.dll을 사용하지 않은 관계로 우리가 직접 구현한 C# IWebBrowser2 인터페이스에는 한 가지 문제를 지니고 있는데, 웹 브라우저에 이벤트를 연결할 수 있는 방법이 모호하다는 것이다. SHDocVw.dll 참조에서는 적절한 RCW 개체가 tlbimp.exe에 의해 ‘Interop.SHDocVw.dll’로 제공되므로 그걸 사용하면 문제가 없었지만, 스마트 클라이언트에서만큼은 SHDocVw.dll을 참조하여 구현하게 되면 오류가 발생하므로 그런 서비스는 받을 수가 없는 상태이다.
여기서 예상할 수 있겠지만 tlbimp.exe가 산출해 준 ‘Interop. SHDocVw.dll’대로 구현을 한다면 자연스럽게 이벤트를 delegate로 연결할 수 있을 것이다. 하지만 아쉽게도 필자 나름대로 잠시 시간을 내어 ‘Interop.SHDocVw.dll’과 맞추려는 시도를 했지만 성공하진 못했다. 독자들 중에서 이에 대해 시도할 계획이 있다면 참고자료 ③을 참고하기 바란다. 필자 역시 언젠가는 재도전을 염두에 두고, 중간 단계의 시도를 TreeEvent.cs에 실어 놓았는데 지면 제약상 ‘이달의 디스켓’으로 제공하니 그 코드로부터 시작한다면 수고가 줄어들 것이다.
그건 그렇고 닷넷에서 제공해 주는 RCW를 사용할 수 없다고 해서 구현이 불가능한 것은 아니다. 역시 unmanaged 시절에 익힌 것처럼 COM 이벤트 방식으로 얼마든지 구현이 가능하다. 잠시 COM에 낯선 독자들을 위해 COM 이벤트 모델을 살펴보고 지나가겠다. <그림 1>을 보면서 다음의 설명을 이해하기 바란다.

<그림1>COM에서 이벤트 구현

보는 바와 같이 COM 개체는 이벤트를 발생하는 인터페이스를 구현한 다음 그 이벤트 인터페이스를 나열할 수 있는 IConnectionPoint Container를 구현해야 한다. 클라이언트 측에서는 바로 그 IConnectionPointContainer 인터페이스를 QueryInterface로 조회하게 되고, FindConnectionPoint 메쏘드를 통해 연결하고자 하는 이벤트 인터페이스에 해당하는 GUID를 넘겨주어 그 이벤트 인터페이스에 대한 연결 지점을 담당하는 IConnectionPoint 인터페이스를 또 다시 구해 올 수 있다. 클라이언트 측에서는 이벤트 인터페이스를 상속받은 클래스를 정의하고 그 클래스의 인스턴스를 만든 다음 IConnectionPoint::Advise에 인스턴스 포인터를 넘겨주면 이른바 ‘이벤트를 거는(sink)’ 작업이 끝나게 된다. 이후로는 COM 서버에서 이벤트가 발생하면 Advise에 전달되었던 클라이언트 측에 구현된 클래스의 포인터를 통해 해당 메쏘드를 호출해 주는 것이 바로 COM 이벤트 구조이다. 한마디로 COM 이벤트는 ‘양방향 통신’을 하게 된다.
앞의 예를 스마트 클라이언트 예제와 비교해 보자. <그림 1>에서 ‘클라이언트’는 우리가 구현한 TreeControl이 될 것이고, ‘Connectable Object’는 웹 브라우저가 될 것이다. 그러면 웹 브라우저로부터 IConnectionPointContainer 인터페이스를 조회한 후 그 인터페이스에서 제공해 주는 FindConnectionPoint 메쏘드를 사용해 웹 브라우저가 구현한 ‘DWebBrowserEvents2’ 인터페이스에 대한 IConnectionPoint 인터페이스를 구할 수 있다. 이후 이벤트를 걸고 싶을 때는 IConnectionPoint::Advise 메쏘드를 호출하면 되고, 이벤트 해제를 하고자 할 때는 IConnectionPoint::Unadvise 메쏘드를 호출해 주면 되는 것이다.
이를 C# 코드로 표현하면 다음과 같은데, 보는 것처럼 간단하게 이벤트를 연결할 수가 있다. 참고로 IConnectionPointContainer 인터페이스는 IServiceProvider와는 달리 닷넷 BCL(Base Class Library) 차원에서 UCOMIConnectionPointContainer라는 인터페이스로 제공되고 있으며, IConnectionPoint는 UCOMIConnection Point라는 이름으로 동일한 구조의 메쏘드까지 포함해 제공하기 때문에 IServiceProvider처럼 개발자가 구현해 줄 필요가 없다.

// DWebBrowserEvents에 대한 GUID 값
Guid DEventsGuid = new Guid( “34A715A0-6587-11D0-924A-0020AFC7AC4D” );
// WebBrowser 개체를 IConnecitonPointContainer로 형변환
UCOMIConnectionPointContainer ICpc = (UCOMIConnectionPointContainer)WBObject;

// DWebBrowserEvents2 인터페이스에 대한 IConnectionPoint 인터페이스 포인터를 반환
ICpc.FindConnectionPoint( ref DEventsGuid, out _ICp );

// TreeControl에서 DWebBrowserEvents2 인터페이스를 구현하고 있기 때문에
// 자신의 this 포인터를 전달하고, 이후 이벤트 연결 해제를 위해 쿠키 값을 반환
_ICp.Advise( this, out _dwCookie );

앞의 코드에서는 지면상 생략했지만 TreeControl 개체는 이미 DWebBrowser2 인터페이스를 상속받아서 그에 대한 멤버 함수를 모두 구현해 놓았기 때문에 마지막 부분의 _ICp.Advise 메쏘드에서 자신의 this 포인터를 전달하는 것이 가능하다. 이렇게 이벤트를 걸어주고 나면 이후로 웹 브라우저로부터 이벤트가 발생할 때마다 DWebBrowserEvents2 인터페이스를 상속받아 구현해 두었던 TreeControl 클래스의 멤버 메쏘드가 실행된다. 우리는 그저 필요한 이벤트에 한해 해당 메쏘드에 코드를 추가해 주기만 하면 된다.
이제 웬만큼 구현이 끝난 것 같다. 여기서 잠시 COM을 하지 않은 독자라면 간과하고 지나갈 수 있을 문제를 짚고 넘어가겠다. 앞에서 QueryInterface로 구한 인터페이스 포인터 값은 알려진 대로 내부적으로 IUnknown::AddRef 가 호출되므로 증가된 참조수를 줄여주기 위해 반드시 Marshal.Release를 호출해 줘야만 하고, 걸어 놓은 이벤트에 대해서도 Unadvise로 해제를 해줘야 한다. 스마트 클라이언트에서 자원 해제를 위한 가장 적절한 위치는 IDisposable.Dispose 메쏘드 내부이다. 그곳에서 _ICp, _htmlDocument, _webBr 개체에 대한 해제 코드를 잊지 말고 넣어주고, 해제와 관련된 코드도 ‘이달의 디스켓’으로 제공하니 확인하기 바란다.
여기서는 스마트 클라이언트를 호스트하고 있는 HTML 문서에 대한 인터페이스 포인터, WebBrowser에 대한 인터페이스 포인터, 그리고 그 WebBrowser에 이벤트를 연결하는 방법을 구현해 보았다. Microsoft.mshtml 개체로의 형변환을 Full Trust 보안에서만 제공해 준다는 것이 다소 원칙에 맞지 않는 듯 싶은데 애석하게도 이 문제는 닷넷 프레임워크 1.1에서만 발생하는 것은 아니다. 필자가 테스트하고 있는 SQL 서버 차기 버전인 ‘유콘’과 함께 설치되는 ‘닷넷 프레임워크 2.0’ 버전에서도 앞의 문제는 동일하게 발생하고 있는 것을 확인할 수 있었다. 물론 닷넷 프레임워크 2.0도 아직 정식 릴리즈가 된 것은 아니기 때문에 확정지을 수는 없겠지만 현재로서는 기대할 수 없는 상황이다.

인터넷 익스플로러와 연동하기-4

정성태

프린트 이메일 전송

스마트 클라이언트와 IE의 세션 공유
스마트 클라이언트를 실무에 적용시키다 보니 IE와 연동을 성공하게 된 것이 IE와 세션 공유에도 도움이 될 수 있었다. 아마 현업에서 직접 스마트 클라이언트를 개발해 본 독자들은 꼭 한번쯤 걸려 넘어졌을 돌뿌리가 바로 IE와 세션 공유가 아닐까 싶은데, 지난 호의 기사만 읽고도 이미 IE와 세션 공유를 할 수 있는 방법을 안 것이나 다름없다. 왜냐면 웹 브라우저에 할당된 세션을 구분해 주는 고유한 ‘세션 ID’는 HTML 스크립트에서도 구할 수 있고, 그렇게 구한 세션 ID를 지난 호에서 배운 ‘속성’ 값에 대입해 내부적으로 보관하고 있다가 필요한 경우 그 값을 쓰면 되기 때문이다. 실제로 서버와 맺어진 세션을 구분해 주는 세션 ID는 쿠키를 통해 서버에서 전달받은 상태이고, HTML 문서는 cookie 속성을 통해 접근을 허용하고 있기에 지난 호에서 배운 실력을 발휘해 본다면 다음과 같은 코드로 아주 쉽게 구현할 수 있다.

Form1._Control1.CookieValue = document.cookie;

하지만 세션과 연동해야 하는 모든 스마트 클라이언트를 사용하는 웹 페이지에서 앞의 구문을 유지해 주는 것도 하나의 일이다. 게다가 이번 호를 통해 그러한 번거로움을 해결할 수 있는 열쇠를 갖게 되었다. 즉 앞의 단계에서 구했던 _htmlDocument 개체에서 cookie 속성을 통해 현재 스마트 클라이언트를 호스팅하고 있는 IE의 쿠키 값을 구해서 사용하면 되기 때문이다. 이미 앞에서 배운 것처럼 쿠키 값은 다음과 같은 코드로 간단히 구할 수 있다.

Type t = _htmlDocument.GetType();
string cookieValue = (string)t.InvokeMember( “cookie”, BindingFlags.GetProperty,
null, _htmlDocument, null );

이렇게 간단히 cookieValue 값은 구했고, 그 쿠키를 설정해 서버와 통신하면 된다. unmanaged 환경에서는 서버와 통신하기 위해 보통 IXMLHTTP 개체를 사용했겠지만 닷넷 프레임워크에서는 BCL 차원에서 제공되는 WebRequest 클래스를 사용하는 것이 더욱 자연스럽다. 사용 방법에 있어서도 IXMLHTTP 개체 못지않게 쉬운 구조를 유지해 주고 있으니 필자는 WebRequest를 선택하여 다음과 같이 구현해 보았다.

// URL에서 호스트 도메인 부분을 구한다.
string currentURL = _webBr.LocationURL;
Uri curURI = new Uri( currentURL );

// Query를 날릴 aspx 경로를 설정
string reqURL = “http://” + curURI.Host + “/WebApp/GetSessionValue.aspx”;
// Factory 메쏘드 패턴으로 WebRequest 개체를 생성
WebRequest webReq = WebRequest.Create( reqURL );

// 웹 브라우저와 세션 공유를 위해 쿠키 값을 WebRequest의 HTTP 요청 헤더에 설정
webReq.Headers.Add( “Cookie”, cookieValue );

// 요청을 보내고, 받은 byte 스트림을 String으로 변환해 출력
WebResponse response = webReq.GetResponse();
Stream stream = response.GetResponseStream();
byte [] byteText = new byte[ response.ContentLength ];
stream.Read( byteText, 0, (int)response.ContentLength );

string text = Encoding.Default.GetString( byteText );
MessageBox.Show( text );

앞에서는 서버에 이미 GetSessionValue.aspx 웹 폼 페이지를 만들어 두었다고 가정하고 있으며, 그 페이지에서는 다른 페이지에서 설정된 세션 변수 값을 출력해 주는 역할을 하고 있다. 기억해야 할 것은 서버와 새롭게 연결을 맺는 클라이언트는 HTTP 요청 헤더에 세션을 공유하고자 하는 연결에 해당하는 세션 ID를 쿠키에 실어서 보내야 한다는 것이다. WebRequest의 경우 헤더를 추가하기 위해 Headers라는 NameVauleCollection 형식의 공용 속성을 제공하므로 기존 웹 브라우저에서 구한 쿠키 값에서 (ASP.NET의 경우) ASP.NET_SessionId 키 값을 동일하게 설정해 보내면 된다. 앞의 코드와 같이 HTTP 요청을 하게 되면 다음과 같은 구조로 HTTP 요청 헤더가 구성되게 된다.

GET /WebApp/GetSessionValue.aspx HTTP/1.1
Connection: Keep-Alive
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: ko,en-us;q=0.5
Cookie: ASP.NET_SessionId=ucrave3mxye3ltqojrbnr455
Host: localhost
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0;
Windows NT 5.2; i-NavFourF; .NET CLR 1.1.4322; .NET CLR 1.0.3705)

보는 것처럼 굵게 표시된 ASP.NET_SessionId가 기존 연결에서 구한 값이기 때문에 서버는 새로운 세션을 생성하지 않고, 이미 맺어진 세션 중에서 주어진 세션 ID에 해당하는 세션 개체를 그대로 사용하게 되어 스마트 클라이언트 내부에서 생성한 WebRequest 개체와 스마트 클라이언트를 호스팅하고 있는 웹 브라우저간에 동일한 세션을 사용하게 된다.

스마트 클라이언트와 COM
지난 호에서는 약간의 COM 지식이면 충분했었는데, 이번 호에서는 COM으로 도배(?)를 한 것 같다. 얼핏 보면 이것이 스마트 클라이언트에 대한 기사인지 COM에 대한 기사인지 구분이 안 갈 정도이다. 그렇다고 해서 COM 지식을 모르고는 스마트 클라이언트를 접근할 수 없다는 인식은 하지 말기 바란다. 본래의 닷넷 프레임워크에서 제공하는 BCL로도 스마트 클라이언트의 위력은 충분히 발휘할 수 있지만, 단지 필자의 연재에서는 기존 액티브X에서 구현했던 것들을 스마트 클라이언트에서도 구현하려다 보니 COM에 대한 배경을 제외시키기가 불가능했을 뿐이다.
그나마 위안을 삼는다면 여기서 다루는 코드들의 대부분이 모든 스마트 클라이언트를 구현하는 데에 있어 변경없이 쓸 수 있기 때문에 별도의 클래스로 구현해 두어 스마트 클라이언트를 구현할 때마다 상속을 통해 처리하면 된다는 것이다.
이로써 스마트 클라이언트를 제작함에 있어서 기존 액티브X를 대체할 수 있을 정도의 기반 지식은 다룬 것 같다. 하지만 역시 스마트 클라이언트의 목표는 사용자들의 컴퓨터에서 쉽게 운영될 수 있을 때 더욱 가치가 있을 것이다. 다음 호에서는 이를 위한 스마트 클라이언트 배포 문제를 다루고 그와 관련된 보안 사항을 다뤄보도록 하겠다.