lottie
Seungjun's blog
blog
Anti Pattern (2)

14. 반복문에서 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;
  };
};

15. try-catch는 반복문 안에서 사용하지 않는다

흔히 예외 처리를 위해 try-catch를 사용한다. **try-catch를 반복문 안에서 사용하면, 순회가 반복될 때마다 런타임의 현재 스코프에서 예외 객체 할당을 위한 새로운 변수가 생성된다.**


해결책 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();
}

16. 같은 DOM 엘리먼트를 반복해서 탐색하지 않는다

getElementByIdgetElementsByTagNamequerySelector는 DOM 엘리먼트를 탐색하는데 사용하는 API이다. DOM 탐색은 비용이 들기 때문에 한 번 탐색하는 것보다 여러 번 탐색할 경우 성능이 저하된다.


해결책 탐색 비용을 절약하기 위해 이미 탐색한 엘리먼트는 캐시하여 사용한다.

// Bad
const className = document.getElementById("result").className;
const clientHeight = document.getElementById("result").clientHeight;
const scrollTop = document.getElementById("result").scrollTop;
document.getElementById("result").blur();
// Good
const el = document.getElementById("result");
const { className, clientHeight, scrollTop } = el;
el.blur();

17. DOM 변경을 최소화 한다

innerHTML 속성 또는 appendChild 메서드를 사용할 때 DOM 변경이 발생한다. DOM 트리 변경은 비용이 들기 때문에 한 번 DOM을 변경하는 것보다 여러 번 DOM을 변경하는 것이 성능에 좋지 않다.


해결책 여러 번 DOM 변경이 필요한 경우 모든 변경 내용을 한번에 반영하여 DOM 변경을 최소화 한다.

const el = document.getElementById("bookmark-list");
// Bad
myBookmarks.forEach(bookmark => {
  el.innerHTML += `<li><a href="${bookmark.url}">${bookmark.name}</a></li>`;
});
// Good
const html = myBookmarks
  .map(bookmark => `<li><a href="${bookmark.url}">${bookmark.name}</a></li>`)
  .join("");
el.innerHTML = html;

18. 불필요한 레이아웃을 발생시키지 않는다

브라우저에서 생성된 DOM 노드는 레이아웃 값(너비, 높이, 위치)을 변경 시 영향받는 모든 노드(자기 자신, 자식 노드, 부모 노드, 조상 노드)의 값을 재계산하여 렌더 트리를 업데이트 한다. 이러한 과정을 리플로우(reflow) 또는 레이아웃(layout)이라 한다.

레이아웃이 발생될 때

  • 페이지 초기 렌더링 시

  • 윈도우 리사이징 시

  • 노드 추가 및 삭제 시

  • 엘리먼트 크기 및 위치 변경 시

  • 특정 프로퍼티( offsetHeightoffsetTop...)를 읽고 쓸 때

  • 메서드(getClientRects()getBoundingClientRect()...)를 호출할 때

강제 레이아웃이 연속하여 빠르게 실행되는 것을 레이아웃 스래싱이라 하며, 브라우저가 레이아웃을 몇 번이고 되풀이해서 재계산하기 때문에 성능에 좋지 않은 영향을 준다


해결책 반복문 안에서 강제 레이아웃을 수행하는 프로퍼티나 메서드를 호출해야 하는 경우, 해당 값을 반복문 밖에서 캐시하여 사용한다.

// Bad
function resizeWidth(paragraphs, box) {
  const { length } = paragraphs;

  for (let i = 0; i < length; i += 1) {
    paragraphs[i].style.width = `${box.offsetWidth}px`;
  }
}
// Good
function resizeWidth(paragraphs, box) {
  const { length } = paragraphs;
  const width = box.offsetWidth;
  for (let i = 0; i < length; i += 1) {
    paragraphs[i].style.width = `${width}px`;
  }
}

19. 이벤트는 인라인 방식으로 사용하지 않는다

자바스크립트 개발 초기에는 DOM 엘리먼트에 리스너를 등록할 때 인라인 방식으로 사용하였다. 아래 예제 코드에서 doSomething()은 외부 자바스크립트 파일에 함수이다. 만약 doSomething의 이름을 바꾸거나 버튼을 클릭했을 때 호출할 함수를 바꾸려면 자바스크립트와 HTML 파일 모두를 수정해야 한다. HTML과 자바스크립트가 서로 의존 관계를 만들어 작은 변경에도 수정 범위가 커지고 유지보수와 디버깅이 어려워진다. 또한 하나의 엘리먼트에 이벤트 리스너를 여러 개 등록할 수 없다.


해결책 인란인 자바스크립트 코드는 HTML에 분리해서 사용한다.

<!-- Bad -->
<button onclick="doSomething()" class="action-btn">Click Me</button><!-- Good -->
<button class="action-btn">Click Me</button>
...
<script src="js/btn-event.js"></script>
// btn-event.js
const btn;
function doSomething() {
  ...
}
...
// Good
btn = el.querySelector('.action-btn');
btn.addEventListener('click', doSomething, false);

20. eval()을 사용하지 않는다

eval()은 문자로 표현된 자바스크립트 코드를 실행하는 함수이다. 파라미터로 문자열을 받아 호출자(caller)의 지역 스코프에서 즉시 실행한다.


eval에서 문자열로 정의한 변수나 함수는 구문 분석 단계가 아니라, 코드가 실행되는 시점에 실제 변수나 함수가 된다.


그래서 프로그램 실행 중 구문분석기가 새로 기동 되어야 하는데 상당한 부하를 만들어 프로그램 실행 속도를 현저히 느리게 한다. 그리고 사용자 입력 또는 네트워크로 들어온 문자열을 eval()로 수행할 경우 서비스 전체에 심각한 보안 문제를 일으킬 수 있다.


해결책 eval을 절대 사용하지 말자.

const nhnCloud = {
  name: "NHN Cloud",
  lab: "FE Dev Lab",
  memberCount: 10
};
const propName = "name";
// Bad
eval(`nhnCloud.${propName}`); // NHN Cloud
// Good
nhnCloud[propName]; // NHN Cloud

21. with()를 사용하지 않는다

with문은 특정 객체에 반복해서 접근할 때 간편함을 제공한다. 하지만 의도와는 다르게 많은 문제점을 낳고 있어 사용하지 않는 것이 좋다. 다음은 with 사용 예제이다.

function doSomething(value, obj) {
  ...
  with(obj) {
    value = 'which scope is this?'
  }
}
// `with`절의 구문은 아래 코드와 같은 의미이다.
// `value = "which scope is this?";
// obj.value = "which scope is this?";`

어떤 코드로 실행되는지 코드만 봐서는 알 수 없다. 코드가 실행될 때마다 다르게 실행될 수 있고 프로그램이 실행되는 동안에도 달라질 수 있다. 의도하는 바가 무엇인지 명확히 알 수 없고 어떻게 실행될지 예측할 수 없다.


즉, 프로그램이 원하는 방향으로 제대로 실행될 것이라고 확신할 수 없다. 이 외에도 with문은 실행할 때마다 새로운 스코프를 생성하여 자원을 추가로 소모하고, 자바스크립트 내부에서 수행되는 변수 탐색 최적화를 방해하여 실행 속도를 현저히 떨어뜨린다.


해결책 특정 객체를 반복해서 접근해야 한다면 새로운 변수에 캐시하여 사용한다.

// Bad
with (document.getElementById("myDiv").style) {
  background = "yellow";
  color = "red";
  border = "1px solid black";
}
// Good
const { style } = document.getElementById("myDiv");
style.background = "yellow";
style.color = "red";
style.border = "1px solid black";

22. setTimeoutsetInterval 사용 시 콜백 함수는 문자열로 전달하지 않는다

setTimeout과 setInterval은 일정 시간 후에 첫 번째 파라미터로 받은 콜백 함수를 실행한다. 첫 번째 파라미터를 문자열로 전달할 수 있는데, 내부에서 eval로 처리되어 실행 속도가 느려진다.


해결책 setTimeout, setInterval 사용 시 콜백 함수는 함수를 직접 전달한다.

// Bad
function callback() {
  ...
}
setTimeout('callback()', 1000);
// Good (1)
function callback() {
  ...
}
setTimeout(callback, 1000);
// Good (2)
setTimeout(() => {
    ...
}, 1000);

23. 함수 생성자 new Function()은 사용하지 않는다

많이 사용하는 방법은 아니지만, 함수 생성자를 이용해서 함수를 선언할 수 있다. 이 경우, 문자열로 전달되는 파라미터가 수행 시점에 eval로 처리되어 실행 속도가 느려진다.


해결책 함수 선언 시 함수 선언식 또는 함수 표현식을 사용한다.

// Bad
const doSomething = new Function("param1", "param2", "return param1 + param2;");
// Good (1) - 함수 선언식
function doSomething(param1, param2) {
  return param1 + param2;
}
// Good (2) - 함수 표현식
const doSomething = function(param1, param2) {
  return param1 + param2;
};

24 . 네이티브 객체는 확장하거나 오버라이드 하지 않는다

자바스크립트는 동적인 특징이 있어 이미 선언된 객체의 프로퍼티를 추가, 삭제, 변경할 수 있다. Object.defineProperty를 사용하면 프로퍼티 속성을 지정할 수 있으며, 설정에 따라 프로퍼티 추가, 수정, 삭제가 불가능하게 할 수도 있다. 네이티브 객체의 프로토타입은 프로퍼티를 추가하거나 기존 프로퍼티를 재정의할 수 있다. 하지만 네이티브 객체를 확장하거나 이미 정의된 메서드를 재정의하면 네이티브 객체의 기본 동작을 기대한 다른 개발자에게 혼란을 줄 수 있다. 같은 이름의 메서드가 어떤 브라우저에서는 지원되고 어떤 브라우저에서는 지원되지 않는 상황에서 충돌이 발생할 수 있다. 자칫하면 네이티브 메서드를 실수로 덮어쓸 수도 있으며, 코드 내 예측할 수 없는 오류를 만들 수 있다.


해결책 네이티브 객체는 절대 수정하지 않는다. 만약 필요한 메서드가 있다면 네이티브 객체의 프로토타입에 작성하지 않고 함수로 만들거나 새로운 객체를 만들어 네이티브 객체와 상호작용한다

const o = {
  x: 1,
  y: 2
};
// Bad
Object.prototype.getKeys = function() {
  const keys = [];

  for (let key in this) {
    if (this.hasOwnProperty(key)) {
      keys.push(key);
    }
  }
  return keys;
};
o.getKeys();
// Good
function getKeys(obj) {
  const keys = [];
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      keys.push(key);
    }
  }
  return keys;
}
getKeys(o);

몽키패칭(monkey-patching):

네이티브 객체나 함수를 다른 객체나 함수로 확장하는 것을 몽키패칭이라 한다. 하지만 이는 캡슐화를 망치고 표준이 아닌 기능을 추가해 네이티브 객체를 오염되므로 사용하지 않는다. 이런 위험에도 불구하고, 신뢰성 있고 매우 중요한 몽키패칭의 특별한 한가지 사용법이 있는데, 바로 폴리필(polyfill)이다.

폴리필은 Array.prototype.map과 같이 자바스크립트 엔진에 새롭게 추가된 기능이 없는 경우, 비슷한 동작을 하는 다른 함수로 대체하는 것을 말한다. 폴리필과 같이 자바스크립트 기능의 호환성 유지 목적을 제외하고는 어떤 경우에도 네이티브 객체의 확장은 옳지 않다.

// 폴리필(Polyfill) 예제
if (typeof Array.prototype.map !== "function") {
  Array.prototype.map = function(f, thisArg) {
    const result = [];
    const { length } = this;

    for (let i = 0; i < length; i += 1) {
      result[i] = f.call(thisArg, this[i], j);
    }
    return result;
  };
}

25. 단항 증감 연산자를 사용하지 않는다

단항 증감 연산자를 사용하면 연산이 먼저인지, 값 할당이 먼저인지, 연산의 결과를 한눈에 파악하기 어렵다.


해결책 값 할당 연산자를 사용하여 읽기 쉬운 코드로 작성한다

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;
}

26. this에 대한 참조를 저장하지 않는다

**this는 함수 실행 시점에 결정된다**. 어떤 함수 내부에서 또 다른 함수를 호출하면 그 함수의 this는 상위 함수의 this와 같지 않다. 소스 코드 작성 시, 상위 함수 컨텍스트의 this를 참조해야 하는 경우가 있다. 비슷한 이름의 참조 변수(thatselfme...)를 만들고 내부 함수의 클로저로 사용하여 상위 함수의 this를 내부 함수의 전달할 수 있다. this와 비슷한 이름의 참조 변수를 사용하는 것은 개발자에게 혼란을 줄 수 있다.


해결책 참조 변수를 따로 선언하지 말고 Function.prototype.bind 함수나 화살표 함수를 사용한다.

// Bad
function() {
  const self = this;
  return function() {
    console.log(self);
  };
}
function() {
  const that = this;
  return function() {
    console.log(that);
  };
}
function() {
  const _this = this;
  return function() {
    console.log(_this);
  };
}
// Good
function printContext() {
  return function() {
    console.log(this);
  }.bind(this);
}
function printContext() {
  return () => console.log(this);
}

27. 문장의 끝은 세미콜론(;)을 생략하지 않는다.

문장 끝 세미콜론(;)을 생략하지 않는다.


세미콜론 생략하면 자바스크립트 구문분석기가 세미콜론을 자동으로 삽입해주는데 의도하지 않았던 코드(전혀 다른 코드)로 해석되어 예기치 못한 동작이 발생할 수 있다. 그리고 자바스크립트 구문분석기가 세미콜론의 위치를 계산하고 삽입하는데 추가 비용이 발생한다.


해결책 문장의 끝은 세미콜론을 사용한다.

28. 변수와 함수는 선언 전에 사용하지 않는다 (ES5)

ES5 이하 버전에서 블록 구문을 사용하지만, 블록 유효범위를 제공하지 않는다. 블록 내에서 var 키워드를 사용해 변수를 선언되면, 선언된 위치에 상관없이 함수 스코프 내 어느 곳에서든 사용할 수 있다. var 키워드를 사용해 선언한 변수, function 키워드를 사용해 선언한 함수는 자바스크립트가 실행되고 컴파일될 때 호이스팅(끌어올림)이 되기 때문이다. 이로 인해 코드 가독성이 떨어지며 오류를 찾기 힘들다.


해결책 함수는 사용하기 전에 선언하고, 변수는 var 키워드와 함께 상단에 선언한다

// Bad
doSomething();
function doSomething() {
  foo1 = foo2;
  ...
  var foo1 = 'value1';
  foo3 = foo4;
  ...
  var foo3;
  ...
  var foo4 = 'value4';
  var foo2;
}
// Good
function doSomething() {
  var foo1 = 'value1';
  var foo4 = 'value4';
  var foo3 = foo4;
  var foo2;
  ...
  foo1 = foo2;
}
doSomething();