JAVASCRIPT 

Anti Patterns

FE개발팀 김동우

Anti Pattern?

  • 습관적으로 사용하는 코드 중 사용을 지양하는 패턴
    • 성능, 디버깅, 잠재적 오류, 가독성 등의 문제
  • 자바스크립트는 Java나 C와 다른 면이 많음
    • 제대로 이해하고 쓰지 않으면 위험
  • FE개발팀 - 안티 패턴

1. Hoisting

DON'T :

변수나 함수를 선언하기 전에 사용

doSomething();

function doSomething() {
    foo1 = foo2;
    ...
    var foo1 = 'value1';
    foo3 = foo4;
    ...
    var foo4 = 'value3';
    var foo2;
}

WHY?

  • 모든 변수나 함수 선언은 Parsing 단계에서 상단으로 끌어올려짐 => Hoisting
  • 선언되기 전의 변수나 함수를 사용할 수 있음
  • 실제로 엔진이 실행하는 내용과 코드의 내용이 다르게 됨
    • 가독성이 떨어짐
    • 잠재적인 오류 발생 가능성
// 함수를 사용하기 전에 선언
function avoidHoisting() {
    // 함수내에서 사용하는 모든 변수를 함수 상단에서 한번에 선언
    var foo1 = 'value1',
        foo2,
        foo3 = foo4 = 'value3';
    ...
}

avoidHoisting();

DO :

상단에서 변수나 함수를 먼저 선언

DON'T :

분기문, 조건문 내에서 변수 선언

function voteUserChoice(votreeId, voteMode) {
    var testResult = validateCheck();
    var userId = ${session.userId};

    if(testResult) {
        var requestBodyData = {};
        var voteList = [];
        var votesLength = $('.vote-tab').size();

        requestBodyData.votreeId = votreeId;
        for (var i = 1 ; i <= votesLength; i++) {
            var voteInfo = {};
            var userSelectionList = [];
            ...
        }
    ...
    }
}

WHY?

  • Block Scope (X)
  • Function Scope (O)
var x = 10;

function scope() {
    var x = 20;

    if (true) {
        var x = 30;
        console.log(x);  // 30
    }
    console.log(x);  // 30
}
scope();

console.log(x);  // 10

DO :

함수의 상단에서 모든 변수 선언

function voteUserChoice(votreeId, voteMode) {
    var testResult, userId,
        requestBodyData, voteList, voteLength,
        i, voteInfo, userSelectionList;
    
    userId = ${session.userId};
    requestBodyData, voteList,

    if (testResult) {
        requestBodyData = {};
        voteList = [];
        votesLength = $('.vote-tab').size();

        requestBodyData.votreeId = votreeId;
        for (i = 1 ; i <= votesLength; i++) {
            voteInfo = {};
            userSelectionList = [];
            ...
        }
    ...
    }
}

2. Global Scope

DON'T :

Global Scope에 변수, 함수 선언

// comment_status.js

function setMemberStatus(name_path, img_path) {
  var loginBtn = document.getElementById('header_login');
  var img = document.getElementById('header_mem_img');
  var logout = document.getElementById('logout');
  var name = document.getElementById('header_name');
  var div = document.getElementById("header_status_div");

  div.style.visibility="visible";
  loginBtn.style.visibility="hidden";
  
  name.setAttribute("value", name_path);
  img.setAttribute("src", img_path);
}

function setLogout() {
  var loginBtn = document.getElementById('header_login');
  var img = document.getElementById('header_mem_img');
  var logout = document.getElementById('logout');
  var name = document.getElementById('header_name');
  var div = document.getElementById("header_status_div");

  div.style.visibility="hidden";
  loginBtn.style.visibility="visible";
  name.setAttribute("title", " ");
  img.setAttribute("src", " ");
}
<!-- main.jsp -->

<script type="text/javascript">
    var $page = $('.page');
    $('.menu_toggle').click(function() {
      $page.toggleClass('menu_effect')
    });

    //interval 수정 (너무 빨리 전환됨)
    $('.carousel').carousel({
      interval : 30000
    //changes the speed
    })
    ...
</script>

<script type="text/javascript">
    var weightList = [];
    var categoryList = [];
    var data = ${topKeywords };

    for (var day = 1; day < 5; day++) {
      var categoryObject = {
        name : day + 'day ago'
      };
    ...
</script>

DON'T :

Global Scope에 변수, 함수 선언

DON'T :

var 키워드 생략

function sayHello() {
    var name = 'NHN';
    var greeting = 'Hello!';
    var message;

    messge = greeting + ' My name is ' + name;

    return message;
}

console.log(sayHello()); // undefined
console.log(messge);  // 'Hello! My name is NHN'
console.log(window.messge); // ?

WHY?

  • 함수 내부에 선언되지 않은 모든 변수 및 함수는 Global Scope에 할당됨
  • var 키워드를 생략한 경우 자동적으로 Global Scope에 할당됨
  • 브라우저 환경의 Global Scope => window
var name = 'NHN';
function hello() {
    return 'Hello~';
};

console.log(window.name); // 'NHN'
console.log(window.hello()); // 'Hello~'

DO :

즉시 실행 함수 표현식(IIFE) 사용

<!-- main.jsp -->

<script type="text/javascript">
(function() {
    var $page = $('.page');
    $('.menu_toggle').click(function() {
      $page.toggleClass('menu_effect')
    });

    //interval 수정 (너무 빨리 전환됨)
    $('.carousel').carousel({
      interval : 30000
    //changes the speed
    })
    ...

})();
</script>

<script type="text/javascript">
(function() {
    var weightList = [];
    var categoryList = [];
    var data = ${topKeywords };

    for (var day = 1; day < 5; day++) {
      var categoryObject = {
        name : day + 'day ago'
      };
    ...
})();
</script>

DO :

네임스페이스 사용

(function() {
    var ns = tui.util.defineNamespace('basecamp.util');
    /*
    var ns;
    window.basecamp = {};
    window.basecamp.util = {};
    ns = window.basecamp.util;
    */

    ns.isNull = function(value) {
      console.log('isNull', value);
    };
    
    ns.validate = function() {
      console.log('validate!');
    }
})();

basecamp.util.isNull(1);  // isNull 1
basecamp.util.validate();  // validate

DO :

모듈 관리 라이브러리 사용

// AMD (require.js)
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {
    var myModule = {};
    // ...
    return myModule;
});
// CommonJS (browserify)
var dep1 = require('dep1');
var dep2 = require('dep2');

var myModule = {};
// ...
module.exports = myModule;

3. == vs ===

DON'T :

==

123     ==      "123"           // true
0	==	"0"		// true
0	==	""		// true
""	==	"0"		// false

0       ==      false           // true
1       ==      true            // true
2       ==      true            // false

false	==	"false"		// false
false	==	"0"		// true
false   ==      ""              // true
false	==	undefined	// false
false	==	null		// false
null	==	undefined	// true

WHY?

  • == 은 자동으로 형변환을 실행 (type coercion)
  • 타입에 따라 적용되는 룰이 다름
  • 예상못한 결과가 나오는 경우가 많음

DO :

===

123     ===     "123"           // false
0	===	"0"		// false
0	===	""		// false
""	===	"0"		// false

0       ===     false           // false
1       ===     true            // false
2       ===     true            // false

false	===	"false"		// false
false	===	"0"		// false
false   ===     ""              // false
false	===	undefined	// false
false	===	null		// false
null	===	undefined	// false
if (Number(strValue) === 1) {
    ...
}

if (String(numValue) === '10') {
    ...
}

DO :

명시적 형변환 사용

4. null vs undefined

DON'T :

undefined 값을 변수에 할당

var name = undefined;  // X
var myObject = {
    prop1: 1,
    prop2: 2
};
myObject.prop2 = undefined; // X

null !== undefined

  • undefined는 변수가 선언되었지만 값이 초기화되지 않은 상태를 뜻함
  • null은 해당 변수가 명시적으로 아무 값도 갖고 있지 않음을 뜻함
var name;
name === undefined;    // true
name === null;         // false

function myFn(param1, param2) {
    console.log(param1 === null); // true
    console.log(param2 === undefined); // true
}
myFn(null);

var obj = {};
obj.a === undefined    // true

Built-in Types

null, undefined, boolean,

number, string, object, (symbol)

typeof null        // 'object' (bug! should be null!)
typeof undefined   // 'undefined'
typeof true        // 'boolean'
typeof 42          // 'number'
typeof "42"        // 'string'
typeof {}          // 'object'

typeof Symbol()    // 'symbol' (ES6)

var name = null;
var myObject = {
    prop1: 1,
    prop2: 2
};
delete myObject.prop2;

DO :

프라퍼티를 삭제할 땐 delete 사용

빈 값을 명시할 때는 null 사용

5. Wrapper Object

DON'T :

new String(), new Number(),

new Boolean()

function gfn_isNull(str) {
    if (str == null) return true;
    if (str == "NaN") return true;
    if (new String(str).valueOf() == "undefined") return true;    
    
    var chkStr = new String(str);
    if (chkStr.valueOf() == "undefined") return true;
    if (chkStr == null) return true;    
    if (chkStr.toString().length == 0) return true;   
    
    return false; 
}

WHY?

  • new를 사용하면 새로운 객체(wrapper object)가 생성이 되어, 타입이나 값을 비교할 때 혼란을 줄 수 있음
  • primitive 값을 사용해도 wrapper object의 메소드를 사용할 수 있음
var wrapper = new String('hello');
typeof 'hello'; // 'string'
typeof wrapper; // 'object'

wrapper === new String('hello'); // false
wrapper === 'hello'; // false
wrapper == 'hello'; // true
'hello' === 'hello' // true

'hello'.toUpperCase(); // 'HELLO';

DO :

String(), Number(), Boolean()

function gfn_isNull(value) {
    var str = String(value);
    if (!str || str === 'null' || str === 'undefined' || str === 'NaN')
        return true;
    }
    return false;
}

String(undefined); // 'undefined'
String(null);      // 'null'
String(0/0);       // 'NaN'
String(true);      // 'true'

Number('123');     // 123
Number('hello');   // NaN
Boolean(1);        // true

6. eval()

DON'T :

eval() 사용

var myValue = eval("myObject." + myKey);
var data = eval('(' + jsonStr + ')'); 

DO :

eval() 무조건 사용 금지

var myValue = myObject[myKey];
var data = JSON.parse(jsonStr);

WHY?

  • 코드를 이해하기 어려움
  • 실행속도를 현저하게 느리게 만듬
    • 파싱단계에서는 문자열 상태
    • 실행시점에 파싱됨 -> 파서를 재기동 해야함
  • 심각한 보안문제를 만들 수 있음
  • 사실상 사용금지된 키워드
    • strict 모드에서 에러로 인식

DON'T :

setTimeout 에서 함수 대신 문자열 사용

function callback() {
...
}
setTimeout('callback()', 1000);
setTimeout(callback, 1000);

//or
setTimeout(function() {
...
}, 1000);

DO :

함수명이나 함수 표현식 사용

DON'T :

new Function() 사용

var doSomething = new Function('param1', 'param2', 'return param1 + param2;');

DO :

함수 선언식, 함수 표현식 사용

// 함수 선언식 (Function statement)
function doSomething(param1, param2) {
    return param1 + param2;
}

// 함수 표현식 (Function expression)
var doSomething = function(param1, param2) {
    return param1 + param2;
}

WHY?

  • setTimeout, setInterval 의 인수로 문자열을 넘길 경우 eval처럼 동작함
  • new Function() 을 사용하면 eval처럼 동작함

7. parseInt()

DON'T :

기수없이 parseInt 사용

function getDate(strMonth, strDay) {
    var month = parseInt(strMonth);    // 0
    var day = parseInt(strDay);        // 0
    ...
}
var today = getDate('08', '09');


WHY?

  • 기수(radix)를 생략할 경우 '0'으로 시작하는 문자열을 8진수로 인식함
  • ES5에서는 수정되었으나, 구형 브라우저(IE8 이하)에서는 여전히 문제 발생
parseInt('08', 10);    // 8
parseInt('09', 10);    // 9

Number('08')           // 8
Number('09')           // 9

DO :

항상 기수(radix)를 명시

형변환은 Number() 사용

8. Array

for (var i = 0; i < player.length; i++) {
    players[i].score++;
}

var names = [];
for (var i =0; i < player.length; i++) {
    names.push(player.name);
}

DO :

for문 대신 forEach, map 메소드 사용

players.forEach(function(player) {
    player.score++;
});

var names = players.map(function(player) {
    return player.name;
});

// tui.util.forEach(players, function...
// tui.util.map(players, function...

WHY?

  • for 문을 사용하기 위한 중복 로직 제거
  • 간결하고 가독성이 좋음
  • 종료조건 실수에 따른 오류 방지
  • 반복문 내에서 독립적인 Scope를 생성 
  • 자바스크립트에서 사실상의 표준이 되어가고 있음
var scores = [70, 75, 80, 61, 89, 56, 77, 83, 90, 93, 66],
    total = 0;

// bad
for (var i = 0; i < scores.length; i ++) {
    total += scores[i];
}

// good
for (var i = 0, len = scores.length; i < len; i++) {
    total += scores[i];
}

DO :

for문 사용시 length 캐쉬

WHY?

  • 반복문의 경우 작은 차이가 큰 성능차이를 만듬
  • array.length에 접근하는 비용이 변수를 직접 사용하는 비용보다 큼

9. Native Object

DON'T :

Native Object 직접 수정 

(monkey patching)

Object.prototype.getKeys = function() {
    var keys = [];
    for (var key in this) {
        keys.push(key);
    }
    return keys;
}

var keys = myObject.getKeys();

WHY?

  • Native Object를 수정할 경우 다른 라이브러리에 영향
  • 브라우저가 예고없이 변경할 수 있음
  • 예측할 수 없는 오류발생 가능성 증가
  • 경우에 따라 Polyfil 은 허용 (하위 브라우저 호환을 위해)
if (typeof Array.prototype.map !== 'function') {
    Array.prototype.map = function(f, thisArg) {
        var result = [];
        for (var i =0, n = this.length; i < n; i++) {
            result[i] = f.call(thisArg, this[i], i);
        }
        return result;
    };
}
function getKeys(obj) {
    var keys = [];
    for (var key in obj) {
        keys.push(key);
    }
    return keys;
}

var keys = getKeys(myObject);

DO :

유틸리티 함수 형태로 작성

10. Event Handler

DON'T :

onclick 속성에 함수 지정

<div class="row">
    <div class="col-sm-4">
        <a href="#" onclick="oauthLogin('FACEBOOK')"> 
        <img class="img-responsive" src="image/facebook.png"/>페이스북</a>
    </div>
    <div class="col-sm-4">
        <a href="#" onclick="oauthLogin('TWITTER')"> 
        <img class="img-responsive" src="image/twitter.png">트위터</a>
    </div>
    <div class="col-sm-4">
        <a href="#" onclick="oauthLogin('PAYCO')">
        <img class="img-responsive" src="image/payco.jpg">페이코</a>
    </div>
</div>

DON'T :

<a href="javascript:..." 사용

<ul class="list-group">
    <c:forEach items="${categoryList }" var="row">
        <li class="list-group-item">
            <a href="javascript:search('','${row.category_name }','${pageContext.request.contextPath}')">${row.category_name }</a>
        </li>
    </c:forEach>
</ul>

WHY?

  • 유지보수가 어려움
    • HTML과 Javascript의 결합도가 증가함
    • jquery등을 이용하는 방식과 혼용될 경우 일관성을 유지하기 어려움
  • 브라우저에 따라 실행 Scope가 달라질 수 있음
  • 함수가 정의되기 전에 핸들러가 실행될 수 있음
<div class="row">
    <div class="col-sm-4">
        <a href="#" class="btn-login" data-type="FACEBOOK">
        <img class="img-responsive" src="image/facebook.png"/>페이스북</a>
    </div>
    <div class="col-sm-4">
        <a href="#" class="btn-login" data-type="TWITTER">
        <img class="img-responsive" src="image/twitter.png">트위터</a>
    </div>
    <div class="col-sm-4">
        <a href="#" class="btn-login" data-type="PAYCO">
        <img class="img-responsive" src="image/payco.jpg">페이코</a>
    </div>
</div>
<script>
$('.btn-login').click(function(e) {
    oAuthLogin($(this).data('type'));
    e.preventDefault();
});
</script>

DO :

Javascript 에서 핸들러 등록

11. jQuery selector

DON'T :

반복된 jquery 셀렉터 사용

$('#graph').css("display","none");
$('#graph').empty();

$('#vote-item-image').css("visibility","hidden");
$('#vote-item-image').attr("src","");
$('#vote-item-image').css("display","none");
$('#vote-item-video').css("display","none");
$('#vote-item-video').attr("src","");

if($('#graph').css("display") == "none"){
    $('#graph').css("display","inline-block");
    $('#graph').empty();
    if($(this).find('#vote-item-hidden-category').val() == 2){
        $('#vote-item-image').css("visibility","visible");
        $('#vote-item-image').attr("src",fileStorageUrl+fileName);
        $('#vote-item-image').css("display","inline-block");
    }
    if($(this).find('#vote-item-hidden-category').val() == 3){
        $('#vote-item-video').attr("src","https://www.youtube.com/embed/"+fileName);
        $('#vote-item-video').css("display","inline-block");
    }
}

DON'T :

반복된 $(this) 사용

$(".votree-box").mouseenter(function(event) {
    var type = $(this).attr("value");

    if(type == 2){
        $(this).parents().find(".votree-hidden-form").css("display","none");
        $(this).parents().find(".fa").css("display","inline");
      
        $(this).children().css("display","none");
        $(this).find("#span-title").css("display", "block");
      
        $(this).find(".votree-hidden-form").css("display","inline");
    }
});

WHY?

  • jQuery selector를 사용할 때마다 DOM 탐색이 일어남
    • DOM 탐색은 느림
  • HTML 구조가 변경될 경우 모든 selector를 수정해 주어야 함
  • $(this) 를 할 때마다 새로운 jquery 객체가 생성됨
    • 불필요한 객체 생성에 따른 퍼포먼스 및 메모리 낭비
$(".votree-box").mouseenter(function(event) {
    var $this = $(this);
    var type = $this.attr("value");

    if(type == 2){
        $this.parents().find(".votree-hidden-form").css("display","none");
        $this.parents().find(".fa").css("display","inline");
      
        $this.children().css("display","none");
        $this.find("#span-title").css("display", "block");
      
        $this.find(".votree-hidden-form").css("display","inline");
    }
});

DO :

변수에 저장해서 사용

var $graph = $('#graph'),
    $voteImage = $('vote-item-image'),
    $voteVideo = $('vote-item-video'),
    hiddenCategoryValue = $(this).find('#vote-item-hidden-category').val();
    
$graph.css('display', 'none').empty();
$voteImage.attr('src', '').css({
    visibility: 'hidden',
    display: 'none'
});
$voteVideo.attr('src', '').css('display', 'none');

if ($graph.css('display') === 'none') {
    $graph.css('display', 'inline-block').empty();
    if (hiddenCategoryValue === 2) {
        $voateImage.attr('src', fileStorageUrl + fileName).css({
            visibility: 'visible'
            display: 'inline-block'
        });
    } else if (hiddenCategoryValue === 3) {
        $voateVideo.attr('src', 'https://www.youtube.com/embed/' + fileName)
            .css('display', 'inline-block');
    }
}

DO :

Chaining 사용

DO :

selector 최적화

12. display:none

DON'T :

display 속성 직접 읽고 쓰기

var $graph = $('#graph'),
    $voteImage = $('vote-item-image'),
    $voteVideo = $('vote-item-video'),
    hiddenCategoryValue = $(this).find('#vote-item-hidden-category').val();
    
$graph.css('display', 'none').empty();
$voteImage.attr('src', '').css({
    visibility: 'hidden',
    display: 'none'
});
$voteVideo.attr('src', '').css('display', 'none');

if ($graph.css('display') === 'none') {
    $graph.css('display', 'inline-block').empty();
    if (hiddenCategoryValue === 2) {
        $voateImage.attr('src', fileStorageUrl + fileName).css({
            visibility: 'visible'
            display: 'inline-block'
        });
    } else if (hiddenCategoryValue === 3) {
        $voateVideo.attr('src', 'https://www.youtube.com/embed/' + fileName)
            .css('display', 'inline-block');
    }
}

WHY?

  • none 상태를 해제할 때 display 타입을 기억해서 'inline', ' block' 등으로 적절한 값을 지정해야 함
  • 코드가 직관적이지 않음
  • hide(), show() 메소드를 사용하면 display 타입에 관계없이 직관적으로 작성 가능

DO :

show(), hide(), is(':hidden') 사용

var $graph = $('#graph'),
    $voteImage = $('vote-item-image'),
    $voteVideo = $('vote-item-video'),
    hiddenCategoryValue = $(this).find('#vote-item-hidden-category').val();
    
$graph.hide().empty();
$voteImage.attr('src', '').hide();
$voteVideo.attr('src', '').hide();

if ($graph.is(':hidden')) {
    $graph.show().empty();
    if (hiddenCategoryValue === 2) {
        $voateImage.attr('src', fileStorageUrl + fileName).show();
    } else if (hiddenCategoryValue === 3) {
        $voateVideo.attr('src', 'https://www.youtube.com/embed/' + fileName).show();
    }
}

13. DOCTYPE

DON'T :

구형 DOCTYPE 사용

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
"http://www.w3.org/TR/html4/loose.dtd">

DO :

HTML5 DOCTYPE 사용

<!DOCTYPE html>

WHY?

  • 짧고 간단함
  • HTML5를 지원하지 않는 브라우저도 표준모드로 렌더링됨

14. use strict;

(function(){
    'use strict';
  
    ...
})();

DO :

'use strict'; 사용 

'use strict';

...
  • 파일 전체에 적용
  • 함수 스코프에 적용

WHY?

  • Javascript를 표준모드에서 실행시킴
  • 잠재적인 오류를 방지할 수 있음
    • 선언하지 않은 변수 사용 방지
    • 읽기전용 속성에 값 할당 방지
    • 객체 리터럴에 중복된 프로퍼티 할당 방지
    • 삭제 불가능한 프라퍼티 삭제 방지
    • with, eval 키워드 사용 방지
    • 8진법 리터럴 사용 방지

15. Include Javascript

DON'T :

head에서 <script> include

<!DOCTYPE html>
<html>
    <head>
        <title>HTML Page</title>
        <script type="text/javascript" scr="../js/jquery-1.8.3.min.js"></script>
        <script type="text/javascript" scr="../js/common.js"></script>
        <script type="text/javascript" scr="../js/applicationMain.js"></script>
    </head>
    ...

WHY?

  • Javascript를 다운받아 실행하기 전까지 렌더링이 멈춤
    • 사용자가 빈 화면을 보는 시간이 길어짐
<!DOCTYPE html>
<html>
    <head>
        <title>HTML Page</title>
    </head>
    <body>
        ...
        <!-- body 요소 안, 맨 마지막에 씀 -->
        <script type="text/javascript" scr="../js/jquery-1.8.3.min.js"></script>
        <script type="text/javascript" scr="../js/common.js"></script>
        <script type="text/javascript" scr="../js/applicationMain.js"></script>
    </body>
</html>

DO :

body가 끝나기 직전에 include

16. External Library

DON'T :

외부 서버에 있는 파일 include

<!-- css -->
<link rel="stylesheet" href="//code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css">
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/bootstrap.css">
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/header.css">
<!-- end css -->
    
<!-- js -->
<script src="${pageContext.request.contextPath}/js/jquery.js"></script>
<script src="${pageContext.request.contextPath}/js/bootstrap.js"></script>
<script src="//code.jquery.com/ui/1.11.4/jquery-ui.js"></script>
<script src="${pageContext.request.contextPath}/js/header.js"></script>

WHY?

  • 외부 서버의 URL 변경이나 장애발생시 서비스에 영향
  • DNS Lookup 비용 추가 발생
    • Javascript의 경우 렌더링이 block됨
<!-- css -->
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/jquery-ui.css">
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/bootstrap.css">
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/header.css">
<!-- end css -->
    
<!-- js -->
<script src="${pageContext.request.contextPath}/js/jquery.js"></script>
<script src="${pageContext.request.contextPath}/js/bootstrap.js"></script>
<script src="${pageContext.request.contextPath}}/js/jquery-ui.js"></script>
<script src="${pageContext.request.contextPath}/js/header.js"></script>

DO :

외부 JS 파일은 직접 다운로드 해서 사용

DON'T :

라이브러리와 소스파일을 같은 폴더에 관리

  • js
    • article.js
    • bootstrap.js
    • bootstrap.min.js
    • code-snippet.js
    • sidebar.js
    • top.js

WHY?

  • 라이브러리와 소스 파일의 구분이 어려움
  • 라이브러리가 많아질 수록 관리가 어려움

DO :

라이브러리는 lib 폴더에 따로 관리

  • js
    • lib
      • bootstrap.js
      • code-snippet.js
      • jquery.js
    • article.js
    • sidebar.js
    • top.js

17. Ctrl+C, Ctrl+V 코드

DON'T :

외부에서 복사해온 코드 사용

function validateEmail(sEmail) {
    var filter = /^([a-z\d!#$%&'*+\-\/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+(\.[a-z\d!#$%&'*+\-\/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)*|"((([ \t]*\r\n)?[ \t]+)?([\x01-\x08\x0b\x0c\x0e-\x1f\x7f\x21\x23-\x5b\x5d-\x7e\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|\\[\x01-\x09\x0b\x0c\x0d-\x7f\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))*(([ \t]*\r\n)?[ \t]+)?")@(([a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|[a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF][a-z\d\-._~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]*[a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])\.)+([a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|[a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF][a-z\d\-._~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]*[a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])\.?$/i;
    if (filter.test(sEmail)) {
	return true;
    }
    else {
	return false;
    }
}

WHY?

  • 코드를 신뢰할 수 없음
  • 라이센스 문제
  • 만약 내부 구현방식을 아무도 모른다면..
    • 버그 발생시 누가 수정?

DO :

  • 공인된 오픈소스 라이브러리를 사용
  • 부득이한 경우
    • 내부 구조를 이해할 수 있는 코드만
    • 팀 컨벤션에 맞게 수정하여 사용
    • 출처를 주석으로 명시
    • 라이센스 확인

18. JSP & Javascript

DON'T :

JSP 문법과 JS 문법 섞어 쓰기

if('${param.categoryCond}' != '') {
    var categoryCond = '${param.categoryCond}';	
} else {
    var categoryCond = '';
}
$.get("<c:url value='/articles' />" + "?page=" + page
    + "&pageItem=10" + searchQuery, function(data, status) {
    var totalItemCount = 1;
    ...
});

WHY?

  • 코드 복잡도가 증가 -> 가독성이 나빠짐
  • 유지보수가 어려움
  • JS를 외부 파일로 분리하기 어려움
<script>
var articleId = ${scrap.articleId};
var scrapId = ${scrap.scrapId};
var url = "${scrap.shareUrl}";
var scrapUid = ${scrap.userId};
var userUid = ${user.userId};
</script>

DO :

Javascript에 데이터만 전달하여 로직 분리

<script id="data" type="application/json">// JSON String //</script>
...
<script>
var data = JSON.parse($("#data").html());
...
</script>

19. Extract method

DON'T :

하나의 함수에서 모두 처리

function createModal(data) {
    var tabNum = data.subVoteList.length;
    for (var i = 1; i <= tabNum; i++) {
        $("#tabs").append('<li class="delete" role="presentation" id="tab' + i + '"></li>');
        if (i == 1) {
            var $activeTab = $("#tabs").find("#tab1");
            $activeTab.addClass("active");
        }
        $("#tab" + i).append('<a id="vote-tab-' + i + '" href="#vote-' + i + '" aria-controls="vote-' + i + '"class="delete tabs-list" role="tab" data-toggle="tab"></a>');
        $("#vote-tab-" + i).text("투표" + i);
    }
    for (var i = 1; i <= tabNum; i++) {
        $("#voteTitle").append('<div role="tabpanel" class="delete vote-tab tab-pane" id="vote-' + i + '"></div>');
        if (i == 1) {
            //TODO REVIEW
            var inActive = $("#voteTitle").find("#vote-1");
            inActive.addClass("in active");
        }
        $('#vote-' + i).append('<div id ="tabC' + i + '" class="delete vote-attribute col-md-12"></div>');
        $('#tabC' + i).append('<label class="delete" for="vote-topic">투표 주제</label>' + '<input type="text" value="' + data.subVoteList[i - 1].topic + '"class="delete vote-topic form-control" id="vote-name-' + i + '" disabled>' + '<label class="delete" for="vote-question">투표 항목</label>');
        $('#vote-' + i).append('<div class="delete vote-attribute col-md-5 pull-right"></div>');

        //Question List
        //TODO REVIEW
        for (var j = 1; j <= data.subVoteList[i - 1].voteItemList.length; j++) {
            $('#vote-' + i).append('<div class="delete question-list" id="vote-' + i + '-attribute-' + j + '"></div>');
            $('#vote-' + i + '-attribute-' + j).append('<div class="delete pull-left question-number">' + j + '</div>');
            $('#vote-' + i + '-attribute-' + j).append('<div id="category' + i + '-' + j + '" class="delete vote-attribute col-md-5"></div>');
            $('#category' + i + '-' + j).append('<select id="vote-' + i + '-question-type-' + j + '" class="delete vote-item-type form-control" disabled></select>');
            switch (data.subVoteList[i - 1].voteItemList[j - 1].categoryId) {
                case 1:
                    $('#vote-' + i + '-question-type-' + j).append('<option>텍스트</option>');
                case 2:
                    $('#vote-' + i + '-question-type-' + j).append('<option>이미지</option>');
                case 3:
                    $('#vote-' + i + '-question-type-' + j).append('<option>동영상</option>');
                default:
                    $('#vote-' + i + '-question-type-' + j).append('<option>타입오류</option>');
            }
            $('#vote-' + i + '-attribute-' + j).append('<div id="inputdata' + i + '-' + j + '" class="delete vote-item-value col-md-12"></div>');
            $('#inputdata' + i + '-' + j).append('<input type="text" class="delete vote-item-input form-control" id="vote-' + i + '-question-' + j + '" value="' + data.subVoteList[i - 1].voteItemList[j - 1].value + '" disabled>');
            $('#vote-' + i + '-question-' + j).append('<span class="delete" id="vote-' + i + '-value-err-msg-' + j + '"></span>')
        }
    }
    $('#sidebar-title-time').append('<label class="delete" for="votree-name">투표 이름 </label> <input type="text" class="delete form-control" id="votree-name" value="' + data.title + '">');

    var startdate = new Date(data.startDatetime);
    var startminutes = "0" + startdate.getMinutes();
    var startseconds = "0" + startdate.getSeconds();
    var formattedstartTime = startdate.getFullYear() + '-' + (('0' + (startdate.getMonth() + 1)).slice(-2)) + '-' + (('0' + (startdate.getDate())).slice(-2)) + ' ' + (('0' + startdate.getHours()).slice(-2)) + ':' + startminutes.substr(-2) + ':' + startseconds.substr(-2);
    $('#sidebar-title-time').append('<label class="delete" for="start-datetime">시작 시간</label> <input class="delete" id="start-datetime" type="text" value="' + formattedstartTime + '">');
    $('#sidebar-title-time').append(
        '<div id="layer" class="layer delete" style="display: none;">' + '<div class="calendar-header">' + '<a href="#" class="rollover calendar-btn-prev-month">이전달</a>' + '<strong class="calendar-title"></strong>' + '<a href="#" class="rollover calendar-btn-next-month">다음달</a>' + '</div>' + '<div class="calendar-body">' + '<table cellspacing="0" cellpadding="0">' + '<thead>' + '<tr>' + '<th class="sun">S</th>' + '<th>M</th>' + '<th>T</th>' + '<th>W</th>' + '<th>T</th>' + '<th>F</th>' + '<th class="sat">S</th>' + '</tr>' + '</thead>' + '<tbody>' + '<tr class="calendar-week">' + '<td class="calendar-date"></td>' + '<td class="calendar-date"></td>' + '<td class="calendar-date"></td>' + '<td class="calendar-date"></td>' + '<td class="calendar-date"></td>' + '<td class="calendar-date"></td>' + '<td class="calendar-date"></td>' + '</tr>' + '</tbody>' + '</table>' + '</div>' + '<div class="calendar-footer">' + '<p>오늘 <em class="calendar-today"></em></p>' + '</div>'
    )



    var enddate = new Date(data.dueDatetime);
    var endminutes = "0" + enddate.getMinutes();
    var endseconds = "0" + enddate.getSeconds();
    var formattedendTime = enddate.getFullYear() + '-' + (('0' + (enddate.getMonth() + 1)).slice(-2)) + '-' + ('0' + (enddate.getDate())).slice(-2) + ' ' + enddate.getHours() + ':' + endminutes.substr(-2) + ':' + endseconds.substr(-2);
    $('#sidebar-title-time').append('<label class="delete" for="start-datetime">종료 시간</label> <input class="delete" id="due-datetime" type="text" value="' + formattedendTime + '">');
    $('#sidebar-option').append('<li class="delete sidebar-item list-group-item" id="options">비밀 투표</li>');
    $('#options').append('<div class="delete material-switch pull-right" id="sidevar-private"></div>');

    if (data.plainPassword != null) {
        $('#sidevar-private').append('<input class="delete" id="is-private" name="isPrivate" type="checkbox" checked />');
        $('#options').append('<input type="text" class="delete form-control" id="private-password" value="' + data.plainPassword + '">');
    } else {
        $('#sidevar-private').append('<input id="is-private" class="delete" name="isPrivate" type="checkbox"/>');
    }
    $('#sidevar-private').append('<label for="is-private" class="delete label-primary"></label>');

    $('#sidebar-option').append('<span class="delete" id="password-err-msg"></span>');

    $('#config').append('<div class="delete vote-flexible-config"></div>');
    for (var k = 1; k <= tabNum; k++) {
        if (k != 1) {
            $('.vote-flexible-config').append('<li id="is-duplicate-' + k + '" class="delete sidebar-item list-group-item" style="display:none" >중복투표</li>');
        } else {
            $('.vote-flexible-config').append('<li id="is-duplicate-' + k + '" class="delete sidebar-item list-group-item">중복투표</li>');
        }

        $('#is-duplicate-' + k).append('<div id = "checkbox' + k + '" class="delete material-switch pull-right"></div>');

        if (data.subVoteList[k - 1].isDuplicate == 1) {
            $('#checkbox' + k).append('<input class="delete label-primary" id="is-duplicate-' + k + '" name="isDuplicate" type="checkbox" checked />');
        } else {
            $('#checkbox' + k).append('<input class="delete label-primary" id="is-duplicate-' + k + '" name="isDuplicate" type="checkbox" />');
        }
        $('#checkbox' + k).append('<label class="delete label-primary" for="is-duplicate-' + k + '"></label>');
    }
    $('.vote-regist-tab-content').slimScroll({
        height: '550px'
    });


    setDatetimePicker();

    $('#is-private').click(function(e) {
        if ($(this).is(':checked')) {
            $(e.target).parent().parent().append('<input type="text" class="form-control" id="private-password">');
        } else {
            $('#private-password').remove();
        }

        $("#private-password").keypress(function(e) {
            if (e.which != 8 && e.which != 0 && (e.which < 48 || e.which > 57)) {
                $("#password-err-msg").html("숫자만 가능합니다.").show().fadeOut("slow");
                return false;
            }
            if ($(e.target).val().length > 3) {
                $("#password-err-msg").html(" 4자리만 가능합니다.").show().fadeOut("slow");
                return false;
            }
        });
    });

    $('.tabs-list').on('shown.bs.tab', function(e) {
        var IdNum = e.target.id.substring(9);
        var styleChange = $('.votree-regist-container').find("#is-duplicate-" + IdNum);
        $('.vote-flexible-config .sidebar-item').hide();
        styleChange.show();
    });
}

WHY?

  • ​기능을 추가하거나 제거하기 어려움
  • 디버깅이 어려움
  • 단위 테스트가 어려움

DO :

작은 기능 단위의 함수로 분리

(Extract Method)

20. Reflow, Redraw

Browser Render 과정

  1.  HTML과 CSS를 파싱하여 DOM Tree와 Style 문맥을 생성
  2.  DOM Tree와 Style 문맥을 기반으로 엘리먼트의 색상, 면적 등의 정보를 가진 Render Tree를 생성
  3. Render Tree를 기반으로 각 노드가 화면의 정확한 비치에 표시되도록 배치 (Reflow)
  4. 배치된 노드들의 visibility, outline, color 등의 정보를 시각적으로 표현 (Repaint)

DON'T :

style 속성값을 순차적으로 변경

function changeDivStyle(){
    var sampleEl = document.getElementById('sample');
    sampleEl.style.left = '200px';            // reflow 1회, repaint 1회 발생
    sampleEl.style.width = '200px';           // reflow 1회, repaint 1회 발생
    sampleEl.style.backgroundColor = 'blue';  // repaint 1회 발생
}

// 총 reflow 2회, repaint 3회 발생
changeDivStyle();

WHY?

Repaint와 Reflow 는 느리다!

(특히 Repaint가 가장 큰 비용)

DO :

Repaint와 Reflow 횟수 최소화

function changeDivStyle(){
    // 한번에 모아서 할당
    $('sample').css({
        left: '200px',
        width: '200px',
        backgroundColor: 'blue'
    });
}

// 총 reflow 1회, repaint 1회 발생
changeDivStyle();

21. Coding Convention

WHY?

  • 통일된 스타일을 유지하여 인해 가독성과 생산성을 올림
  • 특히 자바스크립트는..
    • 다른 언어에 비해 유연한 문법 구조를 갖고 있어 좀더 엄격한 규약이 필요
    • 에러발생을 유발하는 몇가지 나쁜 문법들이 존재함
  • Links

DON'T : Allman

if (foo)
{
  bar();
}
else
{
  baz();
}

DO : 1TBS 

if (foo) {
  bar();
} else {
  baz();
}
if (foo) {
  bar();
} 
else {
  baz();
}

DO : stroustrup 

WHY?

  • Javascript 에서의 사실상 표준
  • 세미콜론 자동 삽입 기능으로 인한 오류 위험 방지
return 
{
    name: 'NHN',
    project: 'basecamp'
};
return;
{
    name: 'NHN',
    project: 'basecamp'
};

=>

var str1 = "hi";
var str2 = 'hello';
var htmlStr1 = "<div class\"=selected\">content</div>";
var htmlStr2 = "<div class='selected'>content</div>";

DO : 일관된 ' ' 사용

DON'T : ' ' 와 " " 섞어쓰기

var str1 = 'hi';
var str2 = 'hello';
var htmlStr = '<div class="selected">content</div>';

WHY?

  • 타이핑이 더 쉬움
  • HTML 문자열 생성시에 Escape 할 필요가 없음
  • 일관성을 유지

DON'T : {} 생략

for (i = 1; i <= 5 ; i++) {
    if (pageLength < startIdx * 5 + i )
        $(".pagination").append('<li class="disabled"><a>' + (startIdx * 5 + i) +'</a></li>');
    else
        if ((startIdx * 5 + i) == currentPage ) {
            $(".pagination").append('<li><a><strong>' + (startIdx * 5 + i) +'</strong></a></li>');
        } else {
            $(".pagination").append('<li><a onClick= pageNum_click('+ (startIdx * 5 + i) +') >' + (startIdx * 5 + i) +'</a></li>');
        }
}

DO : 항상 {} 사용

for (i = 1; i <= 5 ; i++) {
    if (pageLength < startIdx * 5 + i ) {
        $(".pagination").append('<li class="disabled"><a>' + (startIdx * 5 + i) +'</a></li>');
    } else if ((startIdx * 5 + i) == currentPage ) {
        $(".pagination").append('<li><a><strong>' + (startIdx * 5 + i) +'</strong></a></li>');
    } else {
        $(".pagination").append('<li><a onClick= pageNum_click('+ (startIdx * 5 + i) +') >' + (startIdx * 5 + i) +'</a></li>');
    }
}

WHY?

  • 로직의 구조를 모호하게 만들어 가독성이 떨어짐
  • 오류를 발생시킬 잠재적 위험이 큼
  • 코드의 일관성을 유지

DON'T :

세미콜론 생략

function parserTest(options) {
    log('test start!!!')
    (options || []).forEach(function(i) {
    ...
    })
}


// parser는 아래와 같이 해석하고, 세미콜론을 삽입
function parserTest(options) {
    log('test start!!!')(options || []).forEach(function(i) {...});
}

WHY?

  • 세미콜론을 생략했을 경우 parser가 자동으로 삽입
    • 세미콜론이 없어도 에러가 발생하지 않음
  • 의도하지 않은 결과가 나올 수 있음
    • 잠재적 오류 발생
function parserTest(options) {
    log('test start!!!');
    (options || []).forEach(function(i) {
    ...
    });
}

DO :

문장의 끝은 꼭 세미콜론 사용

Naming

  • 변수명, 함수명은 CamelCase 사용
  • 상수명은 대문자와 '_'를 사용
  • 생성자 함수는 대문자로 시작
  • 이벤트 핸들러는 on 으로 시작​
  • private 멤버는 '_' 로 시작
var MINIMUM_WIDTH = 10;              // 상수명
function doSomethingImportant() {};  // 함수명

var Member = tui.util.defineClass({  // 생성자
    init: function() {
        this._name = null;           // private 프라퍼티
    },
    _onClick: function() {},         // private 이벤트 핸들러
});
var firstMember = new Member();

적정분석도구 사용

  • 코드를 정적으로 분석하여 문법오류, 안티패턴, 개발자의 실수 등을 찾아주는 도구
  • 실행해보기 전에 잠재적인 오류를 미리 확인하고 수정
  • JSLint, JSHint, JSCS, ESLint
  • 커맨드라인으로 실행 가능 (배포과정에 포함시켜 검사)
  • 에디터에 통합되거나 플러그인 형태로 사용
  • FE개발팀 - 정적 분석

Javascript Anti Patterns

By DongWoo Kim

Javascript Anti Patterns

  • 471