lottie
Seungjun's blog
blog
Anti Pattern (1)

안티 패턴이란?

안티 패턴이란 습관적으로 많이 사용하는 패턴이지만 성능, 디버깅, 유지보수, 가독성 측면에서 부정적인 영향을 줄 수 있어 지양하는 패턴이다.


실수하기 쉬운 안티 패턴을 사례별로 설명하고 개선 방법을 가이드해보려 한다.

1. <script>는 문서 하단에서 포함한다

CSS나 자바스크립트 같은 외부 파일을 <head> 태그 안에 모아놓는 경우, 브라우저는 페이지를 렌더링하는 과정에서 CSS나 자바스크립트를 만나면 렌더링을 멈추고 이 요소들을


1)다운로드하고 2)구문을 분석하고 3)컴파일한다.


자바스크립트를 많이 사용하는 페이지일수록 눈에 띌 정도로 렌더링이 지연된다.


해결책 모든 자바스크립트 코드는 태그 안, 맨 마지막에 작성한다.

<body>
</body>
<!DOCTYPE html>
<html>
  <head>
    ...
    <!-- Bad: head 태그 안에서 script 삽입 -->
    <script src="../js/jquery-3.3.1.min.js"></script>
    <script src="../js/common.js"></script>
    <script src="../js/applicationMain.js"></script>
  </head>
  <body>
    ...
    <!-- Good: body 태그 안, 맨 마지막에 삽입 -->
    <script src="../js/jquery-3.3.1.min.js"></script>
    <script src="../js/common.js"></script>
    <script src="../js/applicationMain.js"></script>
  </body>
  <html></html>
</html>

2. 외부 리소스 사용 시 URL을 직접 사용하지 않는다

jQuery 같은 유명 오픈 소스들은 보통 바로 접근할 수 있는 URL 제공한다. 이러한 URL을 직접 사용할 경우 외부 요인(URL 변경, CDN 장애 등)이 서비스에 그대로 반영되어 장애로 이어질 수 있다.


해결책 외부 URL을 직접 사용하지 않고 프로젝트에 해당 소스 파일을 다운로드해서 사용한다.

<!DOCTYPE html>
<html>
  <head>
    <title>HTML Page</title>
  </head>
  <body>
    ...
    <!-- Bad -->
    <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>

    <!-- Good -->
    <script src="../js/jquery-3.3.1.min.js"></script>
    ...
  </body>
</html>

3. 전역 변수를 사용하지 않는다

다른 언어와 마찬가지로 자바스크립트에도 전역이 존재하며, 웹 브라우저에서 전역 객체는 window이다. 선언하지 않고 사용하는 변수, 함수 밖에서 선언한 변수는 전역 변수가 된다. 전역 변수는 어플리케이션 전역에 공유되어 어디서나 읽고 쓸 수 있다. 다른 모듈에서 동일한 이름으로 사용하면 덮어쓸 수 있어 조심해야한다.


해결책 네임스페이스 패턴이나 즉시 실행 함수를 활용한다.

네임스페이스 패턴

네임스페이스 패턴은 애플리케이션이나 라이브러리를 위한 전역 객체를 하나 만들고 모든 기능을 이 객체에 추가하는 방법이다. 변수와 함수 선언시 함수 안이나 객체의 프로퍼티로 선언하지 않으면 전역에 추가된다. 전역에 MyApp과 같은 단 하나의 객체를 정의하고, 필요한 변수와 함수를 MyApp 안에 정의한다. 이렇게 하면 name이 window.name을 덮어쓰지 않고, sayName()도 MyApp.sayName()으로 호출되어 오류가 생겼을 때 쉽게 찾을 수 있다.

// Bad - 전역(Global)에 name변수와 sayName()함수 추가
const name = 'Nicholas'; // window.name === 'Nicholas'
function sayName() {
  alert(name);
}

// Good - 전역 객체 MyApp을 정의하고 name변수와 sayName()함수를 그 안에 정의
const MyApp = {
  name: 'Nicholas',
  sayName() {
    alert(this.name);
  }
};

MyApp.sayName();

// Tip - 네임스페이스 확장
const nhnCloud = window.ne || {}; // NHN Cloud의 네임스페이스로 ne를 사용

// 그 하위에 서비스명을 2차 네임스페이스로 사용
nhnCloud.serviceName = nhnCloud.serviceName || {};

// 페이지별 또는 기능별 모듈명을 3차 네임스페이스로 사용
nhnCloud.serviceName.util = {...};
nhnCloud.serviceName.component = {...};
nhnCloud.serviceName.model = {...};

// 필요에 따라 4차, 5차 네임스페이스로 확장하여 사용
nhnCloud.serviceName.view.layer = {...};
nhnCloud.serviceName.view.painter = {...};

즉시 실행 함수

자바스크립트 함수는 지역 스코프(local scope)를 만들어낸다. 함수 내부에서 선언한 변수는 지역 변수가 되고 함수의 라이프 사이클과 함께 유지된다. 즉시 실행 함수는 함수를 생성과 동시에 한번만 실행된다. 참조가 없기 때문에 재실행할 수 없다.

5. 변수 선언 없이 바로 변수를 사용하지 않는다

constletvar 키워드 없이 선언된 변수는 전역으로 처리된다. 오류는 없지만(단, strict모드에서는 같은 코드에서도 참조 오류가 발생) 전역 환경이 오염되고 때때로 매우 찾아내기 어려운 버그를 만든다.


해결책 변수 선언 시 반드시, const, let, var 키워드를 사용한다. const, let 키워드를 사용할 수 없는 환경이면 var 키워드를 이용하여 변수를 선언한다

// Bad
const foo = "foo";
let bar = "bar";
baz = "var"; // baz는 전역적으로 선언되어 전역 환경을 오염시킴

// Good
const foo = "foo";
let bar = "bar";
let baz = "var";

6. 배열과 객체 생성 시 생성자 함수를 사용하지 않는다

배열이나 객체를 선언할 때 생성자(constructor)를 사용할 수도 있지만, 리터럴 표기법을 사용하는 것이 간결하고 직관적이며 속도 면에서도 좋다. 실제로 자바스크립트 엔진은 리터럴 표기법에 맞게 최적화되어있다. 배열 생성자를 사용할 때 숫자 한 개를 파라미터로 넘기면 숫자에 해당하는 길이의 배열을 생성하게 된다. API 스펙과 기대하는 동작이 다소 혼동될 수 있으니 주의해야 한다.


해결책 배열과 객체는 리터럴로 선언한다.

// Bad
const emptyArr = new Array();
const emptyObj = new Object();

const arr = new Array(1, 2, 3, 4, 5);
const obj = new Object();
obj.prop1 = "val1";
obj.prop2 = "val2";

const arr2 = new Array(5);
arr2.length; // 5

// Good
const emptyArr = [];
const emptyObj = {};

const arr = [1, 2, 3, 4, 5];
const obj = {
  prop1: "val1",
  prop2: "val2"
};

7. 동등 비교 연산 시 ==를 사용하지 않는다

자바스크립트는 두 값을 비교 또는 산술하기 전에 암묵적인 형변환을 실행한다. 이 때문에 다른 타입의 데이터 간에 비교와 산술이 가능하다. 하지만 암묵적인 형변환은 전체 코드의 데이터 관리를 어렵게 만들며 때로는 연산 과정에서 발생하는 데이터 타입 오류를 덮어버린다. 등호 연산자(==)를 사용하여 비교 연산 시 코드를 작성하는 사람과 읽는 사람 모두 강제 형변환 규칙을 이해해야 하며 형변환이 발생할 수 있는 모든 경우를 고려해야 하는 단점이 있다.


해결책 암묵적인 강제 형변환이 일어나지 않도록 삼중 등호 연산자를 사용한다. 만약 타입이 다른 데이터 간에 비교가 필요하면 명시적으로 강제 형변환 후 삼중 등호 연산자(=== 또는 ≠=)을 사용한다.

// Bad
undefined == null; // true
123 == "123"; // true
true == 1; // true
false == 0; // true

// Good
123 === "123"; // false
Number("123") === 123; // true
String(123) === "123"; // true
1 === true; // false
0 === false; // false
Boolean(1) === true; // true
Boolean(0) === false; // true
undefined === null; // false
Boolean(undefined) === Boolean(null); // true

// 명시적 강제 형변환
Number("10") === 10;
parseInt("10", 10) === 10;
((String(10) === "10" + "10") === 10(123).toString()) === "123";
Boolean(null) === false;
Boolean(undefined) === false;

8. 중괄호({})를 생략하지 않는다

if/while/do/for 문 사용 시 한 줄짜리 블록이면 중괄호({})를 생략하지 않는 것이 좋다. 중괄호가 없을 경우에 제어문의 동작 범위를 한눈에 파악하기 힘들기 때문이다.


해결책 한 줄짜리 블록에도 { }를 사용하여 명확하게 작성한다.

// Bad
if (condition) doSomething();

if (condition) doSomething();
else doAnything();

for (let prop in object) someIterativeFn();

while (condition) iterating += 1;

// Good
if (condition) {
  doSomething();
}

if (condition) {
  doSomething();
} else {
  doAnything();
}

for (let prop in object) {
  someIterativeFn(object[prop]);
}

while (condition) {
  iterating += 1;
}

9. parseInt는 두 번째 파라미터인 기수를 생략하지 않는다

parseInt는 문자열을 정수로 바꿔주는 함수이다. 첫 번째 파라미터는 정수로 바꿀 문자열이고 두 번째 파라미터는 기수(진법)이다.


기수가 생략될 경우 브라우저는 변환될 숫자 형식을 자체적으로 판단하며, 브라우저에 따라 다르게 해석될 수 있다. (문자열이 '0x' 또는 '0X'로 시작하면16진수로, '0'으로 시작하면 8진수 또는 10진수로 간주한다.)


해결책 항상 두 번째 파라미터를 명시하여 오류를 미리 예방한다. 10진수로 변환이 필요한 경우라면 Number()를 사용하는 것이 속도에서 이득이다.

// Bad
const month = parseInt("08");
const day = parseInt("09");

// Good
const month = parseInt("08", 10);
const day = parseInt("09", 10);

// Tip : 10진수로 변환하는 경우라면 'Number()'를 사용하거나 '+'연산자를 붙이는 것이 더 빠름
const month = Number("08");
const day = +"09";

10. switch문에서 break를 생략하지 않는다

switch문의 각 case절은 break 키워드를 만나면 해당 case절을 벗어난다. break 키워드를 생략하면, break 키워드를 만날때까지 다음 case절을 연달아 실행하는데, 해당 코드를 읽는 사람에게는 생략이 의도인지, 실수인지 확인하기 힘든 코드가 된다. 예를 들어 첫번째 case절에서 A()함수를 호출하고 break을 생략한 뒤 두번째 case절에서 B()를 호출하면 두번째 case절은 A()와 B()를 둘 다 수행한다. 이 경우 코드 가독성에 좋지 않은 영향을 주고 혹시라도 break 키워드를 실수로 작성하지 않았을 때 버그의 원인을 찾기 힘들다. 그러므로 case절 내부에는 반드시 break키워드를 작성하도록 한다. 하지만, 다수 case절을 한번에 처리하는 경우는 예외로 한다.


해결책 break를 생략하지 않는다(단 , 다수의 case절이 동일한 기능을 수행하는 경우를 제외). 어떠한 case에도 해당하지 않으면 default를 사용하여 기본 동작을 설정해주도록 한다.

// Bad - break를 생략하여 case 2일 때는 A()과 B()를 수행하는 코드
switch (foo) {
  case 1:
    A()
  case 2:
    B();
    break;
  ...
}

// Good
switch (foo) {
  case 1:
    A()
    break;
  case 2:
    C();  // A(), B()에서 하는 기능을 모두 포함한 C함수
    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();
}

11. 배열의 순회는 for-in을 사용하지 않는다

보통 객체(Object)의 프로퍼티 순회가 필요할때는 for-in을 사용한다.


자바스크립트의 배열(Array)도 객체지만 배열의 요소를 순회할 때는 for-in을 사용하지 않아야 한다. **for-in은 프로토타입 체인에 있는 모든 프로퍼티를 순회하므로 for를 사용할 때보다 훨씬 느리다.** 게다가 순회 순서는 브라우저에 따라 다르게 구현되어 있어서 배열의 요소 순회가 늘 index 순서대로 수행되지 않을 수 있다.


해결책 배열을 순회할 때는 for문을 사용한다.

// Bad
const scores = [70, 75, 80, 61, 89, 56, 77, 83, 93, 66];
let total = 0;

for (let score in scores) {
  total += scores[score];
}

// Good
const scores = [70, 75, 80, 61, 89, 56, 77, 83, 93, 66];
let total = 0;
const { length } = scores;

for (let i = 0; i < length; i += 1) {
  total += scores[i];
}

12. 배열의 요소를 삭제할 때 delete를 사용하지 않는다

보통 객체(Object)의 프로퍼티를 삭제할 때 delete를 사용한다. 단순히 undefined로 설정되는 것이 아니라 프로퍼티 자체가 완전히 삭제되어 더 이상 존재하지 않는다.


자바스크립트에서 배열(Array)도 객체이기때문에 delete를 사용하면 배열에서 요소가 완전히 삭제되어 배열의 길이가 줄어들 것 같지만, 실제로는 해당 요소 값이 undefined가 될 뿐 배열 길이는 줄어들지 않는다.


해결책 배열의 요소를 삭제할 때는 Array.prototype.splice() 를 사용하거나 배열의 length 프로퍼티를 변경한다

// Bad
const numbers = ["zero", "one", "two", "three", "four", "five"];
delete numbers[2]; // ['zero', 'one', undefined, 'three', 'four', 'five'];

// Good
const numbers = ["zero", "one", "two", "three", "four", "five"];
numbers.splice(2, 1); // ['zero', 'one', 'three', 'four', 'five'];

// Tip - 배열 길이를 줄이고 싶다면 length를 사용
const numbers = ["zero", "one", "two", "three", "four", "five"];
numbers.length = 4; // ['zero', 'one', 'two', 'three'];

13. 순회와 관련 없는 작업을 반복문 안에서 처리하지 않는다

반복문은 주어진 조건 표현식이 true로 평가되는 동안 실행을 반복한다. 반복 작업은 성능에 영향을 미치므로, 코드를 리팩토링할 때 첫 번째로 순환문의 최적화 작업을 고려한다. 동일한 값을 반복적으로 할당하는 것처럼 순회와 상관없는 작업이 반복문 안에서 이뤄지지 않도록 주의한다.


순회와 관련된 작업만 수행하도록 반복문을 최적화한다.

// Bad
for (let i = 0; i < days.length; i += 1) {
  const today = new Date().getDate();
  const element = getElement(i);

  if (today === days[i]) {
    element.className = "today";
  }
}

// Good
const today = new Date().getDate();
const { length } = days;
let element;

for (let i = 0; i < length; i += 1) {
  if (today === days[i]) {
    element = getElement(i);
    element.className = "today";
    break;
  }
}

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