2012년 6월 21일 목요일

Django에서 온라인 카드결제 구현하기

APSys 2012 학회 웹사이트를 만들면서, 학회 등록비를 받기 위해 온라인 결제를 구현하기로 하였다. 내가 있는 연구실에서 예전에 PAM 2009 학회를 열었을 때는 국내 등록자는 정보과학회에서 제공하는 등록 페이지를 이용하고 해외 등록자는 이메일이나 FAX로 등록 요청을 받아 카드 정보를 기록해두었다가 현장에서 카드 정보를 대조하여 결제를 진행하는 방식을 사용했다. 문제는 아무래도 민감한 카드 정보를 온라인으로 주고받다보니 (요즘이야 대부분의 이메일 서버가 SSL/TLS 연결을 기본으로 쓰기는 하지만) 좀 꺼림칙하다는 점, 또 카드 정보를 받는 쪽도 전달 자체가 안전한지 여부와 상관없이 민감한 개인정보를 직접 들고 있다는 것에 대한 부담감이 있게 마련이다.

이번에는 그래서 결제대행사를 거치기로 하였다. 정보과학회하고 얘기해보니 올앳페이라는 결제대행사와 계약이 되어있고 얘네들이 해외카드 결제를 지원한다고 하여 직접 온라인 등록페이지를 만들기로 하였다. 내가 개발에 조금 시간을 써야 하는 일이긴 하지만, 전체적으로 봤을 때 등록비 정산에 관한 업무량을 확 줄일 수 있을 것이라 예상했기 때문이고 나 또한 실제 온라인 결제가 어떤 방식으로 구현되는지 기술적인 호기심이 있기 때문이기도 했다.

올앳페이에서 제공한 매뉴얼을 읽어보니 인터넷 상에서의 온라인 쇼핑은 크게 다음과 같은 과정으로 이루어진다는 것을 알 수 있었다.
  1. 사용자가 물건을 구매하겠다는 의사를 표시함. 즉 "주문"이라는 형태로 쇼핑몰 서버의 DB 또는 사용자 session에 주문할 물품에 대한 정보가 생긴다.
  2. 구매하는 과정으로 들어가면, 쇼핑몰은 배송 등을 위한 주소 등의 추가 정보를 입력받는다.
  3. 결제 방법을 선택하면 각 결제수단에 따라 적절한 인증절차를 거친다. (client side)
    예) ISP 안전결제 등
  4. 결제하기 버튼을 누르면 쇼핑몰 서버로 결제 내용이 암호화되어 전달된다. 쇼핑몰 서버는 주문 정보를 확인하여 금액이 맞게 들어왔는지 한번 더 검증하고, 이를 결제대행사 서버로 보내어 결과를 리턴받는다. (server side)
  5. 받은 결과를 보고 결제의 성공/실패 여부를 확인하고 적절한 후처리를 한다.
이 중에서 결제대행사가 제공하는 부분은 3단계의 client side 구현체와 4단계에서 결제대행사 서버로 데이터를 주고받는 API이다. 이 두 단계에서는 다음과 같이 보안에 대한 고려가 필요하다.
  • 결제 인증 과정에서 신용카드 번호와 비밀번호 등 결제하는 데 필요한 정보가 쇼핑몰 서버로 노출되지 않아야 한다. 이는 결제대행사 측에서 제공한 client side 플러그인이 구현해준다.
  • 쇼핑몰 서버에서 결제대행사 서버로 가는 정보는 암호화되어야 한다. 이는 결제대행사에서 SSL 연결을 제공함으로써 해결 가능하다.
국내에서 대부분의 온라인 쇼핑을 Microsoft의 Internet Explorer로만 가능하게 만드는 주범은 바로 3단계의 client side 구현체로, 이것이 대부분 ActiveX 플러그인 형태로만 제공된다. (경우에 따라 키보드 보안 프로그램 등이 설치되기도 한다. ㅠㅠ) 쇼핑몰 서버에만 노출시키지 않는 것이 아니라, 사용자 PC의 다른 프로그램들에도 노출시키지 않고자 하는, 즉 사용자 PC의 보안을 쇼핑몰/결제대행사 쪽에서 지켜주어야 하는 국내 인식과 관련 제도 때문이다. 또한 결제대행사 자체 플러그인뿐만 아니라 카드 인증 등에 사용하는 ISP 인증 플러그인 등도 기본적으로 Windows binary로만 배포되는 것 같은데 이니시스에 제공하는 오픈웹 결제는 이 부분을 자체 구현하는 식으로 해결한 것 같다. 나머지 단계는 쇼핑몰 제작자의 의지에 따라 얼마든지 크로스브라우징 환경으로 구축할 수 있다.

쇼핑몰 측에서는 결제대행사와의 계약 내용에 따라, 신용카드만 지원할 수도 있고 휴대폰 결제나 무통장 입금 등의 좀더 다양한 결제 수단을 지원할 수도 있는데, 어쨌든 이러한 결제 수단에 대한 인증 과정은 모두 결제대행사의 플러그인에서 처리되고 여기에 쇼핑몰이 개입할 여지는 없다. (어떤 수단을 선택할지 고르는 화면을 쇼핑몰 쪽에서 제공하게 만들 수는 있다.) 개발자 입장에서는 client side에서 javascript 파일 하나 연결해주고 결제 화면 form submit 핸들러를 결제대행사에서 제공해준 것으로 대체하기만 하면 된다.

내 경우 문제는... 올앳페이에서 제공하는 API 라이브러리 중에 Python 구현이 없었다는 것이다. PHP, JSP, ASP, ASP.NET, C(!)로는 올앳페이 측에서 결제 API 라이브러리를 이미 제공하고 있는데, Python용 코드는 혹시나 했는데 역시나 없더라. ㅠㅠ 아직 Python/Django 플랫폼으로 쇼핑몰을 구축하는 사례가 거의 없다는 뜻일 것이다.

그래서 이걸 PHP를 cli로 실행해서 인자를 넘겨 처리하고 결과값을 받아오는 식으로 짜야 하나 잠시 고민했는데, 막상 API 라이브러리 소스코드를 열어보니 괜히 함수별로 똑같은 내용을 복사붙여넣기 해놔서 길이만 길었지 막상 하는 일은 약간의 고유 오류코드 처리를 포함한 SSL HTTP 요청 날리는 게 전부였다. 그래서 1시간만에 뚝딱 포팅해버리고 그날 중으로 실제로 결제 되는 것까지 확인했다.
(프로그램 반복 테스트 과정에서 거의 백만원 정도를 내 개인카드로 긁었다가 식겁하고 얼른 올앳페이 관리자시스템 들어가서 승인취소를 넣었다...ㅋㅋㅋ 원래 디버깅용 test 플래그가 있어서 이걸 쓰면 실제 결제가 이루어지지 않는데 그걸 깜빡....;;)

국내를 대상으로 하는 보통의 온라인 쇼핑몰이라면 여기까지만 하면 땡인데, 내 경우에는 한 단계가 더 있었다. 국제 워크샵이다보니 해외 참가자들의 등록비도 결제할 수 있게 만들어야 하는 것. 사실 그것이 이 일을 한 주 목적이기도 했고.

여기서 문제는 실제로 해외에서 발급받은 신용카드가 아니면 테스트조차 해볼 수 없다는 것이다. -_-;;; 쇼핑몰에서 어찌할 수 없는 3단계(결제 인증)에서 인증이 안 되기 때문이다. 게다가 test 플래그를 켜도 일단 인증 과정을 진행한 다음 올앳페이 서버를 거칠 때 bypass하는 방식이라서 인증이 안 되면 진행이 불가능한 것은 마찬가지. 국내에서 발급받은 카드들은 카드사에서 발급한 것인지 은행에서 발급한 것인지에 따라 3단계에서 막히는지 4단계에서 막히는지 차이가 좀 있으나 어쨌든 결제가 불가능하다. (국내상점에 국내카드로 해외결제를 한다는 것을 못 받아들이는 듯. 올앳 쪽에 문의하니 국내에서의 해외결제는 모두 외환카드를 통해 처리되는데 여기 서버에서 막고 있는 것 같다.)

결국은 교수님이 해외에 계실 때 발급받았던 '진퉁' 해외카드를 써서야 최종 결제 테스트에 성공할 수 있었고, 그 와중에 있었던 자잘한 프로그래밍 실수나 버그들 때문에 며칠 동안은 수동으로 등록절차를 진행해야 했다.

어쨌든...
온라인 결제 연동도 생각보다 어려운 일은 아니라는 것.

다른 사람들을 위해 github에 올앳페이 결제 라이브러리와 Django 예제 코드들을 공개한다.

2009년 8월 20일 목요일

jQuery와 a 태그를 이용한 button 구현

이미 정찬명님의 텍스트 버튼 구현cross-browsing을 위한 시도들이 있었지만, 가장 호환성이 좋고 무엇보다 자바스크립트 동작 말고 말 그대로 링크를 위해 버튼을 쓰는 경우까지 깔끔하게 커버할 수 있는 a 태그를 이용할 때의 치명적인 단점은 버튼의 enabled/disabled 상태를 바꿀 수 없다는 점이다.

회사에서 작업 중인 웹어플리케이션에서 가상머신 인스턴스들을 관리하기 위한 일종의 웹콘솔 UI를 만드는데, 어떤 버튼은 AJAX를 통해 실행되는 기능이기 때문에 자바스크립트 동작을 연결하지만, 같은 형태로 같은 도구모음 영역 안에 들어가는 다른 버튼은 어떤 다른 페이지로의 링크를 담고 있다거나 한 경우가 있었다. 그리고 현재 설정이나 사용자 조작에 따라 버튼이 활성화/비활성화되어야 할 필요도 있었다. 처음에는 일단 a 태그에 아이콘 표시를 위한 span 하나 넣는 정도로 하였지만, 비활성화 처리도 해야겠고 하다보니 button 태그를 쓰게 되었다.

하지만 이미 여러 곳에서 지적하고 있는 것처럼 button 태그로 완벽하게 cross-browsing을 구현하는 건 거의 불가능에 가까울 뿐더러(하더라도 매우 지저분-_-), 어떤 건 말 그대로 링크를 위한 버튼용으로 a 태그를 쓰고 또 어떤 건 button 태그를 쓰고 이러면서 이 둘이 동시에 하나의 영역 안에서 일관성 있게 표현되도록 하자니까 또 그만한 CSS 노가다질이 없었다. (접근성 측면에서는 의미에 맞게 각각 써주는 것이 좋겠지만.)

그래서 a 태그를 그냥 쓰고, 링크만 한 경우는 링크 동작을 막고 click 이벤트 핸들러를 지정한 경우는 해당 이벤트 핸들러만 잠시 detach하는 식으로 disabled 상태를 구현하기로 마음먹었다. 시각적 효과 면에서 a 태그를 쓰는 것이 disabled 상태는 어차피 클래스 하나 먹여주면 간단히 해결할 수 있는 문제고, :hover 상태 표현도 IE6 같은 옛날 브라우저에서도 잘 먹기 때문에 가장 좋다. 그래서 약간의 검색 끝에 jQuery에서 비공식적으로 jQuery.data(element, 'events')라는 속성에 각 이벤트 이름별로 핸들러들을 배열로 저장하고 있다는 사실을 발견하여 이런 코드를 짜게 되었다.

$.fn.disable = function(options) {
    var defaults = {
        className: 'ui-state-disabled'
    };
    var settings = $.extend({}, defaults, options);
    return this.each(function() {
        if (this.tagName == 'button' || this.tagName == 'input')
            $(this).attr('disabled', 'disabled').addClass(settings.className);
        else {
            var $elem = $(this);
            var current_events = $elem.data('events');
            var removed_events = []; // store all existing click events bound via jQuery
            if (!$elem.data('disabled')) {
                if (current_events) {
                    $.each(current_events.click, function(index, item) {
                        removed_events.push(item);
                    });
                }
                $elem.unbind('click') // unbind all click events
                .bind('click', function(ev) { ev.preventDefault(); })
                .data('removed_events', removed_events)
                .data('disabled', true)
                .addClass(settings.className);
            }
        }
    });
};
$.fn.enable = function(options) {
    var defaults = {
        className: 'ui-state-disabled'
    };
    var settings = $.extend({}, defaults, options);
    return this.each(function() {
        if (this.tagName == 'button' || this.tagName == 'input')
            $(this).removeAttr('disabled').removeClass(settings.className);
        else {
            var $elem= $(this);
            var removed_events = $elem.data('removed_events');
            if ($elem.data('disabled')) {// restore saved click events
                $elem.unbind('click')
                .removeData('removed_events')
                .data('disabled', false)
                .removeClass(settings.className);
                if (removed_events) {
                    $.each(removed_events, function(index, item) {
                        $elem.bind('click', item);
                    });
                }
            }
        }
    });
};

 사용법은 간단히 $(selector).disable(), $(selector).enable() 이러면 된다. 단, 아직 중복해서 disable/enable 하는 경우에 대한 테스트는 해보지 않았으므로 잘 안 될 수 있다. (대충 보기에 아마 disable을 중복하면 기존 이벤트를 잃어버리는 문제가 생길 듯하고, enable은 여러 번 해도 상관 없을 듯하...지만 확인은 직접. ㅋㅋ) 아직 disable하지 않은 것을 enable하는 경우나 disable/enable 반복되는 경우도 모두 처리한 버전으로 업데이트. :)

2008년 10월 6일 월요일

DNS 캐시 디버깅하기

웹 개발이나 서버 관리를 하다 보면 도메인 및 많은 수의 virtual host들을 관리해야 하는 경우가 많은데, 이때 네임서버 설정을 변경해도 바로 반영이 안 되어서 한참 동안 접속이 되는 걸 기다리느라 시간을 허비할 때가 많다. DNS 문제는 인터넷의 발전을 가로막는 장벽이라는 이야기가 있을 정도로 그 갱신 주기와 관리에 관련된 주체들이 많이 얽혀있어 처음 이런 문제를 접하는 사람들의 경우 해결하기가 쉽지 않고, 캐시 갱신으로 인해 갑자기 해결되어도 왜 그런지 이해하지 못하는 경우가 태반이다.

그래서 이 글에서는 DNS 설정을 바꿨을 때 어떻게 하면 빨리 대처할 수 있는지 살펴보기로 한다.

1. 네임서버 자체의 TTL 설정 확인
DNS 관리할 때 사용되는 용어 중 TTL(Time-to-live)이란 것이 있는데, 이것은 네임서버가 해당 도메인에 대한 정보를 얼마나 긴 시간 동안 캐시(임시 보관)하고 있을 것인지 결정한다. 이 값이 길수록 캐시 기간이 길어지므로 새로운 정보로 변경했을 때 그 정보가 적용되기까지 시간이 오래 걸린다. 따라서 테스트용 서버와 같이 자주 변경이 이루어지는 경우 이 값을 비교적 작게 유지하는 것이 좋다.

2. ISP가 제공하는 DNS 서버
회사나 큰 기관에서는 자체 DNS 서버를 이용하기도 하고, 개인사용자들은 KT, 하나로통신, LG파워콤 등 자기가 가입한 인터넷 회선 사업자의 DNS 서버를 이용할 것이다. 네임서버 정보를 수정했다고 해도 이들 서버에 적용되기까지 또 시간이 걸린다. 이것은 TTL 정보뿐만 아니라 각 DNS 서버의 운영 정책 등과도 관련이 있기 때문에 정확히 얼마만큼의 시간이 걸린다고 예측하기는 어려우며 DNS 서버마다 갱신 주기가 달라 어떤 곳에서는 새로 바뀐 주소로 접속이 되고 어떤 곳에서는 예전 주소로 접속되고 이런 현상이 발생하기도 한다.

내가 추천하는 한 가지 방법은 OpenDNS를 이용하는 것인데, 이곳은 캐시 갱신이 비교적 빠른 편이고 갱신이 안 되어 접속에 문제가 생겼을 때 캐시 상태를 확인할 수 있는 페이지로 보내주므로 현재 어떤 상황인지 비교적 투명하게 알 수 있다는 장점이 있다. DNS 서버는 ISP가 제공하지 않는 것을 사용하더라도 보통 별다른 제약이 없기 때문에 이 방법을 가장 추천한다.

3. 자기 컴퓨터의 DNS 캐시
DNS 서버에서도 캐시가 이뤄지지만 각자 사용하는 로컬 컴퓨터의 운영체제에서도 자체적으로 캐시가 이루어진다.

 운영체제 로컬 캐시 갱신 명령 수동 mapping 파일
 MS Windows ipconfig /flushdns {SystemDir}\drivers\etc\hosts
 Linux /etc/init.d/nscd restart /etc/hosts
 MacOS X (10.4 이전) lookupd -flushdns /etc/hosts
 MacOS X (10.5 이후) dnscacheutil -flushdns  /etc/hosts

여기서 주목할 것은 etc/hosts 파일인데(심지어 윈도우도 거의 같은 디렉토리/파일명에 똑같은 포맷을 가지고 있다), 이 파일에 "IP주소 도메인" 형태로 한줄씩 수동 mapping을 추가할 수 있다. 이렇게 하면 실제 인터넷 상의 DNS 서버를 이용하지 않고 해당 IP 주소로 자기 PC 레벨에서 무조건 연결시켜주므로 DNS 서버가 갱신되지 않을 때 재빨리 테스트해보기 위한 용도로 적합하다.

거의 모든 운영체제에서 제공하고 있는 네임서버 테스트 용도의 nslookup 명령 결과에 영향을 주는 부분은 여기까지다.

※ Linux의 경우 배포판마다 캐시 갱신 명령이 다를 수 있음.

4. 웹브라우저 자체의 DNS 캐시
그러나 위의 etc/hosts 파일을 고쳐도 적용되지 않는 경우가 있을 수 있는데, 이것은 웹브라우저 자체가 가지고 있는 캐시 때문이다. -_-; 보통은 웹브라우저를 재시작해도 적용되지만 Firefox처럼 세션을 저장했다가 다시 불러오는 경우 바로 갱신되지 않을 때가 있다.
이럴 땐 웹브라우저의 설정을 직접 건드려주는 방법이 있다.

  • Firefox : about:config에서 network.dnsCacheEntries (캐시 항목 개수), network.dnsCacheExpiration (캐시 기간, 단위: 초) 두 항목을 변경하거나 새로 추가한다. entry 개수가 0이 되면 캐시를 전혀 하지 않지만 복잡한 웹사이트 로딩시 속도가 떨어질 수 있다.
  • Internet Explorer : MSDN 참조

이처럼 살펴본 바와 같이, DNS Cache 문제는 여러 단계에서 영향받을 수 있기 때문에 꼼꼼히 확인해봐야 한다.

2008년 9월 17일 수요일

Gecko와 Webkit의 CSS Transform 테스트

Gecko 엔진에서도 CSS Transform 기능이 추가되었다는 소식을 듣고 테스트해보았다. 특히 나는 CSS에서 left: 50% + 20px;과 같은 다른 단위끼리의 간단한 사칙연산을 지원해주었으면 하는 생각을 가지고 있었고, left 속성과 transform 중 linear traslation을 조합하면 해당 기능을 구현할 수 있으리란 생각이 들었기 때문이다.

직접 간단한 예제를 만들어 테스트해본 결과 예상대로 제대로 동작함을 확인할 수 있었다. 오오 ㅠ_ㅠb

Gecko와 Webkit의 CSS Transform 테스트


일단 transform 속성 중 위치를 변경시키는 translate 자체는 거의 100% 호환되는 듯하나, 불행히도(?) rotate시킬 때 어느 점을 기준으로 하는지는 약간 차이가 있는 것 같다.

실제 테스트 페이지도 따로 볼 수 있도록 만들었다. 여기 클릭.

IE에서도 DXImageTransform.Microsoft.Matrix 필터를 이용해 이런 transform이 가능하기는 한데, 직접 해보니 rotation의 경우 원래 블록이 차지하던 영역 밖으로 벗어나는 부분이 그대로 짤려버려서 매우 보기 흉하고, 특히 직접 회전변환 행렬을 계산해서 넣어줘야 한다는 점이 개발자 입장에서 매우 불편하다.

사실상 두 엔진의 transform 속성이 거의 똑같은 문법으로 동작하는만큼 이 기능이 CSS3 표준에 포함되었으면 좋겠다.

2008년 8월 16일 토요일

HTTP 모델링하기

텍스트큐브닷컴에 글을 쓰는 건 꽤 오랜만인 것 같다;; 그 사이 업데이트도 좀 된 것 같고... 글을 쓰긴 써야 할 것 같아서 요즘 한창 생각하고 있는 주제 중 하나를 써본다. (그래도 '웹'에 관한 블로그라고 했으니 그쪽 관련된 것으로 골랐다-_-)

요즘 스팍스에서 KSearch (가제)라는 카이스트 내부를 대상으로 하는 검색 엔진 프로젝트가 진행 중인데, Lucene과 Python을 기반으로 만들면서 후배 녀석들이 웹브라우저와 통신하는 프론트엔드 서버를 위해 말 그대로 SimpleFramework를 만들어둔 것이 있다. 그쪽을 맡아서 하던 후배가 교환학생(...)을 가버리는 바람에, 또 마침 내가 좀 시간이 생겼기도 하고 해서 내가 그 부분을 맡아서 계속 작업해나가는 중이다. (내가 한 작업으로는 Django의 템플릿 엔진을 떼어와 얹은 것 등이 있다)

앞으로 프론트엔드 전체를 Django로 갈아엎어버릴지까진 모르겠지만 Django의 경우 DB와 반드시 연동되어 돌아가야 하고 그와 관련하여 설정을 별도로 해줘야 하는 등 순수 검색 인터페이스만을 위한 단순 UI 구성에는 귀찮은 면이 있어 초간단 WSGI 인터페이스 wrapper를 만들어 쓰고 있다.

Python에서 사용하는 웹서버 CGI 규격이라고 할 수 있는 WSGI 인터페이스가 어느 정도 기본적인 처리는 해주지만 이것을 좀더 쓰기 편하게 만들려면 역시 HTTP에 대한 이해가 필요하다. 여기서는 HTTP를 다루는 프레임웍을 만들고자 할 때 어떤 점들을 고려하면 좋을지 간단히 써보려고 한다.

Request - Response

웹이라는 건 기본적으로 웹브라우저(client)와 웹서버(server) 사이의 통신에 의해 구현된다. 그 통신 방법은 보통 HTTP라는 프로토콜을 이용한다.

웹브라우저는 웹서버에게 이러이러한 정보를 달라고 요구하거나 웹서버가 어떠어떠한 동작을 해줬으면 하는 request를 보내고, 웹서버는 이를 처리한 후 결과를 response한다. Request의 종류는 몇 가지가 더 있지만 나머지는 잘 쓰이지 않고 보통 GET/POST 두 가지를 사용하며 GET은 어떤 정보 제공을 요청하는 것이고 POST는 웹브라우저가 데이터를 제공하고 그에 대한 처리 결과를 알려달라고 요청하는 것이다.

HTTP의 경우, 일반적으로 한 번의 request-response 쌍이 오가고 나면 연결이 끊어진다. 따라서 로그인과 같은 기능을 구현하려면 여러 client들로부터 날아오는 request들을 각 client 단위로 구분하고 인증할 방법이 필요한데, 이것이 세션과 쿠키이다.

쿠키는 웹서버가 웹브라우저에 뭔가 작은 크기의 정보를 저장할 수 있게 해주는데, request가 들어올 때마다 쿠키를 조회하여 자기가 기록했던 정보를 알 수 있다. 따라서 로그인 인증 과정에서 부여한 (랜덤한) 고유값을 쿠키에 넣어두고 그 고유값과 인증 여부의 mapping을 웹서버가 가지고 있으면 그 client를 로그인 상태로 유지하고 쇼핑몰 장바구니와 같이 계속 가지고 있어야 하는 정보들을 관리할 수 있다.

따라서 모델링에는 Request로 날아온 데이터를 잘 unpack해서 다루기 쉽게 해주는 부분과, session을 관리하고 현재 session이 가지고 있는 데이터를 다룰 수 있게 해주는 부분, response와 그 출력을 작성할 수 있게 해주는 부분들이 포함되어야 한다.

Response의 세분화

그러한 웹서버의 response에는 여러가지 종류가 있는데, HTTP specification을 보면 잘 처리되었음을 알리는 200번대 응답코드와 다른 페이지로 이동하라는 것을 알리는 300번대 응답코드, 요청을 수행할 수 없음을 알리는 400번대/500번대 응답코드가 있다.

가장 많이 사용되는 것은 200 OK, 302 Found, 403 Forbidden, 404 Not Found, 500 Internal Error 정도가 되겠다. 그외 세부적인 코드들도 많이 있지만 보통 이 정도만 알고 있으면 무방하다. OOP 언어를 사용한다면 클래스 상속 정도로 모델링해볼 수 있겠다.

Ajax 처리하기

뭔가 특별하고 새로운 신기술 같아 보일 수도 있는(...) Ajax도 기본적으로 request-response라는 건 똑같은데, 다만 그것이 웹브라우저에서 봤을 때 페이지 이동 없이 javascript를 이용해 문서의 일부분만 갱신하는 형태로 보여진다는 것이 다를 뿐이다. 웹서버 입장에서는 다른 것이 전혀 없다.

디버깅 요소 집어넣기

웹프로그래밍에서 가장 어려운 것은 디버깅이다. 간단한 문제라면 웹출력에 몇 가지 문자열을 더 찍어보는 것으로 해결되겠지만 복잡한 것들은 이런 방식으로는 찾아내기 힘들고, 프레임웍 구조나 사용 환경의 차이에 따라 response 출력 과정에서 중간에 '끼어들어' 문자를 찍는 것 자체가 불가능하거나 매우 까다로울 수도 있다.

Django 디버그 모드 오류 페이지 (ⓒDorkyCute)

위 그림은 Django 프레임웍의 디버그 화면이다. 500 Internal Error를 날리되 거기에 디버깅 정보를 함께 실어준다. 코드가 진행되다가 처리되지 않은 예외가 발생할 경우 이렇게 stack trace와 각 단계에서의 local variable 정보, 에러 메시지와 그 위치 등을 상세하게 표현해주는데, 프레임웍을 설계한다면 디버깅 기능은 반드시 고려해야 할 중요한 요소다.

잡다한 표준들

캐시 제어 및 시간 표현을 위한 RFC 규약들, Form 데이터를 주고받기 위한 인코딩/디코딩 방법, 올바른 MIME-Type 지정하기 정도가 기본적으로 고려되어야 할 부분들이다. 여기에 Django처럼 보다 추상화된 형태의 Form 등을 지원한다면 더 좋을 것이다.

-

뭐 대충 적어봤다. 아는 사람한텐 다 아는 내용일 테지만 나 스스로도 요구사항을 정리하는 관점에서 간단하게 적어보았다. 여기에 부가 기능을 더한다면 표현과 로직을 분리하기 위한 template 엔진이나, DB를 쉽게 다루기 위한 ORM 등이 있을 수 있겠다.

2008년 6월 2일 월요일

daybreaker.info 첫화면의 새 글 보여주기

얼마 전에 제 개인 웹사이트인 daybreaker.info 대문을 Django 기반으로 변경했습니다. 아직은 데이터베이스를 쓸 만한 껀덕지도 별로 없지만 몇몇 생각하고 있는 것들이 있어 생각난 김에 바꿨지요.

그러고나서 단순하게 메뉴와 프로필 링크만 달랑 달려있는 현재 대문을 좀더 생동감 있게 바꾸었으면 하는 생각이 들어 mootools와 django를 이용하여 간단하게 최신글 3개씩을 보여주도록 하였습니다.

어떻게 만들었는지 간단히 정리해볼까 합니다.

1. 기존 코드
Mootools 1.11 버전을 이용, #menu-info와 #menu-tip이라는 div 레이어로 각 메뉴 링크의 title 속성을 애니메이션화하도록 되어 있었습니다.

2. Javascript 업글(?)
얼마 전부터 Google이 Ajax library들을 모아서 호스팅해주는 서비스를 시작했습니다. 트래픽을 최대한 아끼기 위해 그것을 이용했습니다.

Tip! google.load()를 이용할 때 <head> 태그 내에 자바스크립트 코드 자체가 직접 포함되어 있을 경우, 그 스크립트에서는 mootools에서 제공하는 domready 이벤트가 제대로 작동하지 않았습니다. 대신 google.setOnLoadCallback()을 이용하거나, 스크립트를 외부 파일로 빼내어 <script src="..."> 태그를 google.load()보다 아래쪽에 두면 문제 없이 실행됩니다.

3. Javascript 작성
뭐 긴말 할 것 없이 직접 소스를 보시는 게 빠르겠습니다. =3=3
앞쪽에 if (window.ie6) {...} 부분은 일단 무시하시고...-_-;;; (이 부분은 프로필 페이지에서 제 사진이 페이드인되는 효과를 구현하는 것인데, IE6에서 css 호환이 잘 안 되어 자바스크립트로 강제하는 것 때문에 쓸데없이 길어졌습니다.)

이전에는 #menu-info, #menu-tip을 HTML에 넣어둔 채로 시작했는데 이번부터는 아예 다 자바스크립트에서 동적으로 생성하여 사용하도록 하여 HTML을 좀더 깔끔하게 유지할 수 있었습니다. 대신 메뉴의 각 링크 element에 tag라는 비표준 속성을 추가하여 이 링크가 어디를 가리키는 것인지 자바스크립트 및 뒤에서 만들 ajax handler에서 구분하기 쉽도록 했습니다. (표준 속성인 id나 name을 이용해도 되지만 그러면 이름을 붙일 때 또 뭔가 'menu-'와 같은 prefix를 붙여야 할 것 같았기 때문에 그냥 깔끔하게 보이려고..-_- 이건 취향 따라 하시면 되겠습니다.)

Mootools가 제공하는 애니메이션 효과나 확장 메소드 등은 잘 문서화가 되어 있는 편이므로 직접 무툴즈 웹사이트를 참고하시길 권해드립니다.

가장 크게 삽질했던 부분은, 처음에는 모든 팁을 하나의 div element를 이리저리 옮겨가며 보여주었는데 ajax 로딩 시간이 마우스 움직임보다 느릴 경우 이전에 로딩을 시작한 내용이 다른 메뉴 항목에 마우스를 올렸을 때 나타난다든지 하는 타이밍 문제였습니다. 이걸 제대로 맞추려고 별의별 이상한 삽질을 다했는데 결론은 'element 개수 몇 개 되지도 않는데 메모리 아끼느니 메뉴별로 만들자'였고 5분만에 해결했습니다. -_-;

4. 서버측 스크립트 작성
이미 Django가 세팅되어 있는 상태에서, 다음과 같이 ajax 핸들러 주소만 추가해주었습니다. (루트에 있는 urls.py에서 /main의 urls.py를 include하고 아래는 그 두번째 urls.py의 내용)

from django.conf.urls.defaults import *
from mysite.main import views

urlpatterns = patterns('',
        (r'^ajax/news/(?P<menu>.+)$', views.ajax_news),
)

위와 같이 하면 menu라는 이름으로 캡쳐된 URL의 부분문자열이 ajax_news라는 뷰 핸들러 함수의 menu 인자로 넘겨집니다.

핸들러의 구조는 대략 다음과 같습니다.

def ajax_news(request, menu):
    # menu 이름으로 해당하는 최신 정보를 뽑아서 items 배열에 dictionary 형식(title, date)으로 저장함.
    # 실제로는 직접 MySQL로 DB를 읽거나 feedparser라는 패키지를 이용해 RSS를 긁었다.
    return render_page(request, 'ajax.main.news.html', {'menu': menu, 'items': items, 'no_date': no_date})

여기서 ajax.main.news.html은 완성된 HTML 문서를 담고 있는 게 아니라 Ajax로 전송되어질 부분(<ul> 블록)만 담고 있습니다. no_date는 Boolean 변수로, True가 지정되면 날짜를 출력하지 않습니다.

한 가지 삽질했던 것은 Feed burner의 RSS에는 date 정보가 없고-_- 본문 앞쪽에 그냥 텍스트로 붙여준다는 점인데, 아마 HanRSS 등에서는 그걸 직접 파싱하는 게 아닌가 싶군요.;; 그냥 저는 귀찮아서 no_date = True로 처리.;;

5. Cache
여기까지 만들고 나니 일단 돌아가기는 했습니다...만 메뉴 항목에 마우스를 올릴 때마다 서버측에서 http request를 날려서 rss를 가져오고 그걸 파싱하고 앉아있으니 엄청 느립니다. 그래서 얼마 전에 퍼키군님이 이름 통계 분석기를 만들면서 memcached를 이용해 큰 성능 향상을 보셨다는 글을 보고, 저도 적용하기로 결정했죠.

방법은 아주 간단합니다. Django 매뉴얼을 따라 memcached를 세팅하고(제 서버는 이미 세팅되어 있었음) settings.py에 CACHE_BACKEND 설정을 추가하고 view 핸들러에 @cache_page decorator를 붙여주면 끝입니다. 데코레이터 인자로 시간(초)을 주면 해당 시간이 지난 뒤 엑세스할 때 새로 내용을 업데이트하지요.

Firebug로 확인해본 결과 캐시가 없을 때는 최대 5초 이상 걸리던(;;) request 반응 시간이 10~50ms 정도로 단축되는 것을 볼 수 있었습니다. 툴팁 텍스트가 튀어나오는 애니메이션이 이루어지는 동안 로딩이 다 끝난다는 뜻이지요. 물론 중간에 캐시를 새로고침하는 경우에도 지루하지 않도록 ajax loading indicator를 넣기는 했습니다.

Django에서는 template의 일정 부분만을 캐싱한다든가 DB 레벨에서 캐싱한다든가 아니면 memcached에 직접 접근한다든가 하는 식으로 여러 가지 캐시 방법을 제공합니다. 현재 텍스트큐브 DB 백엔드를 완전히 새로 짜려고 머릿속으로 구상 중인데(조만간 TNF 분들께 문서 하나 공유 들어가겠습니다) 이런 특성들을 텍스트큐브 2.0에 도입하면 어떨까 생각 중입니다. 웹호스팅 업체에서 memcached를 지원하는지가 문제긴 하지만...-_-;;

뭐 어쨌든 이렇게 해서 또 하나의 삽질을 완성했던 것입니다.; =3=3

2008년 5월 28일 수요일

블로그 개설

음, 드디어 초대장이 날라왔길래 만들어보았습니다. :)

Tistory의 IT Blog에 이어 여기서는 좀더 세분화된 주제로 웹개발 전반 -- 텍스트큐브 및 각종 홈페이지 제작 과정에서 얻은 팁이나 노하우 등 -- 에 대해 써볼 생각입니다.

삽질할 때마다 포스팅 하나의 모토를 실천해보도록 노력하겠습니다.. ㅋㅋ (먼산)

ps. 아마 포스팅의 말투는 딱딱하게 갈 것 같습니다.