JAVASCRIPT
Anti Patterns
FE개발팀 김동우
Anti Pattern?
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
- lib
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 과정
- HTML과 CSS를 파싱하여 DOM Tree와 Style 문맥을 생성
- DOM Tree와 Style 문맥을 기반으로 엘리먼트의 색상, 면적 등의 정보를 가진 Render Tree를 생성
- Render Tree를 기반으로 각 노드가 화면의 정확한 비치에 표시되도록 배치 (Reflow)
- 배치된 노드들의 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?
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