SimpleIsBest.NET

유경상의 닷넷 블로그

Bug Fixed - 스마트 클라이언트 탭키 오류

by 블로그쥔장 | 작성일자: 2005-11-16 오후 3:59:00
이 글은 오래된 전에 작성된 글입니다. 따라서 최신 버전의 기술에 알맞지 않거나 오류를 유발할 수 있습니다. 저자는 이 글에 대한 질문을 받지 않을 것입니다. 하지만 이 글이 리뉴얼 되면 이 글에 대한 질문을 하거나 토론을 할 수도 있습니다.
지난해 그리고 올해 초에 필자를 괴롭혔던 닷넷 프레임워크의 버그가 2.0에서야 수정되었습니다. 버그의 내용은 스마트 클라이언트 상황에서 탭키를 누르면 포커스가 다음 컨트롤로 이동하지 않고 브라우저의 주소 입력창으로 이동해 버리는 괴이한 현상입니다. 이 버그로 인해 필자는 탭키 오류를 수정하기 위해 기존 컨트롤을 상속 받고 탭키 입력을 가로채서 강제로 포커스를 이동시키는 코드를 각 컨트롤마다(탭키 오류를 일으키는 컨트롤들에 대해 전부) 삽입했던 아픈 기억이 납니다. 이제 2.0 에서 이 버그가 수정되어 스마트 클라이언트를 적용할 때 문제로 제기 되었던 탭키 문제를 해결할 수 있게 되었습니다.

Smart Client Bug-Fixed: Tab Key Processing

지금부터 설명할 내용은 스마트 클라이언트에 대한 닷넷 프레임워크 1.x의 버그에 대한 내용이며 이것이 2.0에서 수정되었다는 내용을 다룰 것이다. 이걸 설명하자면 먼저 스마트 클라이언트의 용어에 대해 짚고 넘어가야 하는데... 이게 이야기가 길어질 것 같다는 예감이 귀언저리 살, 후두부, 대퇴부를 팍 걷어 차는 것이다. 대충 스마트 클라이언트란 것은 닷넷 윈폼의 사용자 정의 컨트롤(UserControl 클래스 기반)을 브라우저 안에 ActiveX 처럼 임베딩(embedding) 시켜서 사용하거나, 닷넷으로 작성한 어셈블리(.exe)에 대한 URL을 클릭(바탕 화면이나 브라우저에 표시된 링크)하면 해당 어셈블리가 수행되는 상황을 말한다. 스마트 클라이언트란 용어는 일반적인 용어로서 윈폼 어플리케이션과 같이 강력한 UI를 갖는 클라이언트 들을 통칭하는 말이다. 사실 닷넷 스마트 클라이언트라는 것도 마이크로소프트의 표준 용어가 아닌 우리나라에서나 통용되는 용어이며, 마이크로소프트의 정확한 명칭은 닷넷의 기능으로서 NTD(No-Touch Deployment)를 이야기 하는 것이다. 난중에 시간을 내서 NTD에 대해서 좀 더 자세히 살펴볼 것이니 대충 이 정도로만 요약하고 넘어가자. (사실... 대략 귀찮다... -_-; )

스마트 클라이언트는 웹 어플리케이션 처럼 배포가 용이하지만 HTML 기반의 웹 어플리케이션이 갖는 UI의 한계를 극복하고자 최근 주목 받는 X-Internet(eXecutable Internet)의 한 기술이다. 그래서 웹 브라우저(사실 IE 가 아니면 불가능하다) 내에 닷넷으로 개발한 사용자 정의 컨트롤(UserControl 클래스 기반)을 표시함으로써 닷넷 어셈블리를 웹 서버로부터 자동으로 다운로드 받고, 강력한 윈폼 UI을 사용할 수 있도록 하는 것이다.

Interface with IE

닷넷 사용자 정의 컨트롤이 브라우저 내에 임베딩 될 때 브라우저가 이 닷넷 컨트롤을 특별하게 처리하는 것은 아니다. 브라우저의 입장에서 닷넷 컨트롤은 단순한 ActiveX 컨트롤과 전혀 다를 바가 없다. 이는 곧, 닷넷 컨트롤이 ActiveX 컨트롤로서 작동할 수 있는 능력이 있음을 의미한다. 어찌 되었건 브라우저는 스마트 클라이언트 시나리오에서의 닷넷 컨트롤을 ActiveX 처럼 취급한다는 점이 중요하다.

브라우저는 스마트 클라이언트 컨트롤을 ActiveX 처럼 취급하므로 키보드 처리 역시 ActiveX와 동일한 방식으로 처리한다. 브라우저와 ActiveX가 키보드 입력을 처리하는 방식은 OLE 스펙에 명시한 바대로 컨트롤 및 컨트롤의 자식 윈도우에 대한 키보드 입력이 있는 경우, 브라우저는 해당 키 메시지(Windows 메시지, WM_KEYDOWN을 의미한다)를 컨트롤에게 먼저 보내어 처리하도록 요구한다. 이때 컨트롤이 해당 키를 처리하면 브라우저는 아무런 작업을 하지 않는다. 만약 컨트롤이 해당 키를 처리하지 않고 반환하는 경우에는 브라우저가 해당 키에 대한 처리를 수행하는 것이다. (상세한 처리 방식 및 COM 인터페이스에 대해서는 언급하지 않겠다. 대략 졸라 빡시게 복잡타...)

전통적으로 지금까지 이러한 처리 방식을 제대로 준수하지 않는 ActiveX 컨트롤들은 탭키, 엔터키 등과 같이 브라우저가 스스로 처리하는 키에 대해 문제점을 갖고 있었다. 예를 들어, 사용자가 탭키를 누르면 브라우저는 먼저 ActiveX 컨트롤에게 키를 처리하라고 지시한다. 이 때 ActiveX 컨트롤이 탭키 처리를 제대로 하지 않으면, 브라우저는 해당 키를 자신이 알아서 처리한다. ActiveX 컨트롤이 텍스트 박스와 같이 키보드 입력을 필요로 하는 자식 컨트롤(윈도우)를 여러 개 갖는 복합 컨트롤인 경우 문제는 심각해 진다. ActiveX 컨트롤이 탭키를 스스로 처리하여 ActiveX 컨트롤내에서 포커스가 이동하도록 처리하지 않으면, 브라우저가 ActiveX 컨트롤을 하나의 통짜 컨트롤로 간주하고 탭키를 처리해 버리기 때문에 포커스가 예상대로 움직이지 않고, 브라우저의 주소 입력창 같은 곳으로 슥 이동하는 현상이 발생하곤 하는 것이다.

The Bug : Key processing

지금부터 설명할 버그의 상세 내용은 상당히 복잡하게 느껴질 것이다. 당연하다. 필자도 Reflector를 이용해서 System.Windows.Forms 네임스페이스를 졸라 까본 결과 겨우 겨우 알아낸 것이니까... 뭐 이해가 안가도 괜찮다. 그냥 이런 버그가 있다는 것을 알아두기만 해도 좋다. 아니, 머리가 아플 것 같으면 건너 뛰어도 좋다.

닷넷 프레임워크 1.1의 System.Windows.Forms 네임스페이스의 Control 클래스는 모든 윈폼 UI 컨트롤(Form 클래스를 포함하여)에 대한 부모 클래스이다. 앞서 닷넷의 윈폼 컨트롤들이 ActiveX 컨트롤 역할을 할 수 있다고 했었는데, 바로 Control 클래스가 ActiveX 컨트롤에 대한 구현을 포함하고 있다. Control 클래스 내부에는 internal 클래스로서 ActiveXImpl 이란 클래스가 있는데, 이 클래스다 ActiveX 컨트롤이 구현해야 할 COM 인터페이스들을 구현하고 있다. ActiveXImpl 클래스에 TranslateAccelerator 메쏘드가 바로 브라우저가 포워드 해주는 키보드 처리를 담당하는 메쏘드인데, 이 메쏘드가 버그를 갖고 있는 것이다.

버그의 내용을 보면 이렇다. 키보드 입력이 TranslateAccelerator 메쏘드에 도착하면 Control 클래스는 키보드 입력을 해당 컨트롤의 입력 키인가 살핀다. 이 과정은 컨트롤의 protected virtual 메쏘드인 IsInputKey 메쏘드를 호출하여 이 메쏘드가 true를 반환하면 입력키로 인식한다. 윈폼에서 입력 키라 함은 키 입력 자체를 특별한 키가 아닌 일반 문자처럼 취급한다는 것을 의미한다. 예를 들어 텍스트 박스 컨트롤에서 탭키는 두 가지 의미를 갖을 수 있다. 텍스트 박스에서 탭키가 눌려졌을 때 다음 컨트롤로 포커스를 이동하는 것과 텍스트 박스에 탭키 자체를 문자로 입력 받는 것 말이다. 이 중 후자가 탭키를 입력 키로 인식하는 것이다. 실제로 텍스트 박스의 속성 중 AcceptsTab 이 true 이면 탭키를 포커스 이동이 아닌 탭 문자(ASCII 코드 0x9)로 받아들인다는 것이다.

2005-11-17 필자주)
IsInputKey() 에서 직접 탭키를 처리하는 것은 아니다. 보다 정확히 말하자면 PreProcessMessage 메쏘드가 IsInputKey()를 호출하여 이것이 입력 키인가 확인하고 만약 입력 키라면 아무런 작업도 하지 않는다. 만약 입력 키가 아니라면 ProcessDialogKey() 메쏘드를 호출하여 탭키, 커서키에 의해 포커스가 이동하게 된다.

TranslateAccelerator 메쏘드의 구현 역시 PreProcessMessage 메쏘드를 호출하며 이 메쏘드에서 호출되는 IsInputKey() 호출의 결과에 따라 탭키가 처리되거나 안 되는 것이다.

핵심은 IsInputKey 의 구현에 달려있다. Control 클래스의 IsInputKey 메쏘드의 기본 구현은 입력된 키가 탭키이거나 상/하/좌/우 커서 키인 경우 이를 처리하여 포커스가 이동하도록 구현되어 있으므로 별 문제가 없다. 하지만 IsInputKey는 protected virtual 메쏘드로서 Control 클래스에서 상속받은 클래스들은 이 메쏘드를 오버라이드 할 수 있다. TextBox 클래스 역시 Control 에서 파생된 클래스로 IsInputKey 메쏘드를 오버라이드 하고 있으며, TextBox 클래스의 IsInputKey 구현은 MultiLine 속성과 AcceptsTab 속성이 True 이면 아무런 짓도 하지 않으며 입력된 키가 입력 키임을 알려 버린다. 이 경우, 일반 윈폼 상황에서는 컨트롤을 포함하는 폼과 메시지 루프가 키보드 메시지를 다시 발생해주므로 문제가 없다. 하지만, 브라우저 내에 ActiveX 처럼 임베딩 되는 스마트 클라이언트는 탭키를 브라우저가 처리해 버리고 키보드 메시지를 다시 발생시켜주지 않기 때문에 문제가 되는 것이다. 물론 AcceptsTab 속성이 False 인 경우에는 베이스의 구현, 즉 Control 클래스의 IsInputKey 메쏘드를 호출하므로 역시 문제가 없다. 확인하고 싶은 독자는, Reflector로 System.Windows.Forms.TextBoxBase 클래스(TextBox 클래스의 베이스 클래스 되겠다)의 IsInputKey 메쏘드를 까보기 바란다.

자 이제 정리해서 생각해 보자. TranslateAccelerator 메쏘드가 브라우저로 부터 탭 키를 처리하라고 입력 받았다. 이 메쏘드는 IsInputKey 메쏘드를 호출하여 입력 키인가를 확인하고 탭키 혹은 커서키에 대한 처리를 하고자 한다. 그런데, 상속 받은 클래스가 IsInputKey를 오버라이드하여 별다른 탭키 처리를 안해 주었다면? 탭키는 포커스를 이동하는 역할을 전혀 수행하지 않고 브라우저에게 처리권이 넘겨지게 된다. 이제 브라우저는 입력된 탭키를 자신의 처리 방식대로 처리한다. 포커스를 이동하긴 이동하되, 스마트 클라이언트 외부로 포커스를 이동해 버리게 된다. 대부분의 경우, 포커스는 브라우저의 주소 입력 창으로 이동해 버린다.

이러한 버그는 Control 클래스에 정의된 IsInputKey 메쏘드를 오버라이드하는 컨트롤들에 대해서만 발생한다. 콤보 박스, 리스트 박스 등의 컨트롤은 IsInputKey 메쏘드를 오버라이드 하지 않기 때문에 문제가 없다. 텍스트 박스의 경우도 AcceptsTab 속성을 false로 하는 경우 문제가 없다. 하지만 스마트 클라이언트를 개발할 때 써드 파티 컨트롤들을 많이 사용하는데 많은 써드 파티 컨틀롤들이 IsInputKey 메쏘드를 오버라이드하여 처리한다는 점이다. 대표적인 것인 FlexGrid 컨트롤, Infragistics 의 컨트롤 중 다수가 스마트 클라이언트 시나리오에서 탭키 문제를 유발하곤 한다.

Reproducing the bug

이것이 버그란 것을 주장하기 위해서는 무릇 동일한 현상을 반복할 수 있어야 한다. 그래야 증거로 갖다 댈테니깐... 스마트 클라이언트를 한 번이라도 시도해 본 독자라면 어렵지 않게 버그를 재연(reproduce)할 수 있다.

  • 사용자 정의 컨트롤을 하나 만든다.
  • 사용자 정의 컨트롤에 텍스트 박스와 버튼 등등의 컨트롤을 둔다.
  • 텍스트 박스의 속성에서 MultiLine 속성과 AcceptsTab 속성을 true 로 설정한다.
  • 사용자 정의 컨트롤 DLL을 빌드하고 브라우저에 스마트 클라이언트로 띄운다.
    (필요하다면 CAS 설정을 해준다. 간단한 예제의 경우 CAS 설정이 필요 없다)
  • 텍스트 박스에서 탭키를 눌러보고, 포커스가 어떻게 이동하는가 본다.

Solution Or Workaround

이 버그를 해결하는 방법은 닷넷 프레임워크 2.0을 사용하는 것이다. 닷넷 프레임워크 2.0의 TranslateAccelerator 메쏘드의 구현을 살펴보면 새로운 처리 방식(PreProcessControlMessageInternal 이란 새로운 internal 메쏘드 추가)을 통해 탭키 처리를 수행하고 있다. 탭키가 눌려지면 이 키보드 입력이 아무런 처리 없이 브라우저에게 넘어가는 일이 발생하지 않으므로  문제가 없다.

하지만 2.0을 채용한다는 것은 상당히 부담스러운 일이며, 모든 클라이언트에게 닷넷 프레임워크를 설치해야 하는 스마트 클라이언트 시나리오에서는 더욱 더 그러하다. 1.1 을 사용하면서 이 문제를 피해가는 방법은 탭 키 문제를 발생하는 컨트롤을 사용하지 않는 방법(쫌 무책임한 방법이다... -_-)이 있다. 그러나 미려한 UI를 좋아하는 우리나라의 사용자들, 이거 절대루 안 통한다. 닷넷 프레임워크에 기본으로 포함된 데이터그리드를 쓰라고 하면 아마 개발자를 잡아 먹으려 들 것이다.

또 한가지 방법은 문제를 일으키는 각 컨트롤을 다시 상속 받아 IsInputKey를 재정의 하여 탭키를 처리해 주는 방법이다. 앞서 TextBox 클래스의 경우 TextBox에서 상속한 TextBox2 클래스를 만들고 IsInputKey 를 재정의하여 탭키 입력이 들어오면 처리하는 것을 생각해 볼 수 있다. 하지만 이 방법은 필자의 경우 제대로 작동하지 않았는데, TextBox야 그렇게 처리가 되지만 컨트롤 내부에 또 컨트롤(예를 들어 FlexGrid)이 존재하고 이것이 탭키 문제를 유발하는 경우는 매우 복잡한 처리가 필요했었다. 설명하기 매우 복잡하고, 간단히 말하면 Reflection을 써서 내부 클래스까지 상속받고 IsInputKey 메쏘드가 아닌 PreProcessMessage 메쏘드를 오버라이드하여 탭키를 강제로 처리해 주었었다. 구체적인 방법까지 설명하긴 너무 복잡하므로 더 이상 설명하지 않겠다(코드는 몇 줄 안되지만 내용은 상당히 복잡함).

문제를 일으키는 컨트롤을 상속하여 IsInputKey 메쏘드를 오버라이드하건, PreProcessMessage 메쏘드를 오버라이드하건 강제로 탭키를 처리하는 것은 일반적으로 버그를 피해가는 방법이 아니라, 케이스 마다 다른 코드를 요구한다는 점이 문제이다. 즉, TextBox 는 TextBox 대로 처리가 다르고, FlexGrid는 FlexGrid 대로 처리 방법이 다르다는 것이며, 처리 방법을 알아내기 위해서는 상당시간 해당 컨트롤의 탭키 처리 방식을 Reflector로 까봐야 알 수 있다는 난해한 점이 있다. FlexGrid 같은 컨트롤은 Reflector로 까보기도 힘든데, 쓰바 난독(ofuscate) 처리가 되어 있기도 하다. 또한 어떤 컨트롤은 아예 탭키 문제 해결이 불가능한 경우도 있다. 한마디로 니맘대로 제각각인 줴길슨 인 것이다.

마지막 방법은 1.1에서 버그 패치를 마이크로소프트가 내놓을 때 까지 기다리면 된다. 2.0에서 해결된 것이니 1.1에서도 서비스 팩 혹은 핫 픽스가 나오지 않을까? 아니면? 필자에게 따지지 말자. 안 내놓으면 내놓을 때까지 마이크로소프트를 달달 볶아보는 것도 한 방법이긴 하겠다. 하지만 철수를 운운하는 마이크로소프트에게 한국 사용자의 요구사항이 얼마나 먹힐지는 미지수이다. -_-;

Conclusion

뭐, 2.0에서 해결되서 다행이지, 2.0 베타 2 까지도 이 문제가 해결되지 않아 필자는 씨껍 했었다. 다음 스마트 클라이언트 프로젝트에서도 이 문제를 안고 가야 하는가 하는 걱정 때문이다. 다행이도 이 문제가 2.0에서 해결되었으므로 1.1에서도 패치가 나올 것이라고 필자는 확신하는 바이다. 그때 까지는 고객을 잘 설득하던지, 필자 처럼 Reflector 를 가지고 졸라게 삽질하던지 둘 중 하나를 선택해야 할 것이다. 아~웅~



Comments (read-only)
#re: Bug Fixed - 스마트 클라이언트 탭키 오류 / 장현희 / 2005-11-17 오전 10:02:00
아... 저도 한 때 이 문제로 고민하다가 Reflector의 도움으로 비슷하게 해결한 적이 있었습니다.
그 당시 한 백만번은 삽질했던 기억이...ㅠㅠ

늘 좋은 글 잘 읽고 갑니다.
추운 날씨에 감기 조심하세요 ^^
#re: Bug Fixed - 스마트 클라이언트 탭키 오류 / 블로그쥔장 / 2005-11-17 오전 10:29:00
스마트 클라이언트로 프로젝트 경험이 있으신가 보지요?
이런 문제들은 같이 공유되어야 한다고 생각됩니다... 그래야 원인도 모르고 욕만 얻어듣는 개발자가 없겠지요?
이런 버그를 아직까지 수정하지 않는 마이크로소프트는 반성해야 합니당...
불쌍한 개발자들... T_T