221009 TIL | 자바스크립트 코딩 기법과 핵심 패턴_#1~_#2 정리

패턴

  • 소프트웨어 개발에서 패턴이란 일반적인 문제에 대한 해결책.
  • 곧바로 복사해서 붙여넣을 수 있는 코드 형태의 답이 아닌, 모범적 관행, 쓰임새에 맞게 추상된 원리, 어떤 범주의 문제를 해결하는 템플릿에 가깝다.

패턴의 중요성

  • 검증된 실행 방법을 사용하여 쓸데없이 시간을 낭비하지 않고 더 나은 코드를 작성할 수 있게 도와준다.
  • 일정 정도의 추상화 단계를 제공한다.
    • 인간의 뇌가 주어진 시간동안 생각할 수 있는 정보의 양은 한계가 있기 때문에,
    • 복잡한 문제를 고민할 때 저수준의 세부 사항을 신경쓰지 않아도 되도록,
    • 자기 완결성을 갖춘 구성 요소(패턴)을 사용하여 설명하는 것이 도움된다.
  • 패턴은 개발자와 팀 간의 커뮤니케이션에도 도움이 된다.

패턴의 종류

많은 패턴이 존재하지만, 주로 3가지 패턴에 대해 설명한다.

  1. 디자인 패턴
    • 디자인 패턴 예시로는 싱글톤(singleton), 팩터리(factory), 장식자(decorator), 감시자(observer) 등이 있다.
    • 특정 언어에 종속된 것은 아니지만, 주로 C++나 자바 같은 엄격한 자료형 언어의 관점에서 연구되었다.
    • 느슨한 자료형의 동적인 언어인 자바스크립트에서 디자인 패턴을 자주 적용하지 않지만, 엄격한 자료형 언어가 가지는 특성이나 클래스 기반 상속을 다루기 위해서 차선책이 되기도 한다.
  2. 코딩 패턴
    • 자바스크립트 특유의 패턴이다.
    • 함수의 다양한 활용과 같은 자바스크립트의 독특한 기능과 연결된 실천 방법이다.
  3. 안티 패턴
    • 잘못 사용하는 접근 방법을 말한다.

자바스크립트의 개념

1) 객체지향

  • 자바스크립트는 객체지향 언어이다.
  • number, string, boolean, null, undefined와 같은 다섯 종류의 원시 데이터 타입만 객체가 아니다.
  • number, string, boolean 타입은 객체의 표현과 동일한 원시 데이터 타입 래퍼(primitive wrapper)를 가진다.
  • 이 원시 데이터 값들은 개발자 또는 내부적인 자바스크립트 인터프리터에 의해 쉽게 객체로 변환된다.
  • 자바스크립트의 함수 또한 프로퍼티와 메서드를 가질 수 있기 때문에 객체이다.
  • 자바스크립트에서 변수를 선언한다면 이미 객체를 다루고 있는 것이다.
    • 변수는 자동으로 활성화 객체(Activation Object)라 불리는 내부적인 객체의 프로퍼티가 된다.
      • 전역 변수인 경우 전역 객체의 프로퍼티가 된다
    • 이 변수는 자신만의 프로퍼티를 가지기 때문에 객체와 비슷하다고 볼 수 있다.
    • 변수의 프로퍼티를 attribute라고 한다.
    • attribute의 값에 따라 변수가 수정되거나 삭제될 수 있는지 혹은 for-in 루프로 순회할 때 열거(enumerate)될 수 있는지 등의 여부가 결정된다.
    • ECMAScript 3까지 attributes가 직접 드러나진 않았지만, ECMAScript5에서 attribute 값을 수정할 수 있는 별도의 설명자(descriptor) 메서드가 제공되었다.
  • 두 가지 주요 객체 타입
    • 네이티브 객체 : ECMAScript 표준에 정의된 객체
      • 내장 객체 (예를 들면 Array, Date) 또는 사용자 정의 객체(var a = {};)로 분류된다.
    • 호스트 객체 : 호스트 환경(브라우저 환경 등)에서 정의된 객체
      • window 객체나 모든 DOM 객체를 예로 들 수 있다.
      • 브라우저가 아닌 다른 환경에서 코드를 실행해보면 어떤 객체가 호스트 객체인지 알 수 있다.

🐻 그렇다면, 객체란 무엇일까? 객체는 단지 이름이 지정된 프로퍼티의 모음일 뿐이다. 키-값 쌍으로 이뤄진 목록이다. 객체의 프로퍼티가 함수(함수 객체) 일 경우 메서드라고 부른다.

2) 클래스가 없다

  • 자바스크립트에서는 클래스가 없다. 오로지 객체만을 다룬다.
  • 자바의 객체 생성 방법과 자바스크립트의 객체 생성 방법을 비교해보자.

      // 자바의 객체 생성
      Hello000 hello_oo = new Hello00();
    

    → 같은 단어를 세 번이나 반복하고 있다.

  • 자바스크립트에서는 빈(blank) 객체를 필요한 시점에 생성하고 그 이후에 필요한 멤버를 추가할 수 있다.
    • blank 객체는 사실 완전히 비어 있는 것이 아니라, 몇몇 내장 프로퍼티를 이미 가지고 있지만 자신이 직접 소유(own)한 프로퍼티가 없을 뿐이다.
    • GoF 책에서 말하는 일반적인 규칙 중 하나는 ‘클래스 상속보다 객체의 합성을 우선시하라’는 것이다.
    • 여기저기에 놓여있는 조각들을 사용해 객체를 합성할 수 있다면 복잡한 부모-자식 상속 체인을 사용하거나 클래스화하는 것보다 더 나은 접근 방법이라는 뜻이다.
    • 자바스크립트에서는 규칙을 지키기 쉽다. 클래스가 없어서 어차피 객체 합성을 해야하기 때문

3) 프로토타입

  • 코드를 재사용하는 하나의 방법이지만, 자바스크립트에서 상속을 할 수 있다.
  • 다양한 방법으로 상속을 구현할 수 있지만 주로 프로토타입(prototype)을 사용한다.
  • 프로토타입은 하나의 객체이며, 사용자가 생성한 모든 함수는 새로운 빈 객체를 가리키는 prototype 프로퍼티를 가진다.
  • 프로토타입 객체는 객체 리터럴이나 Object() 생성자로 만든 객체와 거의 비슷하다.
  • 프로토타입 객체의 construtor 프로퍼티가 가리키는 것이 내장된 Object()가 아닌 사용자가 생성한 함수라는 점만 다르다.
  • 사용자는 빈 객체에 멤버를 추가할 수도 있고, 상속을 통해 다른 객체가 이 객체의 프로퍼티를 자기 것인 양 쓰게 만들 수도 있다.

4) 실행 환경

  • 일반적인 자바스크립트 프로그램의 실행 환경은 브라우저이지만, 이것이 유일한 실행 환경은 아니다.
  • 실행 환경은 자신만의 호스트 객체를 제공한다.
  • 호스트 객체는 ECMAScript 표준에 정의되지 않았으며 예상치 못한 방식으로 동작할 수 있다.

ECMAScript5

  • DOM(Document Object Model), BOM(Browser Object Model) 그리고 규정 외의 호스트 객체를 제외한 코어 자바스크립트 프로그래밍 언어는 ECMAScript(또는 ES) 표준에 기반을 두고 있다.
  • ECMAScript5에서 스트릭트 모드(strict mode)라는 기능이 추가되었는데,
    • 실제로는 기능을 추가한 것이 아니라 기능을 제거함으로써 프로그램을 더 간단하게 만들고 오류 발생 가능성을 낮춘다.
    • 문제가 많은 with 구문의 경우 strict 모드일 때 오류가 발생한다.
    • use strict는 문자열에 의해 구동되기 때문에 ES5를 지원하지 않는 구형 브라우저의 경우 무시되는 방법으로 하위 호환성이 유지된다.
      function my() {
      	"use strict"
      	// 함수의 나머지 부문
      }
    

JSLint

  • 자바스크립트는 정적 컴파일을 하지 않는 인터프리터 언어이다.
  • 사소한 타이핑 실수를 알아채지 못한 채 프로그램을 배포할 수 있다. → JSLint는 이런 상황에서 유용하다.
  • https://www.jslint.com/
  • JSLint는 더글러스 크록포드가 개발한 자바스크립트 코드 품질 도구로, 코드를 검사하고 잠재적인 문제에 대해 경고한다.

콘솔

  • 콘솔 객체는 자바스크립트에 포함되어 있지 않지만 개발 환경의 일부분이고 대부분의 최신 브라우저에서 지원한다.

고급 자바스크립트의 코드를 작성하는데 핵심이 되는 모범적인 관행과 습관, 패턴을 검토한다.

예로 들어…

  • 전역 변수의 사용을 최소화한다.
  • var 선언은 한번만 사용한다.
  • 루프 내에서 length는 캐시해두고 사용한다.
  • 코딩 규칙을 준수한다 등…

동료 리뷰 수행, JSLint 실행과 같이 코드 자체 뿐 아니라 코드를 만들어내는 전반적인 습관 과정을 익히면 이해하기 쉽고, 유지보수하기 쉬운 코드를 작성할 수 있다.

유지보수 가능한 코드 작성

  • 소프트웨어의 버그를 고치는 데는 비용이 든다.
  • 비용은 시간이 지날수록 증가하며, 버그가 공개적으로 출시된 제품 안에 숨어 들어간 경우 특히 비용이 커진다.
  • 시간이 흐른 뒤 코드를 다시 들여다 볼때 문제를 다시 학습하고 이해하는 데 걸리는 시간, 문제를 해결하는 코드를 이해하는 데 걸리는 시간이 걸린다.
  • 때문에 코드를 이해하는 데 걸리는 시간을 줄이는 것이 대단히 중요하다.
  • 코드는 작성하는 것보다 읽는 데 많은 시간이 소요된다. 이것은 소프트웨어 개발과 관련된 인생의 진리이다.
  • 애플리케이션이 완성되는 과정에서 여러가지 일이 생기면서 재검토 되고 수정과 변형을 거치며 개발할 때 없었던 오류가 발생할 수 있다. 예로 들어…
    • 버그가 발견된다.
    • 애플리케이션에 새로운 기능이 추가된다.
    • 애플리케이션이 새로운 환경에서 동작해야 한다.
    • 코드의 용도가 변경된다.
    • 코드를 처음부터 완전히 재작성하거나, 다른 구조 심지어 다른 언어로 옮기게 된다.
  • 유지보수가 가능한 코드는 다음과 같은 특징을 가진다.
    • 읽기 쉽다.
    • 일관적이다.
    • 예측 가능하다.
    • 한 사람이 작성한 것처럼 보인다.
    • 문서화 되어 있다.

전역 변수 최소화

  • 자바스크립트는 함수를 사용하여 유효범위를 관리한다.
  • 함수 안에 선언된 변수는 해당 함수의 지역 변수가 되며 함수 외부에서는 사용할 수 없다.
  • 전역 변수는 어떤 함수에도 속하지 않고 선언되거나, 아예 선언되지 않은 채로 사용되는 변수를 가리킨다.
  • 자바스크립트 실행 환경에는 전역 객체(global object)가 존재한다.
  • 어떤 함수에도 속하지 않는 상태에서 this를 사용하면 전역 객체에 접근하게 된다.
  • 전역 변수를 생성하는 것은, 이 전역 객체의 프로퍼티를 만드는 것과 같다.
  • 예로 들어 브라우저에는 전역 객체에 window라는 부가적인 프로퍼티가 존재하는데, 대게 전역 객체 자신/을 가리킨다.
// 전역 변수를 생성한다.
myglobal = "hello"; // 안티패턴
console.log(myglobal); // "hello"
console.log(window.myglobal); // "hello"
console.log(window.["myglobal"]); // "hello"
console.log(this.myglobal); // "hello"

⚡ 전역 변수의 문제점

  • 전역 변수는 자바스크립트 애플리케이션이나 웹페이지 내의 모든 코드 사이에서 공유된다.
  • 애플리케이션 내의 다른 영역에서 목적이 다른 전역 변수를 동일한 이름으로 정의할 경우 서로 덮어쓰게 된다.
  • 웹페이지는 해당 페이지의 개발자가 작성하지 않은 외부 코드를 가져와 삽입하는 일이 종종 있다. 예로 들어…
    • 서드파티 자바스크립트 라이브러리
    • 광고 제휴 업체의 스크립트
    • 사용자를 추적하고 분석하는 서드파티 스크립트 코드
    • 다양한 위짓, 배지, 버튼 등
  • 어떤 서드파티 스크립트에서 result라는 전역 변수를 정의했다고 가정했을 때,
    • 이 페이지의 어떤 함수에서 result라는 또다른 전역 변수를 정의한다.
    • 이 경우 마지막 result 변수가 이전 것을 덮어쓰게 되며, 서드파티 스크립트가 동작하지 않는 나쁜 상황이 발생할 수 있다.
  • 다른 스크립트들이 한 페이지 안에서 공존하려면 전역 변수를 최소한으로 사용해야 한다.
  • 변수를 선언할 때 항상 var를 사용하는 것을 권장한다.책에 ES6가 나오기 전이라…let, const 자료형이 없다 😭
    • 키워드를 사용하여 변수를 선언하지 않았을 때 delete 키워드를 사용해 변수의 정의를 취소할 수 없다/

전역 객체에 대한 접근

  • 브라우저에서는 window 속성을 통해 전역 객체에 접근할 수 있다.
  • 만약 window라는 식별자를 직접 사용하지 않고 전역 객체에 접근하고 싶다면 다음과 같이 정의한다.
var global = (function() {
	return this;
}());

호이스팅(hoistring): 분산된 var 선언의 문제점

  • 자바스크립트는 함수 내 여기저기서 여러 개의 var 선언을 할 수 있지만 실제로는 모두 함수 상단에서 변수가 선언된 것과 동일하게 동작한다.
  • 바로 호이스팅(hoisting) 동작 방식이다.
// 안티 패턴
myname = "global"; // 전역 변수
function func() {
	alert(myname); // "undefined"
	var myname = "local";
	alert(myname); // "local"
}
  • 전역 변수인 myname을 바라보고 global을 출력할 것을 기대하지만, 함수 내에 myname이 선언되었기 때문에 자바스크립트는 myname을 지역 변수로 선언했다고 간주한다.
  • 모든 변수 선언문은 끌어올려지기 때문에 undefined가 출력된다.
  • alert를 출력하고 myname에 local이라는 값이 할당된다.

for 루프

  • 보통 배열이나 arguments, HTMLCollection 등 배열과 비슷한 객체를 순회한다.
// 최적화되지 않은 루프
for(var i = 0; i < myarray.length; i++) {
	// myarray[i]를 다루는 코드
}
  • 루프 순회 시마다 배열의 length에 접근한다.
  • 만약 myarray 배열이 아니라 HTMLCollection이라면 이 때문에 코드가 느려질 수 있다.
  • HTML Collection은 다음과 같은 DOM 메서드에서 반환되는 객체다.
    • document.getElementByName()
    • document.getEelementByClassName()
    • document.getElementByTagName()
  • 기반 문서(HTML 페이지)에 대한 실시간 질의라는 점에서 문제가 발생한다.
  • 때문에 DOM 접근은 일반적으로 비용이 크다.
  • 다음은 배열 또는 컬렉션의 length를 캐시하는 방법으로 for 루프를 좀 더 최적화한 코드이다.
for(var i = 0, max = myarray.length; i < max; i++) {
	// myarray[i]를 다루는 코드
}
  • HTMLCollection을 순회할 때 length를 캐시하면, 사파리3에서 2배, IE7에서 190배에 이르게까지 모든 브라우저에서 속도가 향상된다.
  • JSLint에서는 루프에 조정을 가한다면 i++보다 i = i + 1 , i += 1 처럼 명령문을 작성하는 것을 권장한다.
  • ++와 –는 과도한 기교를 조장하는 것
    • 동의하지 않는다면 JSLint의 설정 값을 끌 수 있다.

for문의 두 가지 변형 패턴

  • 변수를 하나 덜 쓴다.(max가 없다)

      var i, myarray = [];
      for (i = myarray.length; i--); {
      	// myarray[i]를 다루는 코드
      }
    
  • 카운트를 거꾸로 하여 0으로 내려간다. 0과 비교하는 것이 배열의 length 또는 0이 아닌 값과 비교하는 것보다 대개 빠르다

      var myarray = [],
      	i = myarray.length;
        
      while (i--) {
      	...
      }
    

for-in 루프

  • for-in 루프는 배열이 아닌 객체를 순회할 때만 사용한다.
  • for-in으로 루프를 도는 것을 열거(enumeration)이라고도 한다.
  • 객체의 프로퍼티를 순회할 때는 프로토타입 체인을 따라 상속되는 프로퍼티들을 걸러내기 위해 hasOwnProperty() 메서드를 사용해야 한다.
// 객체
var man = {
	hands: 2,
	leg: 2,
	head: 1
};

// 코드 어딘가에서
// 모든 객체에 메서드 하나가 추가되었다
if (typeof Object.prototype.clone === "undefined") {
Object.prototype.clone = function () {}; }
  • 예제에서는 객체 리터럴을 이용하여 간단하게 man이라는 객체를 정의했다.
  • man을 정의하기 전이나 후 어디선가 Object 프로토타입에 clone()이라는 이름의 메서드가 편의상 추가되었다.
  • 프로토타입 체인의 변경 사항은 실시간으로 반영되기 때문에 자동적으로 모든 객체가 이 새로운 메서드를 사용할 수 있다.
  • man을 열거할 때 우리가 추가한 clone() 메서드가 나오지 않게 하려면 hasOwnProperty()를 사용한다.
// 1.
// for-in 루프
for (var i in man) {
if (man.hasOwnProperty(i)) { // 프로토타입 프로퍼티를 걸러낸다.
} }
/*
콘솔에 출력되는 결과
hands : 2 legs : 2 heads : 1 */
// 2.
console.log(i,
" :
, man[i]);
// 안티패턴:
// hasOwnProperty()< 확인하지 않는 for-in 루프 for (var i in man) {
console.log(i, ":", man[i]); }
/*
콘솔에 출력되는 결과
hands : 2
legs : 2
heads : 1
clone: function() */
  • Object.prototype에서 hasOwnProperty()를 호출하는 것도 하나의 패턴이다.
  • 이 방법은 man 객체가 hasOwnPrototype()를 재정의하여 덮어썼을 경우에도 활용할 수 있다는 장점이 있다.
for (var i in man) {
	if(Object.prototype.hasOwnPrototype.call(man, i)) {
		// 걸러내기
		console.log(i, ":", man[i]);
	}
}  
  • 만약 프로퍼티 탐색이 Object까지 멀리 거슬러 올라가지 않게 하려면 지역 변수를 사용하여 이 메서드를 캐시 하는 것도 하나의 방법이다.
var i,
hasOwn = Object.prototype.hasOwnProperty;
for (i in man) {
if (hasOwn.call(man, i)) { // 걸러내기
} }

내장 생성자 프로토타입 확장하기 / 확장하지 않기

  • 생성자 함수의 prototype 프로퍼티를 확장하는 것은 기능을 추가하는 좋은 방법이지만 때로는 지나치게 강력할 수 있다.
  • Object(), Array(), Function()과 같은 내장 생성자 프로토타입을 확장하는 것은 매력적이지만, 코드의 지속성은 심각하게 저해될 수 있다.
  • 따라서 내장 성생자 프로토타입을 확장하지 않는 것이 최선이다.
  • 예외가 허용되려면 다음 조건을 모두 만족시켜야 한다.
    1. 해당 기능이 ECMAScript의 향후 버전이나 자바스크립트 구현에서 일관되게 내장 메서드로 구현될 예정이다. ECMAScript5에 기술되었으나 아직 브라우저에 내장되지 않은 메서드라면 추가할 수 있다. 이 경우에는 유용한 메서드를 미리 정의하는 것이라고 볼 수 있다.
    2. 이 프로퍼티 또는 메서드가 이미 존재하는지, 즉 이미 코드 어딘가에 구현되어 있거나, 지원 브라우저 중 일부 자바스크립트 엔진에 내장되어 있는지 확인한다.
    3. 이 변경사항을 명확히 문서화하고 팀 내에서 공유한다.

switch 패턴

  • 다음 예제는 switch문의 가독성과 견고성을 향상시킬 수 있는 패턴이다.
var inspect_me = 0, result = '';
switch (inspect_me) { 
case 0:
	result = "zero";
	break; 
case 1:
	result = "one";
	break; 
default:
	result = "unknown";
  • 각 case문을 switch문에 맞추어 정렬한다. (일반적인 중괄호 내 들여쓰기 규칙에서 벗어나는 방식이다)
  • 각 case문 안에서 코드를 들여쓰기 한다.
  • 각 case문은 명확하게 break;로 종료한다.
  • break문을 생략하여 통과(fall-through)시키지 않는다. 그런 방법이 최선책이라는 확신이 있다면 해당 case에 반드시 기록을 남긴다. 코드를 읽는 사람에게는 오류로 보일 수 있기 때문이다.
  • 상응하는 case문이 하나도 없을 때도 정상적인 결과가 나올 수 있도록 switch문 마지막에는 default:문을 쓴다.

암묵적 타입캐스팅 피하기

  • 자바스크립트는 변수를 비교할 때 암묵적으로 타입캐스팅을 실행한다.
    • 예로 들어…false == 0 또는 “” == 0은 true를 반환한다.
  • 항상 표현식의 값과 타입을 모두 확인하는 === 그리고 !== 완전항등연산자를 사용한다

eval() 피하기

  • 코드에서 eval()을 발견하면 ‘eval() is evil’이라는 주문을 기억하자.
  • eval()은 임의의 문자열을 받아 자바스크립트 코드로 실행한다.
  • 문제의 코드를 사전에 알 수 있다면(=런타임에 결정되는 게 아니라면) eval()을 쓸 필요가 없다.
  • eval()의 사용은 보안 문제와도 관련된다.
    • Ajax 요청으로 받아온 JSON 응답을 다룰 때 이런 안티 패턴을 흔히 볼 수 있다.
  • setinterval()과 setTimeout() 그리고 Function() 생성자에 문자열을 넘기는 것, new Function() 생성자를 사용하는 것도 권장하지 않는다.
// 안티패턴이다.
setTimeout("myFunc()", 1000); 
setTimeout("myFunc(1, 2, 3)", 1000);
// 권장안
setTimeout(myFunc, 1000); 
setTimeout(function () { 
	myFunc(l, 2, 3);
}, 1000);

parseInt()를 통한 숫자 변환

  • parseInt의 두번째 매개변수를 생략해서는 안 된다.
  • 두번째 매개변수는 기수로 사용되는데 파싱할 문자열이 0으로 시작하는 경우 문제가 생길 수 있다.
    • “09”를 입력받을 경우 8진수로 간주되어 반환 값이 0이 된다.
var month = "06", 
		year = "09";
month = parselnt(month, 10); 
year = parselnt(year, 10);
  • 문자열을 숫자로 반환하는 다른 방법도 있다.
Number("08"); // 8
  • Number()는 parseInt()보다 빠르다.
  • 하지만 parseInt()는 단순히 변환 뿐만아니라 이름이 뜻하는 바대로 파싱을 하기 때문에 “08hello”가 입력될 경우 8을 return할 수 있다.
  • 다른 방법은 NaN이 반환되며 실패한다.

코딩 규칙

  • 코딩 규칙을 수립하고 준수하는 것이 중요한 이유는 이를 통해 코드의 일관성이 유지되고 예측가능해지며 읽고 이해하기 쉬워지기 때문이다.
  • 표준화 규칙을 정해야하는 규칙의 예를 들어보자.
    1. 들여쓰기
    1. tab을 사용하여 들여쓰기를 할 것인가? space를 사용하여 들여쓰기를 할 것인가?
      1. 중괄호
    2. 한 줄 코드에 중괄호를 추가할 것인가?
    3. 중괄호의 위치는 같은 줄, 다른 줄 어디에 둘 것인가?
      1. 공백

요약

  • 전역 변수를 최소화한다. 애플리케이션 당 전역 변수가 한 개만 존재하는 것이 가장 이상적이다.
  • 함수내 var선언을한번만사용한다. 단일한위치에모든변수를모아놓고 지켜볼 수 있고, 변수 호이스팅으로 인해 발생하는 예기치 못한 부작용을 방 지한다.
  • for 루프와 for-in 루프, swith문에 대해 살펴보았다.
  • “eval()은사악하다(eval() is evil).”
  • 내장 생성자 프로토타입을 확장하지 않는다.
  • 코드 작성 규칙을 준수한다. 공백, 들여쓰기를 일관성있게 사용하고, 중괄호와

    세미콜론을 생략할 수 있더라도 반드시 쓴다.

  • 생성자, 함수, 변수명에 명명 규칙을 준수한다.