제공 :
한빛 네트워크
저자 : Pete Hodgson
역자 : 조현석
원문 :
Keeping jQuery in Check
jQuery는 DOM과 다른 브라우저 API를 사용하기 쉽게 만들어 준다. 거의 엄청 쉽다. 전지전능한 $는 항상 내가 "jQuey 덩어리"라고 부르는 설계 디자인으로 이끌어 준다.
jQuery 덩어리 코드는 아무생각 없이 아무데서나 $를 사용한다. AJAX 호출과 DOM 조작은 애플리케이션 로직과 비즈니스 로직 사이에 섞여있다.
얼마나 중요한 자바스크립트 앱을 만들든 이런 접근은 엄청난 고통을 가져다 준다. $로 DOM을 접근하는 것은 공유 전역상태라는 큰 자루를 움직이는 것이다. 이것은 앱의 일부를 수정하거나 확장할때 모든 DOM 상호작용을 머리속에서 조심스럽게 생각해야 한다는 것을 의미한다. 이것은 매우 어려우며, 에러가 발생하기 쉽다.
1. DOM을 테스트하는 것은 재미없다
jQuery 덩어리는 유닛테스트하기 두려운 존재라는 점이 재미있다. 나는 TDD가 당신의 코드의 디자인에 피드백을 주는데 중요한 역할을 한다고 생각한다-한가지 일에 집중되고 약하게 결합된 디자인은 테스트하기 쉽다. 그래서 나는 jQuery 덩어리(유닛테스트하기 어려운)가 유지보수와 발전하기 어려운 잘못 설계된 코드로 만들어 지는게 놀랍지도 않다.
말했듯이, $ 전역변수(전지전능 $라고도 알려진)는 항상 사용할 수 있는 거리에 있고 그것의 편리성은 사용하고 싶게 만든다. 하지만 내포된 의존성, 예를 들면 전역변수는 테스트하기 어렵게 만든다. UI코드는 또한 테스트하기 어렵다고 알려져 있다. jQuery 덩어리 코드는 UI수정에 관한 의존성을 내포한다. 이 이중고는 코드를 매우 테스트 하기 힘들게 만든다. 대부분의 개발자는 jQuery 덩어리에 대한 자동화테스트를 작성하는 일을 맡으면 꽤 엄청난 고통을 맛본다.
$를 가짜로 만들 순 없다.
전화번호를 기본적으로 검증하는 코드를 생각해 보자. 가장 쉬운 방법은 $를 이용해 어떤 DOM이벤트를 어떤 검증함수(또는 jQuery 플러그인, 그것은 또한 검증 오류가 있으면 $를 이용해서 UI를 변경한다.)에 엮어 주는 것이다. 이런 접근의 문제는 당신의 자체 검증 로직은 DOM과 jQuery를 통해서 직접적으로 연관되 있다. $를 사용하는 코드는 실제로 격리하여 테스트 할 수 없다.
그래서 실제 DOM과 엮여 있지 않게 격리하여 유닛테스트 하고 싶다면 우리가 테스트 하고 싶은 우리의 테스트 객체(이 경우 검증 로직)를 $를 흉내내므로써 분리해 내야 한다. 매우 좋은 아이디어이다. 하지만 jQuery의 유능하고 능수능란한 API를 흉내내기를 시도한다면 어떤 고통이 기다리고 있을지 모른다. 일단, 거의 모든 jQuery 호출은 $(...)이다. 여러개의 독립적인 호출을 전달해주는 문자열만 다른 동일한 $(...)로 흉내내려고 한다면 지루하고, 소스코드 길이만 길어지고, 버그나기 쉽다. 게다가, jQuery의 전형적인 부드러운 메소드 체이닝 API 스타일을 적절한 시기에 적절한 함수에 흉내내는것은 엄청난량의 훈련을 해야 한다. 다시한번 말하지만 이것은 지루하고 읽기 어려우며 버그가 발생하기 쉽다. 아, 테스트 객체의 메소드 체인 시퀀스의 어떤 변화도 유닛테스트가 실패하게 만들 것이다. 그리고 알 수 없게 실패할 것이다.
이것에 대한 합당한 대응은 jQuery가 DOM을 이용하는 것에 관한 행위기반의 테스트를 다 포기하고, 대신 상태기반 접근을 사용한다(행위기반과 상태기반 테스트의 차이점에 관해서는 이 글을 참고하자). 상태기반 접근은 jQuery가 직접 접근 할 수 있는 DOM 컨텍스트에서 테스트가 실행된다. 사용자 입력을 시뮬레이션하기 위해 실제 DOM 이벤트를 발생시킨다. 테스트 객체가 jQuery를 어떻게 이용하는지 확인하기 위해, 그것이 DOM을 수정하게 하고 수정사항을 확인한다.
결국 $를 이용하는 어떤 테스트 코드든 DOM이 있어야 작동한다. 그것은 jQuery 덩어리 앱은 브라우저에서 모든 테스트를 돌려야한다는 것이다. Phantom 같은 도구를 이용해서 테스트를 빠르고 더 자동화 할 수 있다. 하지만 여전히 node.js 같은 독립적인 런타임 환경에서 대부분의 테스트를 돌리는 것보다는 선택하고 싶지 않다.
유닛테스트를 브라우저에서 돌리는게 그렇게 큰 문제일까? 브라우저는 결국에 실제 앱이 돌아 가는 곳이다. 하지만 나는 문제가 있다고 본다. 우리가 하는 테스트의 종류에 부정적인 영향을 주고 그래서 우리가 테스트를 얼마나 잘 디자인하는 지에 부정적인 영향을 준다. 만약 우리가 우리의 코드가 jQuery를 이용해서 브라우저와 어떻게 소통하는지 보면서 앱의 로직을 테스트 한다면, 어쩔 수없이 앱 로직은 테스트 하는 것은 UI와 연결된다. 이것은 우리의 테스트가 불안정하다는 것을 의미한다. UI를 변경하면 앱로직 테스트가 아무 이유 없이 실패한다. 예를들어 DOM에서 검증 메시지가 어디 나타나는지 바꾸다면 유닛 테스트는 아무 이유 없이 실패한다. 이것은 또한 우리의 테스트가 느리게 실행 될 것이라는 것을 의미한다. 우리는 브라우저 위에서 돌리거나 아니면 적어도 DOM 같은게 있어야 한다. 마지막으로 그리고 가장 중요한 것은 우리의 테스트는 집중되지 않았다. UI를 고려하는 것이 물을 흐리면 우리가 진짜 알고 싶은 걸 보기 힘들다. 그리고 다시, 테스트 실패가 UI 변경인지 앱 로직의 문제인지 명확하지 않다. 이것은 우리가 고립 테스트라고 말하는게 아니다. 독립된 일부분을 테스트해서 그것이 우리가 예상하는 데로 정확하게 동작하는지 보는것은 불가능하다.
2. 해결책: 분리된 DOM
UI code를 다루는 로직의 문제는 업계에서 처음은 아니다. 90년대에 복잡한 클라이언트 개발을 했다면, 아무생각 없이 데이터베이스를 직접 조회하는 버튼 핸들러의 즐거움을 봤을 것이다. 이 문제를 해결한 방법으로 채택할 수 있을테니 좋은 소식이다.
jQuery를 분리하기
해결방법은 강하게 저수준 UI코드를 분리하여 코드베이스로 만들고 명확한 API를 제공하여 결국 표현 계층을 만든다. API는 두 종류가 있다. 앱이 DOM 조작으로 UI를 변경하는 것과 DOM이벤트를 받아 앱에 전달해 UI의 상호작용에 반응하다.
잘 정의된 표현계층이 있다면 이제 우리는 $를 쓰지 않아도 된다. 이 새로운 분리된 세상에서 우리는 DOM을 얇은 표현계층을 통해서 사용하므로서 효과적으로 DOM과 앱의 나머지 부분을 분리했다.
실전 분리
분리된 DOM 패턴의 그 검증 예제 앱을 보자. 이 예제는 단순하고 약간 투박하지만 표현계층과 앱의 나머지 부분을 이해하는데 도움이 될 것이다.
DOM 이벤트
우리의 표현계층에서 이벤트의 역할은 저수준 DOM이벤트를 추상적인 "우리의 이벤트"로 만드는 것이다.
function onUserFormChanged( callback ){
$("form.userInfo :input").on( "change", callback );
}
이 예제에서는 간단하게 $(...) 를 (...) 로 감쌌다. 하지만 "실제" 표현계층에서는 복잡한게 구현할 것이다. 또한 form.userInfo 같은 셀렉터가 재사용가능한 jQuery 객체로 공유되고 있다.
var $form = $("form.userInfo");
function onUserFormChanged( callback ){
$form.find(":input").on( "change", callback );
}
우리의 이벤트의 목적은 저수준의 DOM이벤트(예를 들면 셀렉트박스를 클릭)가 아니라 고수준의 이해 가능한 이벤트(예를 들면 우리가 원하는 색을 고르는 것)를 구현하는 것이다. 한번 표현 계층에서 구현하면 나머지 부분에서 기술적 구현(클릭한다.)가 아니라 우리의 언어(사용자가 색을 선택한다.)로 코딩할 수 있다. 더해서 앱의 다른 부분에 영향 주지 않고 유연하게 UI구조를 변경할수 있다. 둘다 좋다.
DOM 가져오기
DOM 이벤트를 추상화 한것 같이 DOM 조회도 추상화 할 것이다. 검증코드에서 직접 $로 입력 값을 가져오는 대신 약간의 표현계층을 만든다.
function getUserFormFields(){
return {
name: $form.find("input.name").val(),
email: $form.find("input.email").val(),
phone: $form.find("input.phone").val()
};
}
저수준 가져오기를 폼필드의 현재 입력값을 객체표현으로 반환하는 하나의 메소드로 감쌌다.
DOM 변경
마지막으로, 표현계층은 UI를 업데이트하는 저수준의 것들을 추상화 한다. 우리의 폼 검증기는 오류 입력을 만나면 UI를 업데이트 한다. DOM을 $로 직접 접근하는 대신 다음과 같이 표현계층의 함수를 이용한다.
function updateValidationMessages(messages){
var $messages = $form.find(".validation-messages");
$messages.empty();
messages.forEach( function(message){
$("
>").text(message).appendTo($messages);
});
}
이 저수준 함수는 검증 메세지 리스트의 현재 존재하는 내용을 지우고 전달된 각 메세지를 새로운 로 추가하는 지루한 jQuery 작업을 한다. 재미없고 검증기능 구현하는라 머리 아픈 검증로직 중간에 넣고 싶지 않다. 이 기능으로 우리는 검증코드가 검증에 집중할 수 있다. 그냥 검증 오류가 있으면 updateValidationMessages 를 오류메세지 문자열 배열로 부르면 된다.
템플릿 시스템이 필요하다거나 updateValidationMessages 구현이 마음에 안들지도 모른다. 당신이 맞다. 이렇게 DOM조작을 분리하는 것의 장점은 저수준 DOM조작을 향상하기 쉽다. 왜냐하면 한군데에서 몰아서 하기 때문이다.
모두 합치기
이제 검증코드가 표현 계층의 클라이언트로 동작할 것이다.
function setupUserFormValidation(){
onUserFormChanged( handleUserFormChange );
}
function handleUserFormChange(){
var formFields = prezLayer.getUserFormFields();
var validationIssues = validateFormFields( formFields );
prezLayer.updateValidationMessages( validationIssues );
}
var validator = createValidatorSomehow();
function validateFormFields( formFields ){
var issues = [];
if( validator.emailIsntValid(formFields.email) )
issues.push( "invalid email address" );
if( validator.phoneNumberIsntValid(formFields.phone) )
issues.push( "invalid phone number" );
return issues;
}
In setupUserFormValidation 에서 "사용자 폼 수정"이라는 우리의 이벤트 핸들러를 등록했다. 이벤트가 발생하면 우리는 폼필드의 현재 값을 받고 검증 오류를 점검하고 우리가 찾은 오류를 반영하여 UI를 업데이트한다. 간결하고 이해하기 쉽고 입력검증 한가지에 집중하고 있다. 우리는 DOM과 상호작용하는 저수준의 기술적인 것을 사용하지 않았다. 우리는 쉽게 고립된 상태에서 코드를 테스트 할 수 있다. UI 구조가 변경되어도 코드의 책임의 완전히 관련이 있지 않는 한 영향 받지 않을 것이다.
3. 분리 == 깨끗한 코드
분리된 DOM은 많은 장점이 있다. 표현계층은 결국 CSS 셀렉터나 DOM이벤트를 기능이 풍부한 고수준의 개념으로 변환해주는 DOM위의 추상 계층이다. 앱의 나머지부분이 UI를 고수준 추상화를 통해 접근 하므로써 코드가 이해하기 쉽다. 예를 들면 고수준 함수들을 훑어 보던중에 다음 코드중 어떤것을 선호 하겠는가:
$(".userInfo input.name").val() or getUserForm().name?
장점은 읽기 쉬운 코드보다 더 많다. UI중심 코드를 한군데 몰아두니 HTML이 시간이 지남에 따라 자라기 쉽다. 독립 컴포넌트에 집중함으로서 앱의 대부분을 엄청 빠른 독립 유닛테스트로 테스트 할 수 있다. 이 방법을 따라 많은 자바 스크립트 코드베이스를 몇 초 안에 전체 유닛 테스트를 실행 할 수 있었다. 그것은 너무 빨리 우리의 피드백 루프는 우리가 파일을 변경할 때마다 그냥 모든 단위 테스트를 실행했다! 마지막으로,이 접근법은 또한 jQuery의 다른 자주 사용하는 부분에도 동일하게 잘 적용된다-AJAX호출이라던가. 비슷한 기술은 해당 컨텍스트에 적용되는 유사한 이점을 얻을 수 있다.