Javascript 클린 코드
개발과정의 대부분은 코드 리팩토링이고, 리팩토링을 하기 위해서는 코드의 흐름을 잊어버리지 않고 계속 파악하고 있어야 한다.
코드의 흐름을 파악하기 위해서는 코드를 계속해서 읽어야한다.
(사실.. 기능 구현 조차 코드의 흐름을 파악하고 있어야 하기에, 사실상 개발 과정 내내 코드를 읽어야 한다.)
코드를 읽는 시간을 줄이려면 코드를 최대한 짧고 간결하게, 의미를 알기 쉽게.... 즉, 클린하게 짜야한다.
누가 클린 코드를 교양 서적이라고 했을까... 내가 느끼기엔 그냥 필수다 필수.
클린 코드는 습관에 가깝다고 생각한다. 습관을 들이자...
1. var 대신 let, const를 쓰자
var는 함수 레벨 스코프를 가지고 있고,
let, const는 블록 단위 스코프를 가지고 있다.
TDZ (Temporal Dead Zone)
TDZ는 호이스팅으로 인해서 선언만 되고 초기화는 되지 않은 구간을 뜻한다.
호이스팅은 블록 단위로 발생하는데
var는 함수 레벨 스코프이므로 블록에 구애받지 않고 어디서든 호출이 가능하다. (비록 undefined로 미리 초기화되어서라도 말이다.)
하지만 let, const는 블록 레벨 스코프를 가지므로 초기화 블럭을 만나기 전까지는 변수를 사용할 수 없다.
console.log(test); // ReferenceError
let test = 0; // 초기화로 인해 TDZ가 위에서 끝남
console.log(test); // 0
2. 전역 공간 사용을 최소화하자
javascript는 런타임 환경을 우리가 직접 제어할 수 있다.
전역 공간 사용을 최소화해서 메모리 관리를 하자.
같은 런타임이라면 다른 모듈에도 해당 변수가 남아있다.
// index1.js
var globalVar = 'global';
// index2.js
console.log(globalVar); // global
console.log(window.globalVar); // global
역시나 var 사용을 주의하자
const array [10, 20, 30];
for (var i=0; i < array.length; i++) {
const element = array[i];
}
위의 코드에서 for문에 쓰이는 변수 i는 반복문이 끝나면 GC로 넘어가서 메모리 할당이 해제되길 기다릴 것이다.
하지만 사실 그렇지 않다. 브라우저 콘솔에서 Window
를 찍어보면 아래와 같이 전역 변수로 남아있다.
그렇기에 var 대신 let, const를 사용하자.
3. 임시 변수를 제거하자
한번만 쓰이는 변수는 가능하면 사용하지 말자.
임시 변수를 사용하면 그 변수를 사용해서 무언가를 계속 조작하려는 유혹에 빠질 수도 있다.
예제부터 보자.
function getElements() {
const result = {};
result.title = document.querySelector('.title'),
result.text = document.querySelector('.text'),
result.value = document.querySelector('.value'),
return result;
}
위의 코드가 가독성이 좋아보일까?
리턴을 보면 결국 Object 하나를 리턴한다.
변수를 사용하지 않게 바꿔보자.
function getElements() {
return {
title: document.querySelector('.title'),
text: document.querySelector('.text'),
value: document.querySelector('.value'),
};
}
훨씬 나아졌다.
isNaN
숫자가 아닌지 확인할 때 isNaN을 많이 사용한다.
하지만 가능하면 isNaN()
대신 좀 더 엄격하고 직관적인 Number.isNaN()
을 사용하자.
isNaN
은 주어진 값을 Number로 형변환한 후에 검사한다.
우리가 isNaN을 사용하는 이유는 Number를 연산할 때 제대로 연산이 되는지 확인하려고 사용한다.
그런 의미에서 Number.isNaN
은 주어진 값의 타입이 Number이고 NaN일 때만 true를 반환한다.
애초에 Number가 아닌 다른 타입이 들어오는 상황을 차단하는 것이다.
Number.isNaN = function (x) {
return typeof x === 'number' && isNaN(x);
};
Number.isNaN(NaN); // true
Number.isNaN(0 / 0); // true
Number.isNaN(true); // false
Number.isNaN(null); // false
Number.isNaN(29); // false
Number.isNaN('29'); // false
삼항연산자
일단 코드를 보자.
function example() {
if (condition1) {
return value1;
} else if (condition2) {
return value2;
} else if (condition3) {
return value3;
} else {
return value4;
}
}
// 또는
function example() {
return condition1 ? value1 : condition2 ? value2 : condition3 ? value3 : value4;
}
condition 변수들이 단순히 값이 true인지 확인하는 거라면
아래 방법처럼 object를 활용하는 것이 나을 수도 있다.
const example = {
condition1: value1,
condition2: value2,
condition3: value3,
};
return example[condition1] || value4;
Early Return
function check(active) {
if (active === undefined) {
return helloActive('active');
} else {
return active;
}
}
위의 함수를 아래처럼 줄일 수 있다.
function check(active) {
if (active === undefined) {
return helloActive('active');
}
return active;
}
단축평가
다중 if문 대신 &&
, ||
, ??
를 사용해보자.
if (isLogin) {
if (user) {
if (user.name) {
return user.name;
} else {
return '이름없음';
}
}
}
위의 코드를 간략화하면 다음과 같다.
if (isLogin && user) {
return user.name || '이름없음';
}
하지만 위의 코드는 user.name
이 'undefined' 혹은 'null'와 같은 false
를 출력시키는 문자열이면 문제가 된다 그럴 때 ??를 사용하면 된다.
if (isLogin && user) {
return user.name ?? '이름없음';
}
명시적인 연산자 사용 지양하기
전위, 후위 연산자를 지양하고 최대한 명시적으로 작성하자.
function increment(number) {
// return number++; (X)
return number + 1;
}
if 문, switch 문 대신 object 사용하기
조건식이 값식문으로 표현이 가능할 경우에는 object 형태로 리팩토링이 가능하다
if문 / switch문
if (category === 'total') {
// ...
} else if (category === 'a') {
// ...
} else if (category === 'b') {
// ...
} else {
// ...
}
switch(category) {
case: 'total':
// ...
break;
case: 'a':
// ...
break;
case: 'b':
// ...
break;
default:
// ...
object 형태
key에 조건식을 넣어주고 value에 구현로직을 넣는다.
value를 익명함수로 넣어줬으므로 lazy한 특성을 띄고, 뒤에 소괄호를 붙여줘야만 해당 익명함수를 호출 연산한다.
// else 상황까지 object에 넣는 경우
const obj = {
total: () => { // ... },
a: () => { // ... },
b: () => { // ... },
c: () => { // ... },
default: () => { // ... },
}
const result = obj[category]() || obj['default']();
// key에 'default' 문자열이 들어갈까 걱정된다면
const obj = {
total: () => { // ... },
a: () => { // ... },
b: () => { // ... },
c: () => { // ... },
}
const result = obj[category]() || { // ... }; // 뒤에 오는 함수는 else일 때의 함수이다.
reduce와 ...(spread operator)는 같이 사용하지 말자.
reduce는 배열을 K:V 형태의 객체로 정리할 때 굉장히 유용하다.
(이번에 일하면서 특히 느꼈다... 배열만 고집하지 말자)
reduce를 사용할 때 acc에 ...
를 같이 사용하면 코드가 깔끔해지지만, 사용하는 데이터가 수만개~수십만개일 때 정말 많이 느려진다.
...
은 Object.assign({}, arr)
의 Syntatic Suger이다. 즉, 열거 가능한 객체를 순회하면서 다른 객체에 그대로 복사하는것이다.
여기서 문제가 발생한다. acc가 길어질수록 연산량이 계속해서 늘어난다. (전체 갯수 x 전체 갯수)
즉, O(n^2)의 시간복잡도가 되어버린다. 만약 비슷한 로직으로 구현하고 싶다면 아래와 같이 하자.
const arr = [
{ id: 0, name: 'giwon' },
// ...
];
const obj = Object.fromEntries(arr.map(({ id, name }) => [id, name]));
console.log(obj);
/*
{
0: 'giwon',
// ...
}
*/