Created by Firejune at 2011/10/10 섹시한 자바스크립트 코딩 컨벤션 자바스크립트를 코딩함에 있어서 그동안 습득했던 문법 규칙, 팁, 노하우, 등을 이곳에 정리합니다. 여기에서 다루는 내용은 읽는 사람에 따라서는 저수준 리팩토링일 수 있고, 성능 최적화일 수도 있고, 가독성을 높이는 동시에 청결함을 유지하는 것일 수도, 그리고 무슨 소리를 하는건지 모를수도 있습니다. 지금부터 최근 인기몰이 중인 몇몇 오픈소스 프로젝트의 소스코드들을 예로 들어 가면서 코딩 트랜드를 파악하고 학습하는 시간을 가져봅시다. 알아볼 수 있게 이것은 개발자로써 근본이 되어야 하는 관습입니다. 가독성이 높으면 협업에 있어서 야기되는 혼란을 미연에 방지하는 효과가 있기 때문에 일부 회사나 단체에서는 개발자들이 지켜야 할 가이드에 포함하기도 합니다. 참고로 구글의 자바스크립트 스타일 가이드에서는 세미콜론을 찍는 것 조차도 왈가왈부하고 있습니다. 하지만 우리 실정은 그렇지 않죠? 이러한 규칙이 있을리 만무합니다. 그래서 여러 경험을 통해 얻어낸 자신만의 규칙을 따르거나 개발도구 또는 어떠한 소스로부터 얻어진 규칙을 따라야 합니다. 암묵적으로 지켜지는 규칙들을 대략 읇어보면, 클래스의 첫 문자는 대문자로 구분하고, 전역에서 참조되는 변수는 전부 대문자로 작성하거나 다른 문자를 사용하여 구분하고, 내부에서만 사용되는 메서드나 변수는 언더스코어(_)를 첫 문자로, 괄호와 브레이스 사이는 띄어쓰거나 줄을
변경하거나, 80 또는 160 캐릭터, 2 또는 4 또는 8 탭사이즈 등 수없이 많습니다. 이것들에 대한 자세한 내용을 여기에서 다루지는 않겠습니다. 의미있는 정의 많은 사람들이 자바스크립트(정확게는 DOM스크립트)를 좋아하는 이유 중 하나는 짜놓은 코드를 그냥 읽는 것만으로도 쉽사리 이해할 수 있다는 것입니다. 의미있는 이름을 사용하는 것으로 설명될 수 있는 것은 의외로 많거든요. 해당 함수가 어떠한 일을 처리하는지, 해당 변수가 단수인지 복수인지 등을 주석문 없이도 표현할 수 있습니다. 여기에 바보같은 예를 하나 들어보겠습니다. var foo = document.getelementbyid('foo'); // get element by id form DOM var bars = document.getelementsbyclassname('bar'); // get elements by class name form DOM 누군가가 작성한 코드에서 위와 같은 주석을 발견했다면 분노의 삭제질을 작렬하면서 마음속으로는 죽여버리고 싶다는 생각이 들지도 모릅니다. 이제 모범 사례들을 차근차근 살펴보면서 안구를 정화해 봅시다. 다음은 Node.JS 모듈인 Socket.IO의 클라이언트-사이드에 사용된 스크립트의 일부분입니다. /** * Create a new `Socket.IO client` which can establish a persistent * connection with a Socket.IO enabled server. * * @api public */
function Socket (options) { this.options = { port: 80, secure: false, document: 'document' in global? document : false, resource: 'socket.io', transports: io.transports, 'connect timeout': 10000, 'try multiple transports': true, 'reconnect': true, 'reconnection delay': 500, 'reconnection limit': Infinity, 'reopen delay': 3000, 'max reconnection attempts': 10, 'sync disconnect on unload': true, 'auto connect': true, 'flash policy port': 10843 ; io.util.merge(this.options, options); 이라는 클래스를 정의하는 부분에서 초기화 옵션을 받는 부분으로 this.options 에 정의된 코드들을 살펴보면 주석으로나 사용될 문장들이 Socket 식별자(키)로 작성된 것을 확인할 수 있습니다. 이것은 잘 만들어진 래시피와도 같습니다. 다음 예제는 Socket.IO의 서버-사이드 테스트 소스의 일부분입니다. module.exports = { 'test that protocol version is present': function (done) { sio.protocol.should.be.a('number'); done();,
'test that default transports are present': function (done) { sio.manager.defaulttransports.should.be.an.instanceof(array); done();, 'test that version is present': function (done) { sio.version.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/); done();, 'test listening with a port': function (done) { 헐~ 테스트 항목에 대한 설명이 그냥 메서드 이름으로 작성되었고 테스트를 수행하는 코드는 하나의 온전한 문장이 체인으로 연결된 정적인 네임스페이스들입니다. 멋지지 않나요? 단수/복수 과거/현재 변수를 정의할 때 이름을 단수와 복수 그리고 과거와 현재를 구분하여 정의하면 가독성도 좋아지고 여러모로 편리합니다. 배열과 같은 하나 이상의 아이템을 가진 데이터 형식을 정의할 때에는 복수로 명명합니다. 다음 코드는 Cloud9의 클라이언트-사이드 코드 중 컨텐츠 마임타입(MIME type)들을 정의한 일부분과 한 아이템을 동적으로 픽업하는 예제입니다.('...'은 중략을 의미 함) var contenttypes = { "js": "application/javascript", "json": "application/json", "css": "text/css", "less": "text/css", "scss": "text/x-scss",
"sass": "text/x-sass",... "md": "text/x-markdown", "markdown": "text/x-markdown" ;... var fileext = file.getattribute("name").split(".").pop(); var customtype = contenttypes[fileext]; 그리고 과거와 현재의 뜻을 가진 변수는 어떠한 지표(플래그)를 의미하는 용도로 사용되는 경우가 많습니다. 다음 코드는 jquery 내부에서 사용되는 함수인 _Deferred 에서 과거와 현재를 구분한 변수명을 사용하는 예제입니다. jquery.extend({ // Create a simple deferred (one callbacks list) _Deferred: function() { var // callbacks list callbacks = [], // stored [ context, args ] fired, // to avoid firing when already doing so firing, // flag to know if the deferred has been cancelled cancelled, // the deferred itself deferred = { // done( f1, f2,...) done: function() { if (!cancelled ) { var args = arguments,
i, length, elem, type, _fired; if ( fired ) { _fired = fired; fired = 0; 작은 따옴표가 기본 자바스크립트에서 문자열(String)을 처리할 때 큰 따옴표(")와 작은 따옴표(') 중 어느것을 사용할지 고민되는 경우가 있습니다. 작은 따옴표(Single quotes)로 작성하는 것을 기본으로 하세요. HTML을 작성할 때에는 큰 따옴표가 기본으로 사용되기 때문에 백슬래시(\)를 이용하여 일일이 이스케이프(Escape)하는 상황이 발생하면 가독성을 크게 떨어트립니다. 다음은 HTML에 사용할 앵커를 자바스크립트로 작성하는 예입니다. var anchor = '<a href="/' + foo + '.html">' + foo + '</a>'; // correct var anchor = "<a href=\"/" + foo + ".html\">" + foo + "</a>"; // incorrect var anchor = "<a href='/" + foo + ".html'>" + foo + "</a>"; // nonsense 띄어쓰기 일반적으로 다음과 같은 띄어쓰기 규칙을 따르면 가독성이 높아집니다.
쉼표(,): 전: 0, 후: 1 괄호('(', ')'): 전: 0, 후: 0 세미콜론(;): 전: 0, 후: 1 산술 연산자(+, -, *, /, %...): 전: 1, 후: 1 관계 연산자(==, ===,!=,!==,, =...): 전: 1, 후: 1 1진 연산자(++, --): 전: 0, 후: 0 할당(a=b): 전: 1, 후: 1 논리 연산자(&&, ): 전: 1, 후: 1 키-밸류 연산자({'a':'b'): 전: 1, 후: 1 인라인 주석(//): 후: 1 파일 해더 작성하기 자바스크립트 파일의 최상단에 해더를 작성하여 대략적인 정보를 제공해야합니다. 뭐하는 녀석인지, 어떤 종속성(Dependency)이 있는지, 어떻게 사용하는지, 버전, 라이센스, 작성자, 사이트 링크, 등을 기입합니다. 최근에는 JSDoc 포맷으로 작성하는 경향이 있습니다. 다음은 Cloud9의 플랫폼 코어(apf)에 해당하는 class.js 파일의 해더입니다. /** * All elements that implemented this {@link term.baseclass baseclass have * {@link term.propertybinding property binding, * event handling and constructor & destructor hooks. The event system is * implemented following the W3C specification, similar to the * {@link http://en.wikipedia.org/wiki/dom_events event system of the HTML DOM. * * @constructor * @baseclass
* * @author Ruben Daniels (ruben AT ajax DOT org) * @version 3.0, beta * @since 0.8 * * @event propertychange Fires when a property changes. * object: * {String name the name of the changed property * {Mixed originalvalue the value it had before the change * {Mixed value the value it has after the change * */ 들여쓰기와 줄 변경하기 사실 자바스크립트는 들여쓰기와 줄변경을 무시해도 작동하는 데에는 별 문제가 없습니다. 이것들은 단순히 코드의 가독성을 위한 행위들로 여겨도 무방합니다. 일반적으로 블럭 안쪽에 위치한 문장에 들여쓰기를 하지만 jquery와 같은 라이브러리의 도움을 받는 경우 체이닝(Chaining) 스타일 코딩에서는 체인 단위로 줄을 변경하고 셀렉션 포인트가 변경되는 시점에 들여씀으로서 훨씬 수월하게 코드를 읽을 수 있습니다. $(form).prevall('div.black:first').html(json.timelabel).prevall('div.timeline:first').children('div').css('width', json.gauge + '%').end().end().end().replacewith('<div class="extend-time"><p>' + json.optionlabel + '</p></div>');
자바스크립트에서 여러 요소로 구성된 HTML을 파셜(Partial)하는 경우 다음과 같이 배열을 이용하여 들여쓰기하면 가독성이 높아집니다. 물론, 꼭 배열을 사용해야 하는 것은 아니지만 산술 연산자를 이용하는 것 보다는 성능이 좋기 때문입니다. 아래는 jquery를 이용하여 인플레이스 에디터(inplace editor)를 구현하는 코드의 일부분입니다. $(el).parent().before([ '<form method="post" action="' + el.attr('href') + '">', '<input type="hidden" value="' + el.attr('id') + '" name="id">', '<input type="text" value="" name="key">', '<textarea name="value"></textarea>', '<p>' + beautify(new Date) + '</p>', '</form>' ].join('\n')).prev().find('input[type=text]').focus(); 변수를 연속적으로 선언할 때 var 를 생략하고 쉼표(,)로 구분하여 선언하는 것이 효율적입니다.(이것에 대해서는 잠시 후에 다루도록 하겠습니다.) 이 때 보통 줄바꿈을 하는데, 요즘은 쉼표를 앞쪽에 작성하는 것이 유행하고 있습니다. 다음은 Node.JS용 MVC 프레임웍인 Express의 서버-사이드 소스에서 모듈을 가져오는 부분입니다. /** * Module dependencies. */
var,,, connect = require('connect') HTTPSServer = require('./https') HTTPServer = require('./http') Route = require('./router/route') 위 처럼 쉼표를 앞에 작성하면, 아이템을 추가하거나 주석처리하거나 삭제할 때 용이합니다. 단적으로 앞서 소개한 예제 코드들을 비교해 보면 '의미있는 정의'의 Socket 클래스 소스에 사용된 '단수/복수 과거/현재'의 contenttypes this.options 와 소스에서 맨 마지막에 위치한 아이템을 삭제하거나 주석 처리한다고 가정할 때 문법 오류가 발생하지 않는 코드는 쉼표가 전방에 배치된 쪽입니다. 코드를 줄이자 대부분의 개발자들은 적은 량의 코드로 많은 일을 시키려는 경향이 있습니다. 이것은 과하면 독이되고 안하면 병신소리 듣습니다. 그래서 적절하게 코드를 경량화하는 것이 좋습니다. 적절하다는 의미는 가독성을 해치지 않는 범위 정도로 해석하면 되겠네요. 제 경우 다른 곳에서 가져온 프로그램을 자신의 것으로 소화할 필요가 있을 때 가장 처음으로 수행하는 작업이 바로 경량화입니다. 중복성이거나 불필요한 부분을 제거 및 교정하고 동시에 가능한 한 최대로 경량화하면서 소스의 전반을 이해합니다. 이 작업을 거치고 나면 원본 소스가 가진 라인 수 보다 절반 이하로 줄어드는 경우가 다반사입니다.(그렇다고 서드파티 라이브러리를 함부로 뜯어 고치자는 의미는 아닙니다.) 변수 할당
앞서 잠시 언급한 바와 같이 변수를 연속해서 선언해야하는 경우 중복으로 작성되는 var 를 생략할 수 있습니다. 다음은 Smails의 서버-사이드 소스 중 Socket.IO를 초기화하는 부분입니다. /** * Socket server. */ var io = require('socket.io').listen(smails.port), sockets = io.sockets.sockets, clog = require('clog'), dgram = require('dgram'); io.configure(function () { io.enable('browser client minification'); io.enable('browser client etag'); io.set('log level', 2); 순차적이고 동기식으로 발동하기 때문에 첫 라인에서 선언한 io 를 두번째 라인에서 바로 접근해도 문제되지 않습니다. 스위치가 싫어요 여러 자바스크립트 문법들 중에서도 가장 가독성을 해치는 것이 바로 스위치 문입니다. 한 문장임에도 두 단계의 들여쓰기 깊이를 가지기에 꼭 필요한 상황에서만 사용하는 것이 좋습니다. 보통 if... else... 문이 여러차례 반복되는 문장을 스위치로 리팩토링하는 경우가 많은데, 아래와 같은 단순히 변수를 할당하려는 목적이라면 스위치 보다는 객체 사전을 이용하는 방법이 더 효율적이고 가독성도 높일 수 있습니다.
function getlabel(module) { var label; witch(module) { case 'todo' : label = '할일'; break; case 'calendar' : label = '달력'; break; case 'note' : label = '공책'; break; return label; getlabel('todo'); //=> '할일' function getlabel(module) { return { todo: '할일', calendar: '달력', note: '공책' [module]; getlabel('todo'); //=> '할일' 3항 연산과 조건부 할당 다음은 3가지 코드에 작성된 var value; if (n) { value = n; else { value = 1; value 는 같은 결과값를 가집니다.
// equal next var value = n? n : 1; // equal next var value = n 1; 조건부 함수 호출 callback 이 있는 경우 실행하라는 조건부 호출은 다음과 같이 사용될 수 있습니다. if (callback) { callback(); // equal either callback && callback(); 정의와 동시에 사용 아래 코드는 정의와 동시에 push 메서드를 호출하고 있습니다. (dep.using (dep.using = [])).push(extension); 무의미한 변수 선언
단순 참조용으로 선언된 변수는 생략할 수 있습니다. var self = this; $.ajax({ type: self.method, url: self.action, success: function(text){ $('#article').html(text) ); // to be $.ajax({ type: this.method, url: this.action, success: function(text) $('#article').html(text); ); 더 빠르게 보다 더 빠르게 작동하는 코드를 원하시죠? 지금부터 소개할 내용은 기존의 코드를 재작성하여 속도를 향상시키는 방법들입니다. 성능 최적화에서 가장 중요하게 여겨야 하는 것은 '측정'입니다. 측정없는 최적화는 단지 시간 낭비일 뿐이라는 사실을 기억하세요. 속도 향상을 위한 몇가지 기본적인 팁은 가급적 네이티브 코드를 사용할 것(특히 반복문), try/catch는 속도 저하 요인이 될 수 있으니 지양할 것, 캐시를 잘 활용할 것 등이 있습니다. 네이티브 코드 사용
코드의 사용성을 크게 향상시키는 자바스크립트 라이브러리들이 성행하면서 개발 생산성이 좋아졌고 초심자들의 진입장벽도 많이 허물어졌습니다. 분명히 좋은 현상이긴 합니다만, 네이티브 코드를 이해하지 못하면 결코 최적화된 애플리케이션을 만들어낼 수 없습니다.(참고로 구글은 서드파티 라이브러리에 의존하여 개발하는 것을 그리 달갑게 여기지 않습니다.) 간략한 예를 들어 보겠습니다. jquery에서 $.each를 이용한 반복문 처리 보다는 for( ) 문을 사용하는 것이 빠릅니다. Prototype.js의 bind 메서드를 사용하는 것 보다. Function.call 또는 Function.apply를 사용하는 것이 빠릅니다. 문자열 결합 문자열을 결합할 때 산술연산을 이용하는 것 보다는 배열로 작성하고 join으로 결합하는 것이 더 빠릅니다. // Instead of this... var result = 'a' + 'b' + 'c' + 'd'; // This is much faster: var result = ['a', 'b', 'c', 'd'].join(''); 캐시 효과 반복문 안에서 배열 길이를 추가 정의하는 것만으로 더 빠르게 작동합니다.
// Instead of this... function nodejam(){ nodes = document.getelementsbytagname('p'); for (var i = 0; i < nodes.length; i++) { nodes[i].innerhtml += 'test'; // This is much faster function nodejam(){ nodes = document.getelementsbytagname('p'); for (var i = 0, len = nodes.length; i < len; i++) { nodes[i].innerhtml += 'test'; 계층 구조를 가진 함수를 반복해서 호출할 때 변수로 지정하면 더 빠르게 작동합니다. // Instead of this... function iterateoverme(){ for (var i = 0; i < 1000; i++){ lorem.ipsum.dolor.sit(i); // This is much faster function iterateoverme(){ var sit = lorem.ipsum.dolor.sit; for (var i = 0; i < 1000; i++){ sit(i);
DOM에 삽입하기 전에 먼저 계산하는 것으로 훨씬 더 빠르게 작동합니다. // Instead of this... function subtrees(){ var ul = document.getelementbyid("myul"); for (var i = 0; i < 200; i++) { ul.appendchild(document.createelement("li")); // This is much faster function subtrees(){ var ul = document.getelementbyid("myul"); var li = document.createelement("li"); var parentnode = ul.parentnode; parentnode.removechild(ul); for (var i = 0; i < 200; i++) { ul.appendchild(li.clonenode(true)); parentnode.appendchild(ul); 함수 속에서 또 다른 함수를 중첩(Nesting Functions)해서 사용하지 마세요. 함수안에 정의된 함수는 상위 함수가 호출될 때마다 내부 함수를 새롭게 생성하는 처리과정이 포함되기 때문에 성능이 떨어집니다. // Instead of this... function foo(a, b){ function bar() {
return a + b; return bar(); var baz = foo(1, 2) // 3 // This is much faster function foo(a, b){ return bar (a, b); function bar(a, b) { return a + b; var baz = foo(1, 2) // 3 코딩 패턴 jquery를 통해 자바스크립트에서 효율을 높이기 위한 코딩 패러다임을 제시한 존 레식씨가 선호하는 코딩 관습를 살펴보면 모듈 패턴(Module Pattern)이라는 것이 있습니다. 익명함수를 작성하고 즉시 실행하는 것으로 클로저(Closure)에 의해 "private static scope"가 제공되어 정적인 컨텍스트(Context)를 가질수 있게하는 검증된 자바스크립트 코딩 패턴입니다. (function($) { // do something... )(jquery);
이것은 메모리에 등록된 식별자를 탐색하는 시간을 줄이고 전역 네임스페이스를 오염시키지 않으면서 서로다른 프로그램간의 간섭을 없앨수 있는 매우 좋은 방법입니다. 식별자를 검색하는데 비용이 줄어든다는 것은 곧 성능이 향상되는 것을 의미하며 마치 'with'를 사용하는 것과 같은 효과가 발생합니다. 다음 예제는 얼마전 작성한 Minimap 소스코드를 지면 관계상 재구성한 것입니다.(prototype.js에 기반하며 작동하는 코드 아님) var Minimaps = (function(win, doc, undefined) { /* * Options. */ var $options = { width: 100, height: 400, duration: 0.2, focuswidth: 0.5, focusheight: 0.5, $canvas, $context; /* * Initialize. */ return function(options) { $options = $.extend($options, options); $canvas = new Element('canvas', { id: 'outline', width: $options.width, height: $options.height );
if (!($canvas instanceof HTMLElement) $canvas.nodename.tolowercase()!== 'canvas') return console.error('your browser does not support Canvas'); doc.body.insert($canvas); this.version = '0.1.1'; this.draw = draw; this.scroll = scroll; $context = $canvas.getcontext('2d'); Event.observe(win, 'load', draw); Event.observe(win, 'resize', draw); Event.observe(win, 'scroll', draw); draw(); ; /* * Core. */ function scroll(event) { var x = event.clientx - $canvas.left, y = event.clienty - $canvas.top; scroll.ani && scroll.ani.state == 'running' && scroll.ani.cancel(); scroll.ani = new Effect.Scroll( y / draw.scale - size.win.height * $options.focusheight, x / draw.scale - size.win.width * $options.focuswidth, { fps: 100, duration: event.type == 'mousemove'? 0 : $options.duration ); function draw() { var scalex = $options.width / size.doc.width, scaley = $options.height / size.doc.height;
draw.scale = scalex < scaley? scalex : scaley; $context.clearrect(0, 0, $canvas.width, $canvas.height); $context.scale(1 / draw.scale, 1 / draw.scale);... )(window, document); 위 코드를 구글 Closure 컴파일러로 압축하면 다음과 같습니다. var Minimaps=function(h,d){function g(e){var f=e.clientx-b.left,d=e.clienty-b.top;g.ani&&g.ani.state=="running"&&g.ani. cancel();g.ani=new Effect.Scroll(d/a.scale-size.win.height*c.focusHeight,f/a.scale-size.win. width*c.focuswidth,{fps:100,duration:e.type=="mousemove"?0:c.duration) function a(){var e=c.width/size.doc.width,d=c.height/size.doc.height;a.scale=e<d?e:d;f. clearrect(0,0,b.width,b.height);f.scale(1/a.scale,1/a.scale)var c={width:100,height:400,duration:0.2,focuswidth:0.5, focusheight:0.5,b,f;return function(e){c=$.extend(c,e);b=new Element("canvas",{id:"outline",width:c.width,height:c.height);if(!(b instanceof HTMLElement) b.nodename.tolowercase()!=="canvas")return console.error("your browser does not support Canvas");h.body.insert(b);this.version="0.1.1";this.draw=a;this.scroll=g;f= b.getcontext("2d");event.observe(d,"load",a);event.observe(d,"resize",a); Event.observe(d,"scroll",a);a()(window,document); 제가 무엇을 말하고 싶은지 눈치 까셨나요? 네 그렇습니다. 저는 제가 만든 프로그램이 OO(object-oriented) 스럽게 작성된 클래스이면서 한 인스턴스로 작동되길 바랍니다. 그러나 코딩 스타일은 단순 행동 함수들로 구성된 전형적인 모듈 패턴이지만 초기화 함수를 반환하는 것으로 게으른
함수 정의 패턴(Lazy Function Definition Pattern)을 혼용하는 복합적인 모습입니다. 이러한 코딩은 여러가지 이점이 있습니다. 원하던 대로 인스턴스를 만들어 사용하는 동시에, 컴파일(압축)하기 좋은 구조를 가지며, 변수들을 자유롭게 포메이션할 수 있고, 행동에 따른 디자인 패턴으로 작성되기 때문에 이해하기 쉽습니다. 그리고 무엇보다도 졸라 빠릅니다. 다음 편에 계속... Powered by TCPDF (www.tcpdf.org)