목표
- 개발 시 실수 줄이기
- 모르고 지나칠 수 있던 JS 지식 또는 문법 습득
- 타 개발자가 유지보수 하기 쉽게 개발하기
안티 패턴이란 습관적으로 많이 사용하고 또는 사용해도 상관없는 패턴이지만
성능, 디버깅, 유지보수, 가독성 측면에서 부정적인 영향을 줄 수 있어 지양하는 패턴을 의미한다.
흔히 할 수 있는 실수에 대한 간단한 예시
{}
)를 생략하지 않는다if/while/do/for 문 사용 시 한 줄짜리 블록이면 중괄호({})를 생략하지 않는 것이 좋다.
중괄호가 없을 경우에 제어문의 동작 범위를 한눈에 파악하기 힘들기 때문이다.
// bad
if(true) doSomething();
// good
if(true) {
doSomething();
}
// Bad
function getDateTime(targetDate) {
const month = targetDate.getMonth();
const day = targetDate.getDate();
const hour = targetDate.getHours();
// 임시 변수를 통해 데이터 계산에 대한 유혹에 빠질 수 있게 될 수 있기도 하고
// 타인이 수정을 할 수 없게 하기 위해 연산을 위한 임시 변수를 최대한 생략하도록 한다
return {
month: month >= 10 ? month : '0' + month,
day: day >= 10 ? day : '0' + day,
hour: hour >= 10 ? hour : '0' + hour,
}
}
// Good
function getDateTime(targetDate) {
const { month, day, hour } = getMonthDayHour(targetDate);
return {
month: month >= 10 ? month : '0' + month,
day: day >= 10 ? day : '0' + day,
hour: hour >= 10 ? hour : '0' + hour,
}
}
// 함수를 한번더 추상화 하여 재사용성을 높임
function getMonthDayHour(targetDate) {
return {
month: targetDate.getMonth(),
day: targetDate.getDate(),
hour: targetDate.getHours(),
}
}
**switch
문에서 break
를 생략하지 않는다.**break 키워드를 생략하면, break 키워드를 만날때까지 다음 case절을 연달아 실행하는데,
해당 코드를 읽는 사람에게는 생략이 의도인지, 실수인지 확인하기 힘든 코드가 된다.
가독성은 물론 버그의 원인을 찾기도 힘들어진다.
// Bad - break를 생략하여 case 2일 때는 A()과 B()를 수행하는 코드
switch (foo) {
case 1:
A()
case 2:
B();
break;
...
}
// Good - 다수의 case절이 동일한 기능을 수행할 경우 break 생략 가능
switch (foo) {
case 1:
case 2:
doSomething();
break;
...
}
// Good
switch (foo) {
case 1:
doSomething();
break;
case 2:
doSomethingElse();
break;
...
default:
defaultSomething();
}
for-in
을 사용하지 않는다for-in 은 프로토타입 체인에 있는 모든 프로퍼티를 순회하므로 for를 사용할 때보다 훨씬 느리다.
순회 순서 또한 브라우저에 따라 다르게 구현되어있기 때문에 순회가 항상 순서대로 수행되지 않는
문제를 제기할 수 있다.
const scores = [70, 75, 80, 61, 89, 56, 77, 83, 93, 66];
let total = 0;
// Bad
for (let i in scores) {
total += scores[i];
}
// Good
const { length } = scores;
for (let i = 0; i < length; i += 1) {
total += scores[i];
}
for-in을 사용하고 싶다면 프로토타입을 모두 순회하지 않는 새로나온 for-of를 사용하자.
// for-in > let index in values
// for-of > let value of values
for (let score of scores) {
total += score;
}
var data = [1, 2, undefined, NaN, null, ""];
Array.prototype.getIndex = function () {};
// for in에서 자신의 값 이외의 최상위 값까지 나타냄
// 위와같은 문법이 없으면 가능하지만 없다라는 가정은 없기 때문에 사용을 지향한다.
for (let i in data) {
console.log("forIn: ", data[i]);
}
>>
forIn: 1
forIn: 2
forIn: undefined
forIn: NaN
forIn: null
forIn:
forIn: [Function (anonymous)]
delete
를 사용하지 않는다객채(Object)의 프로퍼티를 삭제할 때 delete를 사용하여 삭제하게 되면 해당 데이터가
undefined로 설정되지 않고 완전히 삭제되어 더 이상 존재하지 않게된다.
배열(Array) 역시 delete를 사용할 수 있지만 배열의 delete는 해당 데이터가 undefined로
변경되고 실제 배열의 길이 또한 줄어들지 않는다.
// Bad
const numbers = ["zero", "one", "two", "three", "four", "five"];
delete numbers[2]; // ['zero', 'one', undefined, 'three', 'four', 'five'];
// Good - slice를 이용하여 삭제하자
const numbers = ["zero", "one", "two", "three", "four", "five"];
numbers.splice(2, 1); // ['zero', 'one', 'three', 'four', 'five'];
continue
를 사용하지 않는다반복문 안에서 continue를 사용하면 자바스크립트 엔진에서 별도의 실행 컨텍스트를 만들어 관리하기 때문에
전체 성능에 영향을 주게된다.
continue를 잘 사용하면 코드를 간결하게 작성은 가능하지만 디버깅 시 개발자의 의도를 파악하기
어렵고 유지 보수가 힘들다
// Bad
let loopCount = 0;
for (let i = 1; i < 10; i += 1) {
if (i > 5) {
continue;
}
loopCount += 1;
}
// Good
for (let i = 1; i < 10; i += 1) {
if (i <= 5) {
loopCount += 1;
}
}
try-catch
는 반복문 안에서 사용하지 않는다순회가 반복될 때마다 런타임의 현재 스코프에서 예외 객체 할당을 위한 새로운 변수가 생성된다
// Bad
const {length} = array;
for (let i = 0; i < length; i += 1) {
try {
...
} catch (error) { // 반복될 시 예외 객체할당을 위한 새로운 변수가 계속 생성됨
...
}
}
// Good
const {length} = array;
function doSomething() {
try {
...
} catch (error) { // 예외 객체할당을 위한 새로운 변수 한번 생성 후 반복
...
}
}
for (let i = 0; i < length; i += 1) {
doSomething();
}
++,--
)를 사용하지 않는다연산이 먼저인지, 값 할당이 먼저인지, 연산의 결과를 한눈에 파악하기 어렵다
let num = 0;
const { length } = arr;
// Bad
for (let i = 0; i < length; i++) {
num++;
}
// Good
for (let i = 0; i < length; i += 1) {
num += 1;
}
일반적으로 런타임 중인 프로그램 메모리의 소스 내용(함수, 메서드, 속성을 바꾸는 것)들이
변경되는 행동을 의미
예로는 아래와 같이 타입이 동적으로 형변환되는 자연스러운 변화 또한 몽키패치라고도 한다.
React를 배우고 자연스럽게 ES6 문법을 사용하면서 var를 당연히 지양해 왔지만 정확히 어떤 이유에 대해서 var를 지양해야 하는지 모르는 경우가 많은것 같다고 생각했다
왜 let, const가 생겨나게 되었고 var를 지양하게 되었는지 알아보자
function scope & block scope
var global = '전역';
if(global === '전역') {
var global = '지역';
console.log("1: ",global); // 1: 지역
}
console.log("2: ",global); // 2: 지역
개발자는 1번은 지역 2번은 전역이 출력이 되도록 의도를 하였지만
var는 function scope이기 때문에 if문 내에서 global변수를 재선언 하였을 때
글로벌로 선언된 global 변수에도 영향이 가는 코드가 되어버리게 된다.
let global = '전역';
if(global === '전역') {
let global = '지역';
console.log("1: ",global); // 1: 지역
}
console.log("2: ",global); // 2: 전역
위의 let 변수는 block scope이기 때문에 if 문 내에서만 스코프가 작동을 하고
상위의 동일한 변수명의 global변수에는 영향이 가지 않는다.
ES5에서는 컨벤션으로 변경불가 데이터를 uppercase 변수로 선언하여 사용을 하였다.
ex: var FOO = 'bar';
하지만 이 변수 역시 중복선언과 값 변경이 가능한 상태임이 확실하고 의도치 않게 값의 변경이
일어날 가능성있고 또한 함수 스코프로 인해 전역 변수에 영향을 줄 수도 있기 때문에 **안전하지 않은 코드**이다.
그렇기 때문에 안전한 코드로 작성하기 위해서 블록 단위 스코프 + 변수 재선언 및 재할당이
불가한 **const** 를 사용하여 선언을 하자. 또한 재할당이 필요한 경우엔 **let** 을 사용하도록 하자
var 선언문이나 function 선언문 등을 해당 스코프의 선두로 옮긴 것처럼 동작하는 특성을 의미
ex)
console.log(foo); // undefined
var foo; // 호이스팅되어 아래선언했지만 콘솔로그보다 위에 선언된 것처럼 동작하게 된다.
console.log(bar); // Error: Uncaught ReferenceError: bar is not defined
let bar; // 반면 let은 참조에러가 나게되고 호이스팅이 되지 않는다
// let 변수는 스코프의 시작에서 변수의 선언까지 일시적 사각지대(Temporal Dead Zone: TDZ)에 빠지기 때문이다.
let 과 const도 호이스팅 대상이지만 var와 달리 호이스팅 시 undefined로 변수를 초기화 하지 않고
TDZ 에 들어서기 때문에 참조 에러가 나게 된다.
var 처럼 할당문 이후에 선언단계를 하게 되어 호이스팅 되면 동작에는 문제가 되지는 않지만
변수 선언이 명확하지 않고 디버깅 시에도 문제가 발생하는 안전하지 않은 코드로 작성이 되고 만다.
**호이스팅 해결 예 1)**
var sum;
console.log(sum);
// 위의 undefined가 아닌 호이스팅 된 맨 아래의 function이 출력됨 [Function: sum]
function sum() {
return 1 + 2;
}
function sum() {
return 1 + 2 + 3;
}
function sum() {
return 1 + 2 + 3 + 4;
}
이러한 호이스팅을 방지하기 위해 선언과 할당을 동시에 해주면 된다.
var sum = 1;
console.log(sum); // 1
function sum() {
return 1 + 2;
}
**호이스팅 해결 예 2)**
var sum;
console.log(sum()); // 10
function sum() {
return 1 + 2;
}
function sum() {
return 1 + 2 + 3;
}
function sum() {
return 1 + 2 + 3 + 4;
}
// 익명함수를 사용한 함수 표현식 사용
var sum = function() {
return 101;
};
***하지만 역시 const를 사용하여 선언하는 것이 제일 좋다***
javascript > 동적으로 변하는 언어 = 타입도 동적 > 타입 검사가 어려움
PRIMITIVE(원시형) vs REFERENCE(자료형)
// typeof
typeof '문자열' // 'string'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
등과 같은 PRIMITIVE는 불변의 값으로서 type이 정해져있어 typeof로 쉽게 타입을 검사할 수 있지만
REFERENCE같은 경우엔 동적인 타입을 가지고 있기 때문에 typeof로 검사가 쉽지않다.
// instanceof > 객체의 프로토타입 체인을 검사
const arr = [];
const func = function(){}
arr instanceof Array // true
func instanceof Function // true
arr instanceof Object // true
func instanceof Object // true
Array, Function 등과 같은 객체들은 결국 자료형이기 때문에 최상위가 Object가 되기 때문에 정확한
타입 체크가 어렵다.
그렇기 때문에 자료형의 타입을 검사할 때에는
Object.prototype.toString.call(''); 또는 toString.call(''); 를 사용하여
검사하도록 하자
console.log(typeof []); // object
console.log(toString.call([])); // [object Array]
console.log(Object.prototype.toString.call([])); // [object Array]
***논리연산자를 활용하여 불필요한 비교를 제거하자***
ex 1)
function fetchData() {
// if 문
if(state.data) {
return state.data;
} else {
return 'Fetching...';
}
// 삼항 연산자
return state.data ? state.data : 'Fetching...';
// 단축평가
return state.data || 'Fetching...';
}
ex 2)
function getActiveUserName(user, isLogin) {
// if 문
if(isLogin && user) {
if(user.name) {
return user.name;
} else {
return '이름없음';
}
}
// 단축평가
if(isLogin && user) {
return user.name || '이름없음';
}
}
**ex 1** - early return 사용하기)
early return : return을 빨리 써주어 뒷 코드의 구조를 간단하게 만드는 패턴이다.
* 코드의 중첩을 줄여 가독성이 좋아지고 조건이 명확해진다.
// else if 를 사용한다는 것은 조건에 대한 명확성이 부족하다는 의미일 수 있다
function getStringType(string) {
int ret;
if (isRule1(string)) {
ret = 1;
} else if (isRule2(string)) {
ret = 2;
} else {
ret = 0;
}
return ret;
}
// early return
function getStringType(string) {
if(isRule1(string)) return 1;
if(isRule2(string)) return 2;
return 0;
}
// 또한 if - else if가 파이프라인의 흐름처럼 실행된다는 오해를 야기할 수 있기 때문에 지양하도록 한다
const x = 1;
if(x >= 0) {
console.log('x는 0과 같거나 크다')
return 1;
} else if(x > 0) {
console.log('x는 0보다 크다');
return 2;
}
위에서 x가 if와 else if 조건에도 만족하기 때문에 순서에 따라
2가 return 될 것이라는 오해를 초래할 있음
위의 코드는 사실
if(x >= 0) {
console.log('x는 0과 같거나 크다')
return 1;
} else {
if(x > 0) {
console.log('x는 0보다 크다');
return 2;
}
}
이와같은 코드로 작동이 되기때문에 1이 return 하게 된다.
따라서 조건문을 명확하게 나누어 주도록 한다.
if(x >= 0) {
console.log('x는 0과 같거나 크다')
return 1;
}
if(x > 0) {
console.log('x는 0보다 크다');
return 2;
}
**ex 2** - switch)
switch(string) {
case isRule1(string):
return 1;
break;
case isRule2(string):
return 2;
break;
default:
return 0;
}
* switch에 비해 유연하고 가독성과 유지 관리 성이 우수하다.
* 수동으로 분리할 필요가 없다.
* case 수가 증가함에 따라 Object의 성능은 switch의 평균 비용보다 우수하다.
- Object 접근방식은 해시 테이블 조회
- switch 접근방식은 일치할 때까지 진입
const type = 'coke';
let drink;
switch(type) {
case 'coke':
drink = 'Coke';
break;
case 'pepsi':
drink = 'Pepsi';
break;
default:
drink = 'Unknown drink!';
return drink;
}
to
**ex 1)**
function getDrink(type) {
return (
{
coke: "Coke",
pepsi: "Pepsi",
lemonade: "Lemonade",
}[type] || "Default item"
);
}
에러를 미리 방지하고 안전하고 확장성 있는 함수를 만들기 위함
**ex 1)**
function sum(x, y) {
x = x || 0; // default 선언
y = y || 0; // default 선언
return x + y;
}
**ex 2)**
switch(day) {
case '월': // some code
case '화': // some code
case '수': // some code
...
// 절대적으로 변하지 않은 값을 계산하더라도 Default값을 항상 지정해주는 습관을 가지자
default:
// some code
}
function getSumSize(width, height) {
return (width || 10) + (height || 10);
}
getSumSize(); // 20
getSumSize(0, 0); // 20
**||** 연산자는 조건이 falsy인 경우 default 값을 반환하는 연산자이다
0 또한 falsy로 간주하기 때문에 0 을 넣고싶어도 false로 간주해 10으로
인식해버리는 문제가 발생하기도 한다
이럴때 null 과 undefined만 falsy로 간주하기 위해 **??** 연산자를 사용한다
* 새로나온 연산자라 구버전 브라우저에서는 안돌아갈 수 있으므로 polyfill을 준비해야한다.
function getSumSize(width, height) {
return (width ?? 10) + (height ?? 10);
}
getSumSize(); // 20
getSumSize(0, 0); // 0
* 주의 : 편리하보니 **||** 와 **??** 의 사용처를 햇갈리곤 하니
꼭 의도에 맞게 **null**과 **undefined** 를 평가할 때에만 사용하도록 한다.
* || 와 ?? 를 동시에 쓸경우
null || undefined ?? 'foo'; // error
> 연산자 사요에 있어서 사용자들의 실수가 많아 javascript 자체적으로 같이 사용을 막아두었지만
같이 사용하기 원한다면 우선순위를 명시적으로 지정해주면 된다
(null || undefined) ?? 'foo'; // 'foo'
**ex 1)**
if(A && B)의 반대
>>
if(!(A && B)) // 일반적인 방법
>>
if(!A || !B) // 드모르간 법칙
**ex 2)**
if(A || B)의 반대
>>
if(!(A || B)) // 일반적인 방법
>>
if(!A && !B) // 드모르간 법칙
const isUser = true;
const isToken = true;
if(isUser && isToken) console.log('로그인 성공'); // '로그인 성공'
if(!(isUser && isToken)) console.log('실패'); // '실패' - 일반
if(!isUser || !isToken) console.log('실패'); // '실패' - 드모르간 법칙
* 괄호를 제거함으로서 가독성도 좋고 명확해 보이고 부정연산에 대한 햇갈림도 줄일 수 있다.