소셜 게임이나 웹 게임 만드시는 분이 많은 것 같습니다. 기술적인 고민 중 하나는 Flash 기반으로 갈 것인가, HTML 기반으로 갈 것인가 일텐데요 이게 좀 민감한 문제일 수 있죠. (대부분의 소셜 게임/웹 게임은 전통적인 2D 스프라이트 애니메이션에 간단한 스크롤 등으로 구현되므로 Unity3D 같은건 일단 뺍시다)

최근 Apple와 Adobe의 신경전, Google의 기묘한 행보(한편으로는 HTML5 미는가 싶으면서 또 한편으로는 Chrome에 Flash 류 지원 강화하고. 아마도... "난 그런거 몰라, 인터넷에 광고할거니까 광고판만 키워줘염" 이런 느낌), Adobe의 6월 반격설 등등.

기업 간 이권 다툼은 그렇다 치고, 당장 현실적인 문제를 따져보면:

  • Flash를 고르면 iPhone/iPad 대응 따로 해야하는데 상당히 귀찮을 수 있음.
  • HTML를 고르면 뭔가 느릴 것 같음. 게다가 iPhone 용은 인터페이스 등이 다를텐데 어차피 따로 만들어야 하는 것 아닌가?
요런 상황이죠. 대체로 Flash를 택하고 iPhone/iPad 용은 따로 개발한다는 식으로 방향을 잡는 것 같습니다.

만약 저한테 고르라고 한다면 두 개(잠시 후 설명하겠지만 정확히는 세 개)를 다 개발하는 방향을 고르겠습니다. Lean Software Development에서 말하는 Set-based development 전략입니다:

엔터프라이즈 애플리케이션을 개발하는 회사에 다니는 친구가 중요한 의사결정을 내렸던 일에 대해 이야기해주었습니다:

"시스템에 쓸 기술 플랫폼을 골라야했지. 근데 당시에 존재하던 세 개의 선택권 중 어떤 게 우리에게 가장 잘 맞는지 확실치 않았어. 그래서 세 가지 모두 개발하기 시작했지. 그러느라 시스템의 하단부가 조금 더 일반적인 방식으로 개발될 필요가 있었는데, 결국은 그 덕분에 시스템이 매우 견고해졌지. 프로젝트 종료 직전에서야 어떤 플랫폼을 선택해야 하는지가 명확해졌는데, 그 전까지는 플랫폼을 확정할 필요가 전혀 없었어. 막상 우리가 선택한 플랫폼은 프로젝트 초기에는 고려 대상이 아니었다네."

A friend from a company that does enterprise applications told us how he made a critical decision:

"We had to choose a technical platform for a system. However, it was not clear which of the three available options was going to be the winner, let alone meet our needs. So we started developing on all three. This required the underlying development to be a bit more general than otherwise, but it turned out to be quite robust because of that. It was really not necessary to decide on a platform until quite near to the end of the project, and by that time, the correct choice was pretty obvious, but it was not the one we would have made in the beginning."

--p43, Lean Software Development - An Agile Toolkit

세 개 플랫폼 모두에 대응해서 개발하다가 프로젝트 종료 시점에 하나를 선택하고 나머지 두 개를 버리라니, 실무라고는 하나도 모르는 이론쟁이의 정신나간 헛소리같죠. 게다가, 위 사례처럼 프로젝트 종료 전에 하라는 선택하는 것도 아니라 세 개를 계속 유지하면서 가라니.


1. 세 개의 플랫폼

보통은 Flash와 HTML 두 가지 플랫폼이라고 생각하기 쉽지만 현실적으로는 세 가지라고 봅니다:

  • Flash: IE 등 느린 브라우저를 위해.
  • Webkit: Safari CSS Visual Effects를 지원하는 Safari, iPhone, iPad.
  • Canvas: 현재 버전의 Chrome과 Firefox
  • (3D 배제한다고 했으므로 Canvas 3D + WebGL이나 Unity3D 등은 일단 KIN)
따라서 IE 버전, Webkit류 버전, Canvas 버전으로 랜더링 부분을 개발하고, 이 위에 iPhone 버전, iPad 버전, 브라우저 버전으로 UI 부분을 개발하는거죠.

어떻게? 자바스크립트와 좋은 설계로.

자바스크립트에서 호스트환경(브라우저, Flash Player 등)에 의존하는 코드를 몽땅 제거하면 해당 코드는 위 세 개 플랫폼에서 모두 사용할 수 있습니다. 80~90% 이상의 코드가 한 글자도 수정없이 모든 플랫폼에서 쓰일 수 있습니다.

대충 다음과 같이 분해하면 됩니다:
  • 입출력 담당 및 호스트환경에 대한 wrapper 코드 세 벌(Flash, Webkit, Canvas)
  • 입력을 해석하고 플랫폼에 맞게 출력을 배치하는 코드 세 벌(브라우저, iPhone, iPad)
  • 나머지 몽땅
작업을 진행하면서 풀어야할 문제들은:
  • Flash와 Webkit과 Canvas의 서로 다른 작동 방식을 어떻게 잘 추상화할 것인가 (성능에 미치는 영향이 최소화되는 방식으로)
  • "나머지 몽땅" 부분에 얼마나 많은 코드를 넣을 수 있을 것인가
등이 있겠습니다만 이것들은 대체로 mystery라기 보다 problem 수준이죠.

Flash/Webkit/Canvas를 추상화하는 문제는 생각보다 삽질이 필요할 수 있습니다. 한가지만 예를 들면, 스프라이트 애니메이션 관련 API를 다음과 같이 한다고 칩시다:
  • 스프라이트를 특정 좌표에 그려주는 API
  • 스프라이트를 원하는 위치로 이동시켜주는 API
이렇게 하고 부드러운 이동에 필요한 이징 함수 등은 직접 구현해서 "나머지 몽땅" 부분에서 처리한다 치면 Canvas와 Flash에선 괜찮을지 모르나 iPhone에선 성능이 느릴 수 있습니다. Webkit에서 이징은 CSS를 이용해 선언적으로 처리해야 iPhone에서도 빠른 속도로 구현됩니다.

이런 식의 문제(어느 정도 수준에서 추상화를 할 것인지)를 잘 해결하려면 각 기술(Flash, Webkit, Canvas)의 API를 어느 정도 이해하고 실제로 삽질도 좀 해봐야하지 않을까 싶어요.

2. 부가적인 이득

위에서 인용한 글귀에도 나오지만 부가적인 이점이 있습니다. "나머지 몽땅" 부분이 플랫폼 중립적인 방식으로 설계/구현된다는 점이죠. 이게 뭔 소리인고 하니:

  • 비교적 아름다운 API
  • 완전한 테스트 가능성 (실제 커버리지가 높냐 낮냐는 개발자가 얼마나 열심히 테스트 코드를 작성하느냐의 문제)
  • 이에 따라 견고하고 유지보수 비용이 낮은 코드 확보
소셜 게임이나 웹 게임 류는 런치 이후의 운영이 매우 중요한데 여러가지를 빠르게 테스트하고자 할 때(A/B Test가 됐건 아니건), 게임 내 여러 시스템(경제가 됐건 전투가 됐건)에 대해 다수 사용자의 인터랙션을 미리 시뮬레이션해보고자 할 때 등 온갖 지점에서 엄청난 이득으로 돌아올 것이라고 확신합니다.

에.. 이건 짐작인데요, 언젠가 "나머지 몽땅" 부분에 들어있는 로직 중 상당수를 서버에서 돌려야할 일이 (여러가지 이유로) 생길 가능성이 있는데, 이 때엔 서버측 코드 새로 짜지 말고 "SSJS (server-side javascript)"를 구글링해보세요. 짐작이기는 하지만, (현재 진행중인 프로젝트라면) 서버측과 클라이언트측에 같은 일을 하는 코드가 이미 상당히 쌓여 있을 것이다에 이 돈 몽땅과 제 손모가지를...


3. 발빼기

어... 전 소셜 게임 개발 안할겁니다.

ㅌㅌ


4. 예상 FAQ

Q1: JS로 짜면 플래시 버전도 꼬진 AVM에서 실행되니까 느리잖아요?
A: 소셜 게임으로 WoW 만들거 아니니까 괜찮습니다. FarmVille 정도는 가뿐하죠.

Q2: JS로 짜면 type checking은?
A: 유닛테스트 잘 만드세요. 피가 되고 살이 됩니다.

Q3: 안해보고 이런 글 써도 되나요?
A: 믿느냐 마느냐는 님의 선택. 진실은 저 너머에...

Q4: 개발도 안할거면서 이런 글 왜 쓰나요?
A: 개버릇 남 못줘서.

Q5: Webkit CSS랑 Canvas가 정말 쓸만한가요?
A: 구글링 plz. 참고로 아이폰에서 HTML/CSS/JS로 요런짓도 가능합니다.

Q6: "나머지 몽땅" 부분에 8~90% 코드를 넣으려면 어떻게 설계를 해야?
A: 소프트웨어 설계에 대한 책들을 참고하세요. 이를테면 패턴 책이나.


...


PS: 행여나 이 글에 낚여서 삽질을 하시다가 프로젝트가 산으로 가면 제가 밥 한끼 정도 사드리는 것으로 보상을.



저작자 표시
신고

이번주 수요일(11일)에 코엑스에서 IE8 Love Developer - 개발자가 주목할 Internet Explorer 8 세미나가 열리는데, 이 중 세번째 시간을 맡게 되었습니다. (제목이 좀 재밌어요. "주목"이라니 ㅋㅋ. "대박 집"이라는 가게 이름이 생각난다는. 근데 공부를 하다보니, 실제로 주목할만 합니다)

에, 하여간 그건 그렇고, 발표 주제를 "Ajax 개발자가 IE8에 대해 알아야 할 모든 것"으로 잡았습니다. 뭐 제목이야 그렇게 잡았는데 실제로 모든 것을 다룰 수는 없겠죠 ㅎㅎ (모든 것을 알고 있는 것도 아니고요) 그래도 MSDN 등에 공개된 관련 문서는 일단 대충 훑었습니다.

그간 기획하네 UX하네 하느라 뒤쳐진 공부도 따라잡을 겸 주말 이틀 합쳐서 대략 20시간 정도 자료 조사 및 코딩 삽질을 통해 대충 공부를 했습니다(MSDN 문서 중 XDR 부분이 Beta 1 기준으로 작성되어 있어서 삽질하며 보낸 시간이 젤루 아깝),

이제는...

  1. 발표 어떻게 진행할지 고민
  2. 이에 따라 PPT 작성

이 남았는데 벌써 시간이 새벽 2시를 향해 갑니다. --;

추가: 세미나가 성공적으로 진행되었습니다. 자료 및 동영상은 이곳을 참고하주세요.

저작자 표시
신고
JSSpec 다음 버전을 개발하고 있습니다.

한쪽 모니터엔 이클립스, 다른쪽 모니터엔 각종 호스트 환경들(Windows Script Host, Rhino, IE6, IE7, FF3, Safari3, Chrome, Opera9)이 띄워진 화면입니다:


크게 봐서 목표는 두 가지 입니다:
  • 다양한 DSL 지원(TDD, BDD, Full-stack BDD?)
  • 서버측 JS 등 콘솔 기반 호스트(Rhino, Windows Script Host, Flash/Flex 등) 지원

개봉박두 --;
신고
JSSpec 관련 메일을 일주일에 몇 통 정도 받습니다. 가장 많이 받는 질문 중 하나는(국내외를 막론하고) 바로
JSSpec에서 setInterval()이나 setTimeout()이 포함된 로직을 테스트하려면 어떻게 하나요?
입니다. 메일로 짧게 답하기가 좀 힘들기도 하고, 성의가 부족해보이기도 하고 그래서... 이 주제로 짧은 글을 하나 쓰려고 합니다.


1. 초간단 요구사항 및 초기구현

화면에 숫자 10 이 나타나고, 이게 0.1초1 씩 감소하다가 0 이 되면 멈추는 그런 애플리케이션을 만든다고 칩시다. 다음은 counter.html 입니다 (doctype 생략):
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<title>Counter</title>
<script type="text/javascript" src="counter.js"></script>
<script type="text/javascript">// <![CDATA[
    window.onload = main;
// ]]></script>
</head>
<body>
    <div id="counter">10</div>
</body>
</html>
다음은 counter.js 입니다:
var counter = 0;
var handle = null;

function main() { startCountdown(); }

function startCountdown() {
    counter = 10;
    handle = setInterval(function() { countdown(); }, 100);
}

function countdown() {
    counter--;
    var element = document.getElementById('counter');
    element.innerHTML = counter;
   
    if(counter === 0) clearInterval(handle);
}
간단한 코드이므로 설명은 생략합니다. 스팩을 먼저 만들고 구현을 하면(즉, BDD 혹은 TDD를 하면) 더 쉽겠지만, 실제 상황과 유사하게 하기 위해서 스팩 없이 구현을 먼저 했습니다. Michael Feather의 명저서 "Working Effectively with Legacy Code(레거시 코드 활용 전략이라는 제목으로 번역되었습니다)"에 의하면, 위 코드는 바로 레거시 코드 입니다.
To me, legacy code is simply code without tests. ... Code without tests is bad code. It doesn't matter how well written it is; it doesn't matter how pretty or object-oriented or well-encapsulated it is. With tests, we can change the behavior of our code quickly and verifiably. Without them, we really don't know if our code is getting better or worse. --pxvi

저는 테스트 케이스가 없는 코드를 레거시 코드로 봅니다. ... 테스트 없는 코드는 나쁜 코드입니다. 얼마나 잘 짰는지는 중요치 않아요. 아무리 예뻐도, 아무리 객체지향적이어도, 아무리 캡슐화가 잘 되어 있어도 소용 없습니다. 테스트가 있으면 빠르고 검증가능한 방식으로 코드의 행위를 수정할 수 있습니다. 테스트가 없으면 코드가 좋아지고 있는지 나빠지고 있는지 알 방법이 없지요.
이제, 이 레거시 코드를 좋은 코드로 고쳐봅시다.


2. 안도감을 느낄 수 있을만큼의 스팩

너무 많지도 않고 너무 적지도 않은 딱 적절한 양의 스팩을 달고 싶습니다. 다른 말로 하자면 "안도감을 느끼며 개발을 할 수 있을만큼의 스팩"입니다.

뭐 정의 자체가 애매하다보니 사람마다 그 기준이 다를 수 있겠습니다만, 저는 아래 두 가지 로직을 커버할 수 있는 정도면 안도감이 느껴질 것 같습니다:
  • countdown()이 호출되면 div#counter 의 값이 1 씩 감소하는가
  • countdown()0.1초 간격으로 호출되는가

이제 하나씩 공략해 봅시다.


3. countdown()이 호출되면 div#counter 의 값이 1 씩 감소하는가

다음은 스팩입니다. body 태그 안쪽만 옮겼습니다.
<div id="counter">10</div>

<script type="text/javascript">// <![CDATA[
describe('Counter', {
    'countdown()이 호출되면 카운터가 1 감소되어야 한다': function() {
        var element = document.getElementById('counter');
        counter = 5;
        countdown();
        value_of(element.innerHTML).should_be('4');
    }
});
// ]]></script>
일단, 전역변수인 counter5로 강제로 설정하고, countdown() 함수를 호출합니다. 그 결과로 div#counterinnerHTML4로 변했는지를 확인합니다.

스팩을 짜놓고 보니까 좀 지저분합니다. 전역 변수(counter)를  하나 제거하면 좀 나아질 것 같군요.
describe('Counter', {
    'countdown()이 호출되면 카운터가 1 감소되어야 한다': function() {
        var element = document.getElementById('counter');
        element.innerHTML = '5';
        countdown();
        value_of(element.innerHTML).should_be('4');
    }
});
위 스팩이 통과하려면 counter.js를 다음과 같이 수정해서 전역 변수 counter를 모두 제거해야 합니다:
var handle = null;

function main() { startCountdown(); }

function startCountdown() {
    handle = setInterval(function() { countdown(); }, 100);
}

function countdown() {
    var element = document.getElementById('counter');
    var counter = Number(element.innerHTML) - 1;
    element.innerHTML = counter;
   
    if(counter === 0) clearInterval(handle);
}
한편, div#counter 엘리먼트도 어디서든 접근할 수 있다는 점에서 전역 변수라고 볼 수 있겠습니다. countdown() 함수가 이 엘리먼트에 의존하고 있기 때문에 아래의 HTML이 테스트 코드와 프로덕션 코드 두 곳에 중복으로 나타나죠:
<div id="counter">10</div>
스팩을 아래와 같이 수정하면 좋겠습니다:
describe('Counter', {
    'countdown()이 호출되면 카운터가 1 감소되어야 한다': function() {
        var element = {innerHTML: '5'};
        countdown(element);
        value_of(element.innerHTML).should_be('4');
    }
});
countdown() 함수가 element를 인자로 받도록 수정하고, 가짜 엘리먼트를 하나 만들어서 넘겨주었습니다. 이 방식은 countdown() 함수가 element의 innerHTML 속성을 이용할 것이라는 가정을 하고 있다는 점에서 좋지 않습니다만(테스트가 구현 방식에 종속됨), 기존 방식(전역 변수에 의존)보다는 좋다고 판단하였습니다. "best way"는 아니지만 "better way"인거죠.

위 스팩이 통과하려면 counter.js는 아래와 같이 바뀌어야 합니다:
function startCountdown() {
    handle = setInterval(function() {
        var element = document.getElementById('counter');
        countdown(element);
    }, 100);
}

function countdown(element) {
    var counter = Number(element.innerHTML) - 1;
    element.innerHTML = counter;
    if(counter === 0) clearInterval(handle);
}
이 정도로 하고 다음 스팩을 추가하겠습니다.


4. countdown()이 0.1초 간격으로 호출되는가

"countdown()이 0.1초 간격으로 호출되는가"라는 질문은 달리 표현하자면, "0.1초가 지나면 countdown()이 호출되는가"입니다. 이제 드디어 본론에 해당하는 내용이 나왔습니다.

대체 "0.1초가 지나면"이라는 것을 어떻게 테스트하면 좋을까요? JSSpec에 아래와 같은 가상의 기능이 추가되어서 0.1초를 "기다릴 수 있으면" 될까요?
    'countdown()이 0.1초 간격으로 호출되어야 한다': function() {
        // 여기에서 뭔가를 수행하고

        wait_for(100); // 0.1초(100msec)를 멈춰서 기다린 후

        // 여기에서 결과를 확인한다
    }
위와 같은 방식이 기술적으로 가능한지 여부를 떠나서, 이는 좋은 단위 테스트라고 볼 수 없습니다. 만약 요구사항이 0.1초가 아니라 한 시간이었다면, 위 테스트를 수행하는데 한 시간이 걸린다는 얘기인데, 이렇게 되면 원하는 때에 피드백(즉, 테스트 성공 여부에 대한 확인)을 받을 수가 없게 됩니다.

요약하자면, "0.1초가 지난 후 어떻게 되는가"를 테스트하기 위해 실제로 0.1초를 기다려야 하는 방식은 적절치 않습니다.

좀 더 적절한 방법은 0.1초를 가짜로 흘려보내는 것입니다. 자바스크립트의 Date 클래스를 다음과 같이 확장하고, 애플리케이션의 모든 코드에서 new Date() 대신에 Date.get()을 쓰도록 수정할 것을 권장합니다:
Date._preset = -1;
Date.preset = function(msec) {Date._preset = msec;};
Date.pass = function(msec) {
   Date._preset = Date._preset == -1 ? msec : Date._preset + msec;
};
Date.get = function() {
   return Date._preset == -1 ? new Date() : new Date(Date._preset);
};
preset() 함수는 시간을 임의로 설정하여 멈추어놓기 위해 사용됩니다. pass() 함수는 시간을 가상으로 흘려보냅니다. get() 함수는 현재 시간을 얻어옵니다. get() 함수가 반환하는 값은 preset() 혹은 pass()가 사용되지 않았다면 실제 시스템 시간이지만, preset()이나 pass()가 사용되었다면 가짜 시간입니다. 위 코드는 실제 프로덕션에서도 사용될 코드이므로 스팩이 아닌 counter.js에 추가합니다. 이제 아래 스팩을 추가해봅시다:
    '0.1초가 지나면 countdown()이 호출되어야 한다': function() {
        // countdown 바꿔치기
        var backup = countdown;
        var called = false;
        countdown = function() { called = true; };

        try {
            Date.preset(0); // 시간 고정
            updateIfTimePassed();
            value_of(called).should_be(false);
           
            Date.pass(100); // 0.1초 흘리기
            updateIfTimePassed();
            value_of(called).should_be(true);
        } finally {
            // countdown 복원
            countdown = backup;
        }
    }
좀 길군요. 하나씩 설명을 하겠습니다.

첫째, updateIfTimePassed() 함수가 추가되었습니다. 이 함수가 하는 일은 자신이 마지막으로 호출된 후로 0.1초 이상이 흘렀으면 countdown() 함수를 호출해주는 것입니다.

둘째, 이번에 작성한 스팩은 0.1초 간격으로 countdown() 함수가 호출되는가를 확인하기 위한 것이지 countdown() 함수가 제대로 작동되는가를 확인하는 것이 아닙니다. 다른 말로 하자면 countdown() 함수가 제대로 작동하건 안하건 0.1초 간격으로 호출만 된다면 이 스팩이 깨지지 않아야 합니다. 따라서, countdown() 함수를 가짜로 대체하고 다시 복구하는 코드(변수 backup이 사용되는 부분들)가 들어 있습니다.

셋째, 조금 전에 Date 클래스에 추가한 메서드들을 사용하여 시간을 고정시킨 후 가짜로 흘려보내고 있습니다. 이렇게 하면 이 테스트는 실제로 0.1초를 보내지 않고 순식간에 수행됩니다.

이번에는 이에 따른 프로덕션 코드의 수정입니다:
var updatedAt = null;

function startCountdown() {
    handle = setInterval(updateIfTimePassed, 10);
}

function updateIfTimePassed() {
    if(!updatedAt) updatedAt = Date.get();
   
    var now = Date.get();
    var timePassed = now - updatedAt >= 100;
    if(timePassed) {
        var element = document.getElementById('counter');
        countdown(element);
        updatedAt = now;
    }
}
변화되지 않은 부분들 - main(), countdown() - 은 생략하였습니다. 변화된 부분은 다음과 같습니다.

첫째, startCountdown 내부의 setInterval이 updateIfTimePassed를 0.1초 간격이 아닌 0.01초 간격으로 호출하고 있습니다. 0.01초는 대략 "시스템에 무리를 주지 않는 한도 내에서 최대한 빈번한 간격"입니다. 이렇게 수정하자 이 메서드는 말 그대로 카운트다운을 시작하는 일만 하게 되었고, 카운트다운의 간격 등에 대해서는 아무런 간섭도 하지 않게 되었습니다.

둘째, updateIfTimePassed() 함수가 새로 만들어졌습니다. 이 함수는 자신이 마지막으로 호출된 이후로 0.1초 이상이 지나면 countdown() 함수를 호출하도록 되어 있습니다.

셋째, 이 과정에서 전역 변수 updatedAt이 추가되었습니다. 좋지 않지만 잠시 참고 가보도록 하겠습니다.

Date 클래스를 확장하는 방식에 대해서 조금 더 부연설명이 필요하신 분은 유닛테스트에서 시각과 시간이라는 글을 참고해주세요. 자바를 기준으로 설명하고 있지만 결국 같은 방식입니다.


5. 리팩토링

원하는 스팩을 두 개 만들었으니 이제 좀 마음놓고 리팩토링을 해야 합니다. 마음에 안드는 부분들이 많습니다.

첫째, 0.1초 간격으로 무언가를 호출하는 로직(updateIfTimePassed 함수)이 카운트다운을 수행하는 로직(countdown 함수)과 엉켜있습니다. 게다가 countdown 함수에 필요한 인자를 넘겨주기 위해서 DOM에도 의존하고 있습니다.

둘째, 전역 변수가 두 개(handle, updatedAt) 있습니다. 그리고 DOM 이 숨은 전역 변수 역할을 하고 있기도 합니다. 이게 무슨 말이냐하면, 전역 변수라는게 나쁜 이유는 어디에서든 접근할 수 있기 때문에, 코드의 여러 지점이 이 전역 변수를 중심으로 엮인다는 점입니다. 그렇게 본다면 div#counter 라는 엘리먼트가 여기저기에서 쓰이면서 의존성을 만들어내고 있다는 점에서 숨은 전역  변수입니다. 예전에 박응주님이 자바의 ThreadLocal이나 WebWork의 ActionContext 등도 전역 변수이다라고 말씀하신 것과 같은 맥락입니다.

(저는 예전에 위 두 가지 문제를 해결하기 위해 Timer 혹은 Scheduler라는 클래스를 만들었었는데 아주 만족스러웠습니다.)

셋째, 스팩 자체도 깔끔하지가 않습니다. 가짜 객체(element, countdown)를 만들어서 이런저런 의존성을 끊어주어야만 테스트간 격리(test isolation)가 이루어진다는 것은 결국 좋지 못한 설계로 인해 생긴 문제에 다름 아닙니다. 위에서 언급한 두 가지 문제를 해결하면 스팩도 더 깔끔해지겠죠. 원래 테스트하기 쉬운 코드일수록 설계가 좋은 코드라는 말이 있습니다. 그 반대도 성립합니다. 설계가 좋은 코드일수록 테스트하기가 쉽기도 합니다. 역시나 가장 좋은 방법은 테스트(혹은 스팩)를 먼저 만들고나서 설계를 하는 것(즉, TDD 혹은 BDD를 하는 것)입니다.

이제 퇴근을 해야겠으니 이런 부분들은 미해결로 남겨놓겠습니다. ^^;


PS - 요약 및 일반화된 결론

setInterval 을 테스트하는 문제는 사실 더 큰 문제의 구체적 사례일 뿐입니다. 문제를 더 일반화하면 이렇게 됩니다:
제어하기 힘들거나 비용이 많이드는 외부 시스템(시스템 타이머, 디스크, 네트워크, 외부 라이브러리, 프레임워크의 노출된 인터페이스 등등)과 엮인 코드를 어떻게 테스트할 것인가.
이 문제에 대한 일반해는 InsertTestableLayer를 참고하시기 바랍니다.

그런데, TestableLayer라는 것을 넣으면 테스트가 가능해진다는 점 말고 대체 뭐가 좋아지는걸까요. 위에서 설계가 좋아진다고 쓰기는 했는데 구체적으로 뭐가 어떻게 좋아진다는 것일까요. 뭐 여러가지 주절주절 설명할 수 있겠지만... 자바스크립트의 경우는 이렇습니다:

비즈니스 로직(0.1초 간격으로 1씩 카운트다운)에 해당하는 코드를 전혀 수정하지 않고 다른 호스트 환경(host environment - Rhino, jslibs, ASP, JScript.NET, Silverlight, Flash/Flex 등)에서 그대로 사용할 수 있게 됩니다. 이것도 또한 반대로 얘기할 수 있는데, 코드를 전혀 수정하지 않고 다른 호스트 환경에서 사용할 수 있게 된다면 제어하기 힘든 외부 시스템에 대한 의존이 제거되었다고 볼 수 있습니다.

신고
  • Ajaxian » Greasemonkey, Chrome Edition

    Greasemonkey의 Chrome 버전이 조만간 나올 듯.

  • Cognitive Daily: Emotion, risk, evolution, and gender

    감정 상태에 따라 위험감수(risk taking) 행동에 변화가 생기는데, 여기에도 남녀간의 성차가 존재한한다고 합니다. 2004년도 논문을 요약한 글이라고 합니다. 화가 난 상황에서 남성의 위험감수 성향이 높아지는데 여성의 경우 차이가 없었고, 혐오를 느끼는 상황에서 남성의 경우 위험감수 성향이 높아지고 여성의 경우 위험감수 성향이 낮아진다고 하는군요. 하지만 그 이유에 대한 진화적 설명은 (요약문만 보자면) "just so stories"로 들린다는.

  • Practical Functional Javascript

    제가 존경하는 Oliver Steele 횽아가 10월 초에 발표한 슬라이드. 자바스크립트로 하는 함수형 프로그래밍.

  • Cooper Journal: Human motivation as a way to understand user goals

    user goal을 이해하기 위해 인간의 동기에 대해 공부할 필요가 있다는 글. 지당하신 말씀입니다 :-) 이 분야도 역시 진화심리학으로부터 얻을 것이 많겠다고 생각합니다.

  • jslibs - Google Code

    파이어폭스의 자바스크립트 엔진인 SpiderMonkey를 이용하여 만든 독립실행형 자바스크립트 런타임입니다. 파이어폭스 없이도 실행되며, 다양한 라이브러리가 제공됩니다. 심지어는 OpenGL까지도.

신고

JSON-Object-Element Mapper

간만에 하는 포스팅입니다.

다운로드: http://jania.pe.kr/aw/moin.cgi/JsonObjectElementMapper


최근 3주 중 2주 동안 자바스크립트 노가다를 조금 했습니다. 마지막 한 주는 2주간 했던 노가다를 앞으로 어떻게 줄여볼까 하는 궁리를 하면서 보냈습니다. (버그 안잡고! ㅋ) 그래서 일명 JOE ( JSON-Object-Element mapper, "죠"라고 읽습니다 --; ) 라는 놈을 만들었습니다. 참고로, jquery 플러그인입니다.


1. 간단한 예제:
<div id="names">
   <p class="name"><span class="first">Alan</span> <span class="last">Kang</span></p>
   <p class="name"><span class="first">Cate</span> <span class="last">Kim</span></p>
</div>

서버에서 그려서 보내준 위와 같은 HTML이 있다고 칩시다. 아마도 Name이라는 클래스의 인스턴스 두 개를 담고 있는 배열을 템플릿 엔진에 전달해서 그려낸 결과겠죠.

요즘은 Ajax다 뭐다 해서, 이 HTML을 클라이언트에서 조작해야하는 경우가 왕왕 생깁니다. 이를테면, 사용자가 첫번째 사람 이름(Alan Kang)을 변경하면 동적으로 HTML이 갱신되고, 서버로 Ajax 요청을 보내는 식입니다. 이때, 클라이언트 사이드에서 이런 일이 가능하다면 좋겠죠?
$.joe.def('Name', {first: '.first', last: '.last'});

위 코드를 통해 Name이라는 클래스의 매핑정보를 정의합니다. first와 last 속성이 있고 각각 CSS Selector .first 및 .last에 대응됩니다.
var names = $('#names p.name').toObj('Name');

위 코드에서는 jquery의 CSS selector 문법을 통해 원하는 Element을 선택하고(두 개의 p.name 엘리먼트) 이를 위에서 정의한 클래스(Name)의 인스턴스와 매핑합니다. 이렇게 해서 얻어진 names는 length가 2인 배열이고, 배열의 원소는 자동으로 생성된 객체입니다.
  console.log(names[0].first()); // Alan
  names[0].first('Allen');
  console.log(names[0].first()); // Allen

getter와 setter의 이름은 동일하고 인자의 갯수에 의해 다형적으로(polymorphic) getter 혹은 setter로 실행됩니다. 이는 jquery의 컨벤션을 따른 것이예요. 위 코드를 통해 첫번째 원소의 first 값을 'Alan'에서 'Allen'으로 변경하면 HTML의 해당 부분이 알아서 갱신됩니다. 이제 화면은 바꿨으니 서버로 요청을 보내야합니다. JSON이 필요한 상황이죠.
var json = names[0].toJson();

JSON도 쉽게 얻어졌습니다. 이 JSON의 내용은 {first: 'Allen', last: 'Kang'} 입니다. 전송하는 코드는 생략하겠습니다.


2. 컴포지션, 타입 지정, 속성 매핑

참 쉽죠?

( dotty님 블로그에서 보고 너무 재미있어서 함 퍼왔습니다 --; )

예제를 조금 더 복잡하게 바꿔 보겠습니다. Person은 name, age, profile image url을 갖습니다. 여기에서 name은 위에서 정의한 Name 클래스의 인스턴스입니다.
<div class="person">
  <p class="name"><span class="first">Alan</span><span class="last">Kang</span></p>
  <p class="age">18</p>
  <p><img class="profile" src="alan.jpg" /></p>
</div>

일단 두 클래스(Name과 Person)를 정의해야겠죠:
$.joe.def('Name', {first: '.first', last: '.last'});
$.joe.def('Person', {
  name: '.name {{Name}}',
  age: '.age {{number}},
  profile: '.profile [[src]]'
});

Name을 정의하는 부분은 별 것 없으니 생략합니다. Person의 정의가 재미있는데요,
  • name 속성은 .name에 매핑되고, 타입은 Name 입니다.
  • age 속성은 .age에 매핑되고, 타입은 number 입니다.
  • profile 속성은 .profile에 매핑되고, 타입은 생략되었으므로 string입니다. 다만 [[src]] 를 통해 innerHTML이 아닌 img.src 속성에 매핑되도록 지정하였습니다.

이제 정의를 했으니 인스턴스를 만들어봅시다:
var person = $('.person').toObj('Person');

첫번째 예제에서와 달리 이번에는 CSS selector에 의해 선택된 Element가 하나 뿐이므로 배열이 아닌 단일 객체가 만들어집니다. 이제, 아래와 같이 사용할 수 있습니다:

person.name().first('Allen'); // 이름을 Alan에서 Allen으로 변경
person.age(person.age() + 1); // 나이 한 살 더 먹기
person.profile('alan.png'); // img.src 값을 alan.jpg 에서 alan.png로 변경


3. Detached Object, Methods

위 코드를 보면 불편한 점이 둘 있습니다. 첫째, 성(last)과 이름(first)을 다 바꾸려면 메서드를 두 번 호출해야 하나? 둘째, 나이를 1 증가시키는 코드가 꼭 저렇게 길어야 하나?

첫번째 문제를 해결하려면 새로운 name 객체를 구해서 이를 바로 대입하면 될겁니다:
var newName = ...?? ;
person.name(newName);

하지만 새 name 객체를 만들 방법이 마땅치 않습니다. 기존에 이미 존재하는 Element에 대해 toObj()를 호출해야 하는 방법 밖에 없죠. 꾸졌습니다. 그래서, Element와 무관하게 새 객체를 만들 수 있도록 하는 기능을 추가했습니다:
var newName = $.joe.Name('Allen', 'King');
person.name(newName);

이렇게 만들어진 객체는 Element와 관련성이 없기 때문에 detached object라고 부릅니다. 사실 detached object의 용도는 다양합니다. 우리가 만들려고 하는 애플리케이션이 주소록이고, 주소 데이터가 만 개 쯤 있다고 칩시다. 이를 객체 형태로 만들어서 메모리에 담아놓고 각종 조작(정렬, 갱신, 추가, 삭제 등)을 하는 것이야 별 문제가 없겠으나, 이러한 모든 조작이 DOM에 영향을 준다면 심각한 성능 문제가 발생합니다. 따라서, 원하는 경우에 객체를 Element와 분리시킬 필요가 있습니다. 객체와 Element를 분리시키는 방법은 두 가지가 있는데, 위와 같이 애초에 Element와 연결되지 않은 객체를 새로 생성하거나, Element와 연결된 객체를 강제로 연결 해제하는 아래와 같은 방법이 있습니다:
var person = $('.person').toObj('Person');
person.detach(); // 연결 끊기
person.name().last('Allen'); // DOM은 갱신되지 않습니다.
person.attach($('.person')); // 다시 연결하기. 이 때 DOM이 일괄 갱신됩니다.

이제 두 번째 문제를 해결해봅시다. age를 1 올리는데 person.age(person.age() + 1) 같은 지저분한 코드를 써야하는 문제였습니다. 그냥 이렇게 할 수 있으면 좋겠습니다:
person.older();

아래와 같이, 클래스를 정의할 때 원하는 메서드를 추가해주면 됩니다:
$.joe.def('Person', {
  name: '.name {{Name}}',
  age: '.age {{number}},
  profile: '.profile [[src]]',

  older: function() { this.age(this.age() + 1); }
});

이제 지저분한 코드가 안으로 숨었고, 좀 더 객체지향적인 코드가 되었습니다. 기존 코드는 왜 객체지향적이지 않을까요? Person이 갖고 있는 속성인 age의 값을 외부에서 얻어와서 뭔가 연산을 수행하고 이를 다시 age에 대입해주는 코드는 캡슐화(encapsulation) 및 정보은닉(information hiding)에 위배되기 때문이죠. 자기가 가진 속성에 대한 연산은 자기가 하는 것이 좋습니다.


4. 배열 다루기

지금까지는 고정된 HTML이 있는 상태에서 HTML의 특정 내용을 바꾸는 작업만 했습니다. 즉, Alan과 Cate가 존재하는 상황에서 이들의 이름을 바꾼 적은 있지만 제 3의 인물이 추가된 적은 없었습니다. 이게 가능하려면 객체를 통해 HTML을 뽑아낼 수 있어야 합니다.

이번엔 좀 더 복잡한 HTML을 예제로 써보겠습니다. "가족(Family)"을 소개합니다. div.person 의 내부는 기존 예시와 동일한 구조라서 생략하였습니다.:
<div class="family">
  <div class="head">
    <div class="person">... Father ...</div>
  </div>
  <div class="members">
    <div class="person">... Alan Kang ...</div>
    <div class="person">... Cate Kim...</div>
  </div>
</div>

Family에는 가장(head)이 있고, 구성원들(members)이 있습니다. 가장과 구성원들은 모두 Person 클래스의 인스턴스입니다. 물론 Person은 Name을 포함하고 있습니다. Person과 Name의 정의는 생략하고 Family를 정의하는 부분만 살펴보겠습니다:
$.joe.def('Family', {
  head: '.head .person {{Person}}',
  members: '.members {{array<Person>}}'
});

members 속성의 타입은 array<Person> 입니다. C++/Java/C# 계열 언어의 제너릭스 문법과 유사합니다. Person 객체들을 원소로 갖는 배열이라는 뜻입니다. 배열에는 아래와 같은 방식으로 접근할 수 있습니다:
var family = $('.family').toObj();
family.members()[0].name().first('Allen');

가족(family) 구성원(members) 중 첫번째(0) 사람의 이름(first)을 'Alan'에서 'Allen'으로 변경하는 코드입니다. 이 코드를 보면 자연스럽게 몇 가지 질문이 생길겁니다. 첫째, 가족 구성원을 추가하려면? 둘째, 가족 구성원을 통으로 변경하려면? 셋째, 특정 가족 구성원을 대체하려면?

둘째 문제가 쉬우니, 이걸 먼저 풀어보겠습니다. 그냥 배열을 통으로 대입하면 됩니다:
var newMembers = [
  new $.joe.Person(...1...),
  new $.joe.Person(...2...),
  new $.joe.Person(...3...)
];
family.members(newMembers);

Person 객체(detached object) 셋을 담고 있는 배열을 만들고, 얘를 통으로 members에 대입해버리는거죠. 쉽습니다.

첫째 문제와 셋째 문제는 이런식으로 풀리질 않습니다. 그래서 다른 방법을 도입했습니다. 속성의 타입이 array<T>인 경우, 자동으로 추가적인 accessor를 생성해줍니다. 예를 들어 속성 이름이 members인 경우, 아래 메서드들이 추가됩니다:
  • insertMembers(index, value);
  • deleteMembers(index);
  • appendMembers(value);
  • prependMembers(value);
  • replaceMembers(index, value);

이름이 뻔하니까 설명은 생략하겠습니다. 아마 여기까지 읽으신 분 중에 뭔가 이상하다 싶은 부분을 발견하신 분이 있을지도 모르겠습니다. 네 뭔가 이상합니다. 배열에 새 원소가 추가되면 DOM에 새 Element가 생성되어야 하는데, 우리가 정의한 클래스는 그런 일을 해낼만큼의 충분한 정보를 가지고 있지 않습니다.


4. HTML 출력

사실, 위 코드가 작동하려면 객체로부터 Element를 만들어낼 수 있게끔 뭔가 정보를 추가로 제공해야 합니다. 클래스를 정의할 때 아래와 같이 템플릿 정보를 줘야 하는 것이죠:
$.joe.def(
  'Name',
  {first: '.first', last: '.last'},
  '<p class="name"><span class="first"><@= first() @></span><span><@= last() @></span></p>'
);

보통은 <%= ... %> 를 사용하는데, 이렇게 하면 .rhtml 등 서버측 템플릿 언어와 함께 쓰는 경우 충돌이 나서(--;;) 어쩔 수 없이 <@= ... @>로 변경했습니다.

Name 클래스는 간단하니까 <@= ... @>로 해결했다고 치고, Family 클래스는? members의 갯수만큼 반복(iterate)이 필요합니다. 어떻게 할까요? 어떻게 하긴요, 걍 똑같이 합니다:
<div class="family">
  <div class="head">
    <@= head().toHtml() @>
  </div>
  <div class="members">
  <@ for(var i = 0; i < members().length; i++) { @>
  <@= members(i).toHtml() @>
  <@ } @>
  </div>
</div>

5. HTML 분리하기

이제 대충 끝났습니다. 하지만 마음에 안드는 문제가 하나 있습니다. 자바스크립트 코드 안에 HTML 템플릿이 들어있잖아요. 이걸 좀 HTML 파일 안으로 넣고 싶습니다.

2005년인가 2006년에 D모사 H모 서비스 주소록을 만들 때 사용했던 트릭이 있었는데, 템플릿 코드를 style="display: none" 으로 감춰진 엘리먼트 안에 넣어놓는거예요. 그렇게 해놓고 cloneNode를 하건, innerHTML을 하건 해서 원할 때 마다 가져다 쓰는거죠. 얼마전에 은둔고수 응주님께서도 비슷한 트릭을(특히, 이 트릭이 왜 필요한지를) 설명하신 바 있습니다.

그리고, 두둥, John Resig 사마가 최근 궁극의 솔루션을 제시하시기에 이르게되는거시었던거시었습니다. (사실 위에서 소개한 템플릿 기능도 Resig 횽아가 갈쳐줬다눈)
<script id="t_name" type="text/html">
  <p class="name">
    <span class="first"><@= first() @></span>
    <span class="last"><@= last() @></span>
  </p>
</script>

짠! style="display: none;" 할 것 없이 걍 script 엘리먼트를 만들고 그 안에 HTML 템플릿을 넣는 것이죠. 간단해보이지만 재미있는 트릭입니다. 첫째, type이 "text/html"로 되어 있는데, 브라우저는 이런 스크립트를 모르죠. 모르는 스크립트는? 자동으로 무시합니다.

한편, 검색엔진은? 위와 같은 템플릿 코드는 검색엔진이 안보는 것이 좋을텐데요, 역시나 script 태그의 내용은 검색엔진에 의해서도 무시됩니다. 몇몇 실험적인 봇은 script를 평가하려고 시도하겠지만, type이 "text/html"인 스크립트라면? 무시하는거죠.

하여간, 위 방식을 이용하면 이제 클래스 정의가 다음과 같이 깔끔해집니다:
$.joe.def(
  'Name',
  {first: '.first', last: '.last'},
  $('#t_name').html()
);

이제 표현을 위한 코드가 완전히 분리됐습니다.


6. 정말 표현을 위한 코드가 분리된 것인가?

매핑 코드에 나오는 CSS selector들을 보면서 이런 의문을 가질 법도 합니다:
CSS라면 표현(representation)을 위한 장치인데, 클래스 정의(logic)에 CSS selector를 이용하는 것이 바람직스러운가?

사실 CSS 스팩을 잘 읽어보면 CSS는 두 파트로 구분됩니다.
p.name {color: blue;}

위 코드에서 p.name 부분은 Selector 이고, color: blue; 부분은 declaration 입니다. 잘 짜여진 CSS의 경우 Selector에는 문서의 의미(semantic, logic)가 담기고, declaration에 문서의 표현(representation)이 나타납니다.

예를 들어, 아래 예시에서 첫줄의 selector는 좋고, 둘째줄은 나쁩니다. 브라우저 버그, CSS 2.1의 제약 등으로 인해 어쩔 수 없이 쓰게 되긴 합니다.
.name .first {color: blue;}
.corner_left_top {background-image: ... ;}

현실의 제약 때문에 어쩔 수 없는 부분들을 제외한다면 최대한 아래와 같이 읽을 수 있도록 selector와 declaration을 구분해줘야 합니다:
제목은 빨간색
본문은 검은색에 작은 글씨
중요한 인용은 이텔릭

굵은 글씨가 selector에 해당하는 부분입니다. 문서의 의미가 드러나 있습니다. 반면 아래와 같은 selector는 지양하는 것이 좋겠죠:
세번째 div 밑에 있는 p는 빨간색
네번째 div 밑에 있는 모든 요소는 검은색에 작은 글씨
네번째 div 밑에 있는 모든 요소 중 blockquote.italic은 이텔릭

이렇게 되면, HTML이 수정될 때 CSS를 좀 덜 바꿔도 되고, CSS를 수정할 때 HTML을 좀 덜 바꿔도 됩니다. (두 파일이 완전 독립적일 수는 없을 것 같아요. CSS 2.1이 꼬져서)

에, 그래서 요지는 문서의 의미에 해당하는 올바른 Selector만 가지고 $.joe.def 를 하면 된다는겁니다.


7. Microformats

CSS Selector가 "의미있게 잘 쓰이려면" HTML 자체도 의미있게 작성되어야 합니다. 어떤 HTML이 의미있는 것인가? 광범위한 질문이라, 외력을 빌려 간단히 요약하자면, Microformats 스타일의 HTML이 좋다고 할 수 있습니다. 지정된 Microformat을 따르건, Microformat의 컨벤션만 빌려서 쓰건 간에 상관 없습니다.

이 방식을 따르면 HTML이 좀 더 의미를 충실히 표현하게 되고, 그렇게 되면 CSS Selector가 더 의미 있어지며, 그렇게 되면 joe의 클래스 정의도 더 의미 있어집니다. 일반적으로, 한가지 장치가 여러가지 문제를 일관성 있게 해결해준다면, 그 장치는 좋은 장치라고 믿어도 되는거죠. 이 경우엔 Microformats가 그렇습니다.

반대로 말하자면, joe를 잘 쓰려고 노력하다보면 좋은 CSS, 좋은 HTML이 부수효과로 얻어질 가능성이 높습니다. :-)


8. 동적 컨텐츠와 정적 컨텐츠를 동일하게 다루기

눈치 빠른 분은 이미 아셨겠지만, joe를 사용하면 서버에서 보내준 정적 컨텐츠와 클라이언트에서 만든 동적 컨텐츠를 동일하게 취급할 수 있게 됩니다.

별도의 데이터를 자바스크립트 객체나 배열에 넣어놓고 쓰는 것이 아니라 브라우저의 DOM 자체를 데이터 소스로 사용하기 때문입니다. (물론 detached object가 있지만 이건 캐싱, 배치처리, 버퍼링, Ajax 통신 등의 용도로 한시적으로 사용하는 개념입니다)

예를 들어, 현재 로그인 한 사용자의 이름과 ID를 자바스크립트에서 알아야 하는 경우가 있습니다. 이 때 우리가 보통 쓰던 방식은 var curUser = {id: ..., name: ...}; 등과 같이 객체를 만들어서 어딘가에 쳐박아두고 필요할 때 불러쓰는 방식입니다. 하지만, 로그인 한 사용자의 이름과 ID는 이미 HTML에 담겨 있을겁니다. 중복이죠. 웹 애플리케이션에서 이상적인 역할 분담(separation of concerns)은 대략 아래와 같습니다:

  • 구조와 의미(혹은 데이터): HTML
  • 표현: CSS
  • 행위: Javascript

curUser라는 객체를 별도로 만들너 놓는 것은 중복일 뿐 아니라 부적절한 역할분담입니다. 이로 인해 발생할 수 있는 대표적인 문제는 HTML이 수정되었는데 curUser는 수정되지 않아서 로직이 꼬이는 경우죠. 페이지가 복잡해지면 복잡해질수록, 동적으로 변하는 요소가 많으면 많을수록 큰 문제가 됩니다.

반면, joe 방식에서라면 별도의 curUser를 만드는 것이 아니라,

  1. 화면 상에 나타나는 User 객체에 대한 마크업에 의미를 부여하여 최대한 구조를 통일하고
  2. 화면 상단 쯤에 표현되는 현재 로그인한 사용자 정보도 동일한 마크업을 사용하여 그려줍니다.
  3. 이 때, 표현은 CSS로 분리되어 있으므로 충분히 자유롭게 바꿀 수 있습니다.
  4. $.joe.def 를 통해 User 객체를 정의하고 curUser를 자동으로 생성합니다.

이렇게 하면 위에서 소개한 역할 분담에 좀 더 가까워지고 로직이 꼬이는 문제가 발생하지 않습니다. 왜냐하면 직접적인 DOM 조작을 통해 HTML이 변경되어도 매핑된 curUser 객체는 자동으로 갱신되기 때문입니다(getter가 호출되는 시점에서 DOM에 접근해서 데이터를 읽어오기 때문입니다).


9. 맺음말

아, 길게도 썼습니다. 코딩은 고작 300줄 하고 이빨은 이렇게 길게 까고. 원래 나대는 인생이란 그런거죠 ㅎㅎ 제가 앞으로 바라는 바는? ( 전 바라는 바하려는 바가 일치하지 않아요. 왜냐하면 땡기면 하고 안땡기면 안하거든요 --; )

  • 서버측에서도 DOM과 Javascript를 지원하는 프래임워크가 나와주고
  • joe가 이 프래임워크와 통합되면

좋겠습니다. 서버측 자바스크립트에 대해서는 예전에 글을 하나 쓴 적이 있는데, 지금은 여기에 생각이 더 보태졌어요. 간단히 말하자면, 추상화를 통해 감춰야 할 대상이 있고 오히려 드러내야할 대상이 있는데, HTML, DOM, HTTP는 감춰야할 것이 아니라 드러내야할 것이라는 생각이 강해졌습니다. 그렇게 되면, Client/Server 모두 합쳐서 DOM/HTML/CSS/Javascript/HTTP 정도만 알면 모든 웹 프로그래밍이 가능합니다.

이 얘기는 다음 기회에. (에, 비슷한 프로젝트로 http://beebole.com/pure 가 있습니다만 제 생각이랑은 초큼 달라요)


신고
오늘 코엑스 애니랜드 가서 Wii 를 사왔습니다. 프로젝터랑 연결해서 대형화면으로 즐겨볼까 합니다 ㅋ 근데 아직 스피커를 연결 못해서(단자 모냥이 안 맞아요 ㅜㅠ) 리모콘에서 나는 소리만으로 "처음 만나는 Wii"를 초큼 해봤습니다.

에, 주제는 그게 아니고, 이 Wii 리모콘이라는게 참 신기하단 말이죠. 일단 양방향 통신이 가능하고, 3차원 공간상에서의 움직임을 인식하기도 하면서, (좀 찾아봤더니) 앞 부분의 적외선 센서는 동시에 네 개의 적외선 신호를 인식할 수 있다고 합니다. 게다가, 블루투스를 지원하기 때문에 Wii 본체가 아니라 일반 컴퓨터랑도 연결이 가능하다는 사실.

눈이 번쩍. 뭔진 모르겠으나 재밌는 놀이를 할 수 있겠다는 생각이. 좀 더 찾아보니 역시나. 이미 많은 사람들이 Wii 리모콘을 가지고 다양하게 놀고 있더군요. 이를테면 마이너리티 리포트 놀이를 하시는 분도 계시고요.

저도 뭔가 하고 싶은데, 회사에도 맨날 자바스크립트만 하다보니 이것도 자바스크립트로 해야겠다는 오기 혹은 사명감 따위의 이름모를 악감정(응?)이 발동하였습니다. ㅋ

1. 우선 Wii 리모콘과 PC를 연결해야 하는데, 제 PC에는 블루투스가 없다는 --; 하지만 옆에 있는 UMPC(후지쯔 U1010)에 있으므로 OK. "블루투스 장치 연결 마법사"를 시작하고 Wii 리모콘의 1, 2 버튼을 동시에 눌러서 연결 성공. (Nintendo RVL-CNT-01 이라는 이름으로 연결됩니다.)

2. 위에서 링크한 "마이너리티 리포트 놀이" 하시던 분이 만들어 놓은 .NET Managed DLL 발견. (아싸 날로 먹었다)

3. Windows Scripting Host의 JScript로 해볼까 싶었는데 위 라이브러리를 어떻게 COM으로 등록하는지 몰라서 초큼 삽질하다가 포기. RegAsm.exe 로 해봤는데 "No types were registered"라고만 나옴. 음. 찾아볼까 하다가 귀찮아서 포기. 안되면 말지 뭐. JScript는 포기.

4. JScript.NET으로 선회. ㅎㅎ 위 DLL과 같은 디렉토리에 test.js 라는 파일을 생성:
    > import WiimoteLib;
    > new Wiimote();

5. 컴파일
    > jsc test.js

6. 실행
    > test.exe

7. 끝. 이제 DLL 소스와 도움말(chm) 참고해서 노는 일만 남았습니다. 뭘 하면 좋을까요? ㅡㅡ;

이제 준비는 끝났고, 앞으로 진척 상황을 연재 형식으로 써볼 생각입니다. :-)

* PS: 구글신께서 알려주신건데, 닌텐도의 Wii에는 오페라가 들어 있잖아요? 그 오페라에는 특별한 객체가 있다고 합니다. 대충 이런 식인가봐요:
    > var remote = opera.wiiremote.update(1);
    > if(remote.isEnabled) { ... }
신고
http://jania.pe.kr/xtx


요런 놈을 하나 만들어봤습니다. 스크립트 두 줄만 써주면 보통 TEXTAREA에 몇 가지 유용한 단축키들이 추가됩니다.
  • Tab, Shift+Tab 으로 들여쓰기, 내어쓰기
  • Alt+Up/Down 으로 줄단위 이동
  • Ctrl+Alt+Up/Down 으로 줄 복사
  • Ctrl+D 는 현재 줄 삭제

짬나면 자동완성이나 템플릿 같은 기능들을 좀 추가해볼 생각입니다. 괜찮다 싶으면 Xquared소스 편집기에도 붙여볼 생각이예요. 에 물론 Plugin 형식으로요. 이것저것 다 붙이면 무거워지니깐.

(이전 포스트에서 3시간 작업하다가 날려먹었다는 코드가 이거였어요 --;)
신고
립트음. 잠시 블로그를 쉬는 동안 몇 번의 릴리즈가 있었는데 적지를 못했습니다. UI가 매우 후지긴 하지만 일단...
  • WYSIWYG 편집 중에 동영상을 재생할 수 있고, Copy&Paste, Drag&Drop 등이 가능합니다.
  • Google Gadget 등을 삽입할 수 있습니다. Gadget도 마찬가지로 Copy&Paste 등이 가능합니다.
이제 조금만 더 나아가면 Microformat을 삽입하고 편집할 수 있게 될 것 같습니다. 음. 그 사이에 대략 다음과 같은 일들이 있었습니다:

2008/04/23

xquared_client_20080423.tar.gz

  • 개선: 소스 코드의 구조를 전체적으로 수정하였습니다. 패키지 개념을 도입하고 모듈 간 의존성을 낮추어 동영상 등 부가적인 기능을 Plugin 형태로 분리하였습니다.
  • 수정: TEXTAREA가 아닌 엘리먼트를 Xquared와 연결할 수 없도록 제한하였습니다.
  • 추가: 마우스로 편집기 하단을 드래그하여 크기를 조절할 수 있게 해주는 플러그인(EditorResizePlugin.js)이 추가되었습니다. examples 디렉터리의 resize.html을 참고하세요. (zenguy님과 지나감님이 제안하신 기능입니다. 감사합니다.)
  • 추가: 야후! 비디오를 삽입할 수 있도록 하였습니다.
  • 추가: 새 버전의 판도라TV 플래이어를 삽입할 수 있도록 하였습니다.
  • 수정: 동영상 삽입 시 URL을 입력하지 못하도록 제한하였습니다. OBJECT 혹은 EMBED만 사용 가능합니다.
  • 수정: 편집 모드 BODY에 margin과 padding이 없을 때 동영상이 재생되지 않던 문제를 수정하였습니다.
  • 수정: 잘라내기(Ctrl+X)로 동영상을 제거하는 경우 화면에서 동영상이 사라지지 않던 문제를 수정하였습니다.


2008/03/27

xquared_client_20080327.tar.gz

  • 추가: 다음 tv팟 (http://tvpot.daum.net), 판도라TV(http://www.pandora.tv), 엠엔케스트(http://mncast.com) 동영상을 추가할 수 있게 되었습니다. 툴바의 동영상 버튼 The image “http://jania.pe.kr/xquared/client/images/toolbar/movie.gif” cannot be displayed, because it contains errors. 을 누르세요.
  • 추가: 동영상 삽입 시 URL 대신 OBJECT 코드를 입력해도 인식할 수 있도록 하였습니다.
  • 추가: IFRAME을 넣을 수 있습니다.
  • 추가: config.autoFocusOnInit을 true로 설정하면 에디터가 실행될 때 자동으로 포커스를 갖도록 할 수 있습니다.
  • 개선: 배포판에 API 문서를 포함시켰습니다. docs 디렉터리를 참고하세요.
  • 개선: Mac 용 기본 단축키 몇 가지를 추가하였습니다. Meta+B, Meta+I, Meta+K, Meta+U가 각각 강한 강조, 강조, 취소선, 밑줄을 실행합니다. 기존 단축키인 Ctrl+B, Ctrl+I, Ctrl+K, Ctrl+U도 그대로 유지됩니다. 단축키를 제거하거나 추가하는 방법은 기능 확장하기를 참고해주세요.
  • 수정: IE에서 HTTPS 페이지 상에 Xquared를 띄울 경우 보안 경고가 나오던 문제를 수정하였습니다.


2008/03/20

xquared_client_20080320.tar.gz

  • 개선: 배포 파일의 구조를 크게 수정하였습니다. 자세한 내용은 변경된 튜토리얼을 참고하세요.
  • 개선: xq.Editor와 xq.Toolbar를 분리하였습니다.
  • 수정: 동영상이 다이얼로그를 통해서는 삽입되지 않고, HTML 소스를 직접 수정하는 방법으로만 삽입될 수 있었던 버그를 수정하였습니다.
  • 수정: FF와 IE에서 한 문서에 여러 개의 동영상이 있는 경우 첫번째 동영상만 제대로 로딩되는 문제를 수정하였습니다.
  • 수정: Safari에서 줄바꿈 및 블럭 단위 이동 등이 제대로 수행되지 않던 문제를 수정하였습니다.
  • 수정: FF에서 초기에 xq.Editor.focus()나 xq.RichDom.focus()를 호출해도 포커스가 잡히지 않던 문제를 수정하였습니다.
  • 수정: IE6/7에서 CSS를 수정하여 소스 편집 창에 border를 준 경우 브라우저가 멈추는 버그를 수정하였습니다. (gEEkInsIdE님이 알려주신 문제입니다. 감사합니다.)
  • 수정: 글자 크기 및 글꼴 메뉴에서 특정 항목이 아닌 제목 부분을 선택하면 에러가 발생하던 문제를 수정하였습니다. (zenguy님이 알려주신 문제입니다. 감사합니다.)
  • 수정: FF에서  HTML 속성이 '=' 문자로 끝나는 경우(예를 들면 <a href='http://x.com?y='>), Validator가 오작동하는 문제를 수정하였습니다.
  • 수정: FF에서 한글을 입력하던 중간에 편집기에서 포커스가 사라지면 동일한 글자가 하나 더 생기던 문제를 수정하였습니다.
  • 수정: FF에서 빈 블럭을 위/아래로 옮길 때 캐럿이 사라지는 문제를 수정하였습니다.
  • 수정: 긴 문서에서 다이얼로그를 창의 중앙(centerOfWindow)에 띄울 때 스크롤바의 위치가 계산되지 않아서 위치가 틀어지던 문제를 수정하였습니다. (zenguy님이 알려주신 문제입니다. 감사합니다.)


2008/03/14

xquared20080314.tar.gz

  • 추 가: YouTube 동영상을 삽입하는 기능이 추가되었습니다. WYSIWYG 편집 중에도 재생이 가능합니다. 툴바의 Movie 아이콘을 클릭하면 새 동영상을 삽입할 수 있습니다. 곧 다음 TVPot 등 다른 동영상들도 추가할 수 있도록 할 예정입니다.
  • 추가: Xquared의 기능을 더 쉽게 확장할 수 있도록 하기 위해 플러그인 API를 추가했습니다.
  • 개 선: Validation 로직이 개선되었습니다. 태그 별로 허용되는 속성 집합을 정의할 수 있게 되었습니다. 이에 따라 Validation 설정 변경 로직이 바뀌었습니다. 기존에 Validation 설정을 수정하여 사용하고 계셨다면 설정하기 페이지를 참고하여 해당 코드를 고쳐주시기 바랍니다.
  • 개선: 초기 로딩 속도 및 편집 모드 전환 속도가 향상되었습니다.
  • 수정: 중첩된 리스트 편집 중 DEL, BS 누를 때 리스트가 엉키는 문제 일부를 수정하였습니다.
  • 수정: IE6,7 에서 자동으로 생성되는 공백 문자가 제거되지 않던 상황 일부를 수정하였습니다.
  • 수정: xq.Editor.onEditorElementChanged에 인자를 추가하였습니다(editor instance).
  • 수정: 서식 제거(removeFormat)를 할 때 링크는 삭제되지 않도록 하였습니다.


2008/03/06

xquared20080306.tar.gz

  • 개선: Windows 및 Mac용 Safari에서 일부 단축키가 인식되지 않던 문제를 수정하였습니다.
  • 개선: Windows 및 Mac용 Safari에서 Validation/Invalidation이 제대로 되지 않던 문제를 수정하였습니다.
  • 개선: 큰 문서를 읽어올 때의 성능이 약간 향상 되었습니다. (자주 쓰는 Regular Expression을 미리 컴파일하도록 하였습니다)
  • 수정: Firefox에서 xq.RichDom.collectStructureAndStyle()이 간혹 오작동하여 툴바 갱신 중 예외가 발생하는 문제를 수정하였습니다.
  • 수정: xq.RichDom.hasFocus() 함수가 제대로 작동하지 않던 문제를 수정하였습니다.
  • 수정: prototype.js 가 xquared.js 보다 먼저 로딩될 경우에 발생하는 문제를 수정하였습니다.


2008/03/03

xquared20080303.tar.gz



신고
신고
< Newer     Older >

티스토리 툴바