5 Closure

5.1 클로저의 원리 및 이해

  • MDN 에서는 클로저는 "함수와 그 함수가 선언될 당시의 lexical environment의 상호관계(combination)에 따른 현상 이라고 표현하고있다. 
  • '선언될 당시의 lexical environment는 실행 컨텍스트중 하나인 outerEnvironmentReference에 해당한다.
    • lexicalEnvironment의 environmentRecord와 outerEnvironmentReference에 의해 변수의 유효범위인 스코프가 결정되고 스코프 체인이 가능해진다고 이미 설명한 바 있다.
  • 위에서 의미하는 combination이란, 내부함수에서 외부변수를 참조하는 경우에 한해서만 combination, 즉 '선언될 당시의 LexicalEnvironment와의 상호관계' 이다.

 

var outer = function () {
  var a = 1;
  var inner = function() {
    return ++a;
  }
  return inner();
}

var outer2 = outer();
console.log(outer2);  // 2
console.log(outer2);  // 2
  • 위의 예제를 살펴보면, outer 내부의 inner function이 outer의 a식별자를 참조하고있다. 그러나 특별한 현상은 안보인다.
  • 그러나 outer 함수의 실행컨텍스트가 종료된 시점에서는 a변수를 참조하는 대상이 없어진다.
  • a, (inner 변수의 값)들은 언젠가 가비지컬렉터에 의해 소멸될 것이다.
  • 만약 outer의 실행 컨텍스트가 종료된 후에도 inner 함수를 호출할 수 있게 만들면 어떻게 될까??

 

 

var outer = function () {
  var a = 1;
  var inner = function() {
    return ++a;
  }
  return inner;   // ** 함수의 실행결과가 아닌 함수의 주소 자체를 리턴
}

var outer2 = outer();
console.log(outer2());  // 2
console.log(outer2());  // 3
  • inner 함수의 실행 결과가 아니라 inner 함수 자체를 반환했다.
  • 결과 또한, a가 2,3 이전 값을 기억 했다.
  • inner 함수의 실행 시점에는 outer 함수는 이미 실행종료된 상태인데, outer 함수의 LexicalEnvironment에 어떻게 접근할 수 있을까?
  • 잘 알다시피, 가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그값은 수집대사엥 포함시키지 않는다.
  • inner 함수의 실행 컨텍스트가 활성화되면 outerEnvironmnetReference가 outer 함수의 LexicalEnvironment를 필요로 할 것이므로 수집대상에서 제외된다.

[120page 그림]

(설명: 원래는 LexicalEnvironment 전부를 gc하지 않았으나, 2019년 기준으로 v8엔진에서는 내부함수에서 사용하는 변수만 남겨두고 나머지는 gc)

  • 즉 "어떤함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상" 이란 "외부함수의 LexicalEnvironment가  가비지컬렉팅 되지 않는 현상" 을말하는 것이다.
  • 이를 정리하자면 클로저란 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상이다.

 

다른책에서는 Clousre를 이렇게 표현하는데 그 중 가장 이해하기 쉬운걸 소개함

- 함수를 선언할 때 만들어지는 유효범위가 사라진 후에도 호출할 수 있는 함수 - 존레식, <자바스크립트 닌자 비급>
- 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 함수 - 송형주 고현준, <인사이드 자바스크립트>
- 자신이 생성될 때의 스코프에서 알 수 있었던 변수들 중 언젠가 자신이 실행 될 때 사용할 변수들만을 기억하여유지시키는 함수 - 유인동 <함수형 자바스크립트 프로그래밍>

  • 주의할점은 '외부로 전달' 이 곧 retunr 만을 의미하는 것은 아니다.

 

- return 없이도 클로저가 발생하는 경우

(function () {
  var a = 0;
  var intervalId =  null;
  var inner = function() {
    if(++a >= 10) {
      clearInterval(intervalId);
    }
    console.log(a);
  };
  intervalId = setInterval(inner, 1000);
}) ();
(function () {
  var count = 0;
  var button = document.createElement('button');
  button.innerText = 'click';
  button.addEventListenr('click', function() {
    console.log(++count, 'time clicked');
  });
  
  document.body.appendChild(bttuon);
}) ();
  • 함수내부에서 지역변수를 참조하고 있다. 두상황 모두 지역변수를 참조하는 내부함수를 외부에 전달했기 때문에 클로저 이다.

 

5.2 클로저와 메모리 관리

  • 클로저는 매우 중요한 개녀이나, 일부는 메모리 누수의 위험을 이유로 클로저 사용을 지양해야 한다고 한다.
  • '메모리 누수' 라는 표현은 개발자의 의도와 달리 어떤값의 참조 카운트가 0이 되지않아 GC의 수거대상이 되지 않는 경우 이다.
  • 적절한 사용과 적절한 클로저의 메모리 해제만 있으면 괜찮다.
  • 참조카운트를 0으로 만들면 GC의 수거대상이된다. 식별자에 참조형이 아닌 기본형 데이터 (보통 null이나 undefined)를 할당 하면 된다.
var outer = function () {
  var a = 1;
  var inner = function() {
    return ++a;
  }
  return inner;   // ** 함수의 실행결과가 아닌 함수의 주소 자체를 리턴
}

var outer2 = outer();
console.log(outer2());  // 2
console.log(outer2());  // 3

outer2 = null;             //outer 식별자의 inner 함수 참조를 끊음
(function () {
  var a = 0;
  var intervalId =  null;
  var inner = function() {
    if(++a >= 10) {
      clearInterval(intervalId);
      inner = null;                       //inner 식별자에대하여 참조를 끊음 
    }
    console.log(a);
  };
  intervalId = setInterval(inner, 1000);
}) ();
(function () {
  var count = 0;
  var button = document.createElement('button');
  button.innerText = 'click';
  button.addEventListenr('click', function() {
    console.log(++count, 'time clicked');
    if(count >= 10) {
      button.removeEnventListner('click', clickHandler);
      clickHandler = null; // clickHandler 식별자의 함수 참조를 끊음.
    } 
  });
  
  
  document.body.appendChild(bttuon);
}) ();

 

 

5.3 클로저 활용 사례

5.3.1 콜백함수 내부에서 외부 데이터를 얻고자 할때.

5.3.2 정보은닉

 

5.3.3 부분적용함수

 - debounce

  • 기본적인 예제는 인터넷에서도 많으니 실무에서 활용할 수 있는예제 디바운스만 정리해보았다.
  • 디바운스는 짧은 시간동안 동일한 이벤트가 많이 발생할경우, 이를 전부처리 하지않고, 처음 또는 마지막 발생한 이벤트에 대해 한번만 처리하는 것이다. (Front-End performance 개선에 많은 도움이 된다)
  • scroll, wheel, mousemove,resize 그리고 최근에 검색어 자동완성 등에서 사용한 경험이있다.

 

var debounce = function (eventName, func, wait) {
  var timerId = null;
  return function (event) {
    var self = this;
    console.log(eventName, ' event 발생';
    clearTimeout(timerId);
    timerId = setTimeout(func.bind(self, event), wait);
  };
};

var moveHandler = function(e) {
  console.log('move event 처리');
}

documnet.body.addEventListener('mousemove', debounce('move',moveHandler, 500));

 

 

5.3.4 커링 함수

  • 커링 함수 (currying function)란 여러개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인형태로 구성한 것을 말한다 (흡사 build pattern과 비슷하지만, function 으로 사용되니 좀다르긴 한듯)
  • 커링은 하나의 한개의 인자만 전달하는 것을 원칙으로 한다.
var curry3 = function (func) {
  return function (a) {
    return function (b) {
      return func(a,b);
    }
  }
}

var getMaxWith10 = curry3(Math.max)(10);

consoel.log(getMaxWith10(3));  // 3
console.log(getMaxWith10(20)); // 20
  • 위처럼 일부 매개변수는 고정되고, 가변적인 parameter를 넘겨줄때 사용된다.
  • 프로젝트 내부에서 자주 쓰이는 함수의 매개변수가 항상 비슷하고, 일부만 바뀌는 경우에 대해 이를 적용하면 유용하다.

 

var getHttpRequest = function (baseUrl) { 
  return function (path) {
    return function (id) {
      return fetch(baseUrl + path + '/' + id); 
    }
  }
}


//es6로 만들면 가독성이 훨씬 높아진다.
var getHttpRequest = baseUrl => path => id => fetch(baseUrl + path + '/' + id);
  • getHttpRequest라는 공통함수를 만들었다. 이를 기반으로 아래와 같이 코드 중복을 제거할 수있다.
var userInfoUrl = 'http://localhost:1234/';
var fileUrl = 'http://localhost:8574/';

// 유저 직군별 request 준비
var getUserInfo = getHttpRequest(userInfoUrl);   // http://localhost:1234/
var getDeveloperInfo = getUserInfo('developer'); // http://localhost:1234/developer
var getTaInfo = getUserInfo('ta');               // http://localhost:1234/ta

// file 종류별 request 준비
var getFileInfo = getHttpRequest(fileUrl);   // http://localhost:1234/
var getZipInfo = getUserInfo('zip');         // http://localhost:1234/zip
var getImageInfo = getUserInfo('image');     // http://localhost:1234/image


// 실제 get request
var zipfile = getZipInfo(123);    // http://localhost:1234/zip/123
var zipfile2 = getZipInfo(456);   // http://localhost:1234/zip/456

var image = getImageInfo('navi'); // http://localhost:1234/zip/navi
var image2 = getImageInfo('dog'); // http://localhost:1234/zip/dog


// ..... ommitted

 

  • 특히 이런이유로 커링이 광범위 하게 사용되는데, Redux middleware에서도 사용된다.
// Redux middleware 'Logger'
const logger = store => next => action => {
  console.log('dispatching', action);
  console.log('next state', store.getState());
  return next(action);
}


// Redux Middleware 'thunk'
const thunk = store => next => action => {
  return typeof action === 'function'
    ? action(dispatch, store.getState)
    : next(action);
}
  • 두 미들웨어는 공통적으로 store, next, action 순서로 인자를 받는다.
  • store는 한번만 생성되고 바뀌지 않고, dispatch의 의미를 가지는 next도 마찬가지지만, action은 매 요청마다 달라진다.
  • 그러므로 store, next 값이 결정되면 redux 내부에서 logger 또는 thunk에 store,next를 미리 넘겨서 반환된 함수를 저장시켜놓고, 이후에는 action만 받아서 처리할 수 있게끔 한 것이다.

5.4 정리

  • 클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상이다.
  • 내부함수를 외부로 전달하는 방법에는 함수를 return하는 경우뿐 아니라 콜백으로 전달하는 경우도 포함된다.
  • 클로저는 그 본질이 메모리를 계속 차지하는 개념ㄴ이므로 더는 사용하지 않게 된 클로저에 대해서는 메모리를 차지하지 도록 관리해줄 필요가 있다.

'To be Developer > JavaScript' 카테고리의 다른 글

Core Javascript 3 - this  (0) 2020.05.14
Core Javascript - 2 Execution Context  (0) 2020.05.13
Core Javascript 1 - Data Type  (0) 2020.05.12
[javascript] Closure와 필요성.  (0) 2019.11.08
[javascript] let 과 var 그리고 hoisting  (0) 2019.11.07

3.this

  • 대부분 객체지향 언어에서 this는 클래스로 생성한 인스턴스 객체를 의마한다.
  • 그러나 자바스크립트에서의 this는 어디에서든 사용 가능 하다.
  • 함수와 객체(메서드)의 구분이 느슨한 자바스크립트 에서는 this는 실질적으로 이둘을 구분하는 유일한 기능이다.

 

3.1 상황에 따라 달라지는 this

  • JS에서 this는 기본적으로 실행 컨텍스트가 생성될 때 함께 결정된다.
  • this는 함수를 호출할 때 결정된다. 함수를 어떤 방식으로 호출하느냐에 따라값이 달라진다.

- 전역공간에서 this

  • 전역공간에서 this는 전역 객체를 가르킨다.
//browser 환경에서 전역객체
console.log(this)    // {alert: f() ........}
console.log(window)  // {alert: f() ........}
console.log(this === window) //true
//nodejs 환경에서 전역객체
console.log(this)    // {process: {title: 'node' ........}
console.log(global)  // {process: {title: 'node' ........}
console.log(this === global) //true

 

- 다음 소스코드를 살펴보자.

var a = 1;
console.log(a);         // 1
console.log(window.a);  // 1
console.log(this.a);    // 1
  • 위에서 우리가 확인할 수 있는 사실은 자바스크립트의 모든 변수는 실은 특정객체의 프로퍼티 라는점이다.
  • 특정 객체란 실행 컨텍스트의 LexicalEnvironment 이다.
  • 전역컨텍스트의 L.E는 전역객체를 그대로 참조한다.
  • '전역변수를 선언하면 자동으로 전역객체의 프로퍼티로도 할당한다' 라는 말은 틀린말이다.
  • '전역변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당한다'가 올바른 표현이다.

 

- 그럼 전역공간에서는 var로 indentifer를 선언하는 대신, window의 프로퍼티에 직접 할당해도 상관없을까?

var a = 1;
window.b = 2;
console.log(a, window.a, this.a); // 1 1 1
console.log(b, window.b, this.b); // 2 2 2

window.a = 3;
b = 4;
console.log(a, window.a, this.a); // 3 3 3
console.log(b, window.b, this.b); // 4 4 4
  • 대부분의 경우에는 이말이 맞다
  • 그러나 전역변수의 선언과 전역객체의 프로퍼티 할당사이에 전혀다른 경우가 있다. 'delete' 이다
var a = 1;
delete window.a;                  // false
console.log(a, window.a, this.a); // 1 1 1
var b = 2;
delete b;                  // false
console.log(b, window.b, this.b); // 2 2 2
window.c = 3;
delete window.c;                  // true
console.log(c, window.c, this.c); // error !
  • 전역객체의 프로퍼티로 할당한 경우는 삭제가 되는 반면
  • 전역변수로 선언한 경우에는 삭제가 되지않는다.
  • var로 선언한 전역변수와 전역객체의 프로퍼티는 호이스팅 여부 및 configurable 여부에서 차이를 보인다.

 

3.1.2 메서드로서 호출할 때 그 메서드 내부에서의 this

 - method vs function

  • 함수와 메서드는 근본적으로 코드뭉치이다.
  • 유일한 차이는 독립성에 있다.
  • 함수는 그 자체로 독립적인 기능을 수행하는 반면, 메서드는 자신을 호출한 대상객체에 관한 동작을 수행한다.
  • JS에서는 상황별로 this 키워드에 다른값을 부여하게 함으로써 이를 구현했다.

 

  • JavaScript를 처음 접하는 사람들은 흔히 메서드를 '객체의 프로퍼티로 할당된 함수'로 이해하지만 이는 반은맞고 반은 틀리다.
  • 객체의 메서드로서 호출할 경우에만 메서드로 동작하고, 그렇지않으면 함수로 동작한다.
var func = function (x) {
  console.log(this, x);
}

func(1);  // Window {.....} 1

var obj = {
  method: func
}

obj.method(2); // {methog: f} 2
  • 위의 예제에서 볼 수 있듯이, 원래의 익명함수는 그대로인데 이를 변수에담아 호출한 경우와 obj객체의 프로퍼티에 할당해서 호출한 경우에 this가 달라지는 것이다.

 

- 그렇다면 '함수로서 호철'과 '메서드로서의 호출'을 어떻게 구분할까??

  • 앞에 점(.)이 있는지 여부만으로 간단하게 구분할 수 있다. (대괄호 표기법 포함)

 

var obj = {
  method: function (x) { console.log(this, x); }
}
obj.method(1);    // { method: f } 1
obj['method'](2); // { method: f } 2
  • 점 표기법이든, 대괄호 표기법이든 어떤 함수를 호출할 때 그 함수이름 앞에 객체가 명시돼 있는 경우는 메서드로써의 호출이다.

 

- 메서드 내부에서의 this

  • this에는 호출한 주체에 대한 정보가 담긴다.
  • 마지막 점 앞에 명시된 객체가 곧 this이다.
var obj = {
  methodA: function() { console.log(this); }
  inner: {
    methodB: function() { console.log(this); }
  }
}

obj.methodA();               // { methodA: f, inner { .... } } { === obj}
obj['methodA']();            // { methodA: f, inner { .... } } { === obj}

obj.inner.methodB();         // { methodB: f } { === obj.inner}
obj.inner['methodB']();      // { methodB: f } { === obj.inner}
obj['inner'].methodB();      // { methodB: f } { === obj.inner}
obj['inner']['methodB']();   // { methodB: f } { === obj.inner}

 

3.1.3 함수로서 호출할 때 그 함수 내부에서의 this

  • fucntion을 함수로서 호출할 경우에는 this가 지정되지 않는다.
  • this란 호출한 주체의 정보이기 때문이다. 그러므로 함수로서의 호출의 this는 window 객체이다
  • 그러나 더글라스 크락포드는 이를 명백한 설계상의 오류라고 지적했다.

 

- 메서드의 내부함수에서의 this

  • 앞서 말한 '설계상의 오류' 때문에 실제 동작과 우리의 예측과는 다를 때가있다.
var obj1 = {
  outer: function () {
    console.log(this)              // (1)
    var innerFunc = function() {
      console.log(this)           // (2) (3)
    }
    innerFunc();
    
    var obj2 = {
      innerMethod: innerFunc
    };
  
    obj2.innerMethod();
  }
};

obj1.outer();
  • 정답은 (1): obj1, (2): window, (3): obj2이다.
  • 같은함수임에도 7번째줄에 binding 되는 this와 12번째줄에 의해서 바인딩되는 this가 달라진 것이다.
  • 즉 this란 함수를 실행하는 당시 scope와는 관계없이, function 앞의 object가 가장중요한 것이다.

 

- 메서드의 내부 함수에서의 this를 우회하는 방법

  • 위와같은 상황에서 우리는 this를 확실하게 구분할 수 있지만, 본래 this의 취지와는 달라졌다.
  • 호출주체가 없을때도 주변 환경의 this를 그대로 상속받는게 더 일관적이다.
  • this 역시 현재 컨텍스트에 바인딩 된 대상이 없으면 직전 컨텍스트의 this를 바라보도록 말이다.
  • 가장 대표적인 방법은 변수를 활용하는 방법이다.
var obj1 = {
  outer: function () {
    console.log(this)             // (1) { outer : f}
    var innerFunc1 = function() {
      console.log(this)           // (2) { window }
    }
    innerFunc1();
    
    
    var self = this;
    var innerFunc2 = function() {
      console.log(self);
    }
  
    innerFunc2();               // (3) { outer: f }
  }
};

obj1.outer();
  • 위처럼 변수에 할당해서 출력한다면 의도한 대로 동작한다.
  • 사람마다 self, _this, that 혹은 다른변수명을 많이쓰지만 self가 가장많이쓰인다.

 

- ES6 에서 this를 바인딩하지 않는 함수

var obj = {
  outer: function () {
    console.log(this);   // (2) { outer: f }
    var innerFunc = () => {
      console.log(this); // (2) { outer: f }
    };
    innerFunc();
  }
};

obj.outer();
  • arrow function에서 실행컨텍스트를 생성할때 this binding 자체가 빠지게 되어, 상위 스코프의 this를 그대로 활용할 수 있다. 
  • react에서 arrow function 사용시에 this를 bind 해주지 않아도 되는 이유도 위와같은 이유때문이다.
    (arrwo function을 쓰지않는다면 this.methodName.bind(this); 이런식으로 해주어야함. 그렇지않으면 null)

 

3.1.4 콜백함수 호출시 그 함수 내부에서의 this

  • 함수 A의 제어권을 다른 함수(또는 메서드) B에게 넘겨주는 경우 함수 A를 콜백 함수라 한다.
  • A는 함수 B의 내부로직에 따라 실행되며, this 역시 함수 B내부로직에서 정한 규칙에 따라 값이 결정됨.
  • 기본적으로 콜백함수도 함수라 window를 참조하지만, 제어권을 받은 함수에서 콜백함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 됩니다.

 

setTimeout(function () {console.log(this); }, 300); // (1)

[1,2,3,4,5].forEach( function (x) {                 // (2)
  console.log(this, x);
});

documnet.body.innHTML += '<button id="a"> 클릭 </button>';
documnet.body.querySelector('#a').addEventListener('click', function (e) { console.log(this,(e); });
  • 위는 콜백함수 이다.
  • 1,2의 경우는 따로 this를 지정하지 않아 this는 window 객체가 된다.
  • 그러나 마지막 3번째 콜백함수에서는 제어권을 앞에 지정한엘리먼트에게 수여하기때문에 this 는 a tag가 될 것이다.

 

3.1.5 생성자 함수 내부에서의 this

  • 객체지향 언어에서는 생성자를 class, 클래스를 통해 만든 객체를 instance라고 한다.
  • 프로그래밍 적으로 본다면 생성자는 인스턴스를 만들기위한 틀이다.
  • 어떤 함수가 생성자 함수로서 호출된 경우 내부에서 this는 만들어질 구체적인 인스턴스이다.
  • 생성자 함수 (new 명령어)를 호출하면 생성자의 prototype 프로퍼티를 참조하는 __proto__ 라는 프로퍼티가 있는 개체를 만들고, 공통 프로퍼티등을 this에 부여합니다.
  •  
var Cat = function (name, age) {
  this.bark = '야옹';
  this.name = name;
  this.age = age;
}

var choco = new Cat('초코',7);
var nabi = new Cat('나비', 5);
console.log(choco, nabi);

/*
Cat{ bark: '야옹' name: '초코' ..... }
Cat{ bark: '야옹' name: '나비' ..... }

*/

 

 

3.2 명시적으로 this를 바인딩 하는 방법

3.2.1 Call method

  • call method는 호출 주체인 함수를 즉시 실행하도록 하는 명령이다. 
// Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])

var func = function (a,b,c) {
  console.log(this, a, b, c);
}

func(1,2,3);     // Window {...} 1 2 3
func({ x: 1 }, 3,4,5) // { x: 1} 4 5 6
  • 위와같이 call method를 사용하면 임의 객체로 this를 지정 할 수있다.
// Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])

var obj = { 
  a: 1,
  method: function (x) {
    console.log(this.a, x, y);
  }
}

obj.method(2,3);             // 1 2 3
obj.method.call({a:4}, 5, 6) // 4 5 6

 

3.2.2 apply Method

  • call method와 거의 비슷하지만, 두번째 이후 인자의 매개변수는 array로 한번에 받는다.
// Function.prototype.call(thisArg[, argsArray])

var obj = { 
  a: 1,
  method: function (x) {
    console.log(this.a, x, y);
  }
}

obj.method(2,3);             // 1 2 3
obj.method.apply({a:4}, [5, 6]) // 4 5 6

 

3.2.3 실 사용 예시

- 생성자 내부에서 다른 생성자 호출

  • 생성자 내부에 다른 생성자와 공통된 내용이 있을 경우 call 또는 apply를 이용해 다른 생성자를 호출하면, 간단하게 반복을 줄일 수 있다.
  • (마치 상속과 비슷한 느낌이다.)
function Person(name, gender) {
  this.name = name;
  this.gender = gender;
}

function Student(name,gender,school) {
  Person.call(this, name, gender);
  this.school = school;
}

function Employee(name,gender,company) {
  Person.call(this, name, gender);
  this.company = school;
}

var by = new Student('보영', 'female', '한국대');
var jn = new Employee('재난', 'male', '구글');

 

- 여러 인수를 묶어 하나의 배열로 전다할고 싶을때 - apply 활용

var number = [10, 20, 3, 16, 45];
var max = min = numbers[0];
numbers.forEach(function(number) {
  if(number > max) {
    max = number;
  }
  if(number < min) {
    min = number;
  }
});

console.log(max, min);          // 45 3
  • 위와 같이 배열의 가장 큰값 작은값을 구하려면 구현해야된다.
  • 그러나 Math.min, Math.min 과 apply를 사용한다면 아주 간결하게 만들수있다. 
var number = [10, 20, 3, 16, 45];
var max = Math.max.apply(null, numbers);
var min = Math.min.apply(null, numbers);
console.log(max, min);          // 45 3
  • es6에서는 sperad 연산자의 등장과 함께, 더욱 쉽게 구현 가능해졌다.
  • 그러나 실무에선 es5환경이 훨씬많으므로, 위와같은 방법을 더 많이쓴다.
var number = [10, 20, 3, 16, 45];
var max = Math.max(...numbers);
var min = Math.min(...numbers);
console.log(max, min);          // 45 3

 

3.2.4 bind 메서드

  • bind는 ES5에서 추가된 기능으로, call과 비슷하지만 즉시 호출하지않고, 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환하는 메서드이다.
// Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])

var func = function (a,b,c,d) {
  console.log(this, a, b, c, d);
}

func(1,2,3,4);     // Window {...} 1 2 3 4


var bindFunc1 = func.bind( { x: 1 } );
bindFunc1(3,4,5,6) // { x: 1} , 3,4,5,6

var bindFunc2 = func.bind( { x: 1 }, 4, 5 );
bindFunc2(5,6) // { x: 1}  4 5 5 6
bindFunc2(6,7) // { x: 1}  4 5 6 7

 

- name property

  • bind 메서드를 적용해서 새로만든 함수는 name property에 bound 접두어가 붇는다.
  • 'bound xxx' 라면 xxx원본 함수에 bind 메소드를 적용한 것이다.
  • call 이나 apply 보다 코드를 추적하기에 더 수월하다.
var func = function(a,b,c,d) {
  console.log(this,a,b,c,d);
};

var bindFunc = func.bind({x:1}, 4, 5);
console.log(func.name);       // func
console.log(bindFunc.name);   // bound func

 

-상위 컨텍스트의 this를 내부함수나 콜백함수에 전달하기 call vs bind

var obj1 = {
  outer: function () {
    console.log(this)              
    var innerFunc = function() {
      console.log(this)     
    }
    innerFunc.call(this);
  }
};

obj1.outer();
var obj1 = {
  outer: function () {
    console.log(this)              
    
    var innerFunc = function() {
      console.log(this)     
    }.bind(this);
    
    innerFunc();
  }
};

obj1.outer();

 

- *** callback 함수에 this context 전달

var obj1 = {
  printThis: function() {
    console.log(this);
  },
  printThisLater1: function() {
    setTimeout(this.logThis, 500);
  },
  printThisLater2: function() {
    setTimeout(this.logThis.bind(this), 500);
  },
};

obj1.printThisLater1();  // window { ... }
obj1.printThisLater2();  // obj { logThis: f, ... }

// polling 기능을 만들던중, polling이 좀 비효율적이라 
// long polling으로 기능을 만들고 싶었다.
// react component 내부에서 setTimeOut 함수를 호출 할경우, 
// state를 읽지못하는 이슈가 존재했다. function 실행하는 주체가 global 객체로 변하면서
// state를 읽을수 없는 문제였다.
// 그러나 this context에 대한 개념을 이해한뒤에 bind로 this 값을 전달하면서 
// long polling을 위해 state값을 읽고 해결할수있었다.

 

3.2.5 Arrow Function의 예외 사항

  • ES6에 새롭게 도입된 Arrow Function은 실행 컨텍스트 생성 시 this를 바인딩 하는 과정이 제외되었다.
  • 이 내부에는 this가 아예 없으며, this에 접근하면 스코프체인상 가장 가까운 this에 접근하게 된다.
var obj = {
  outer: function () {
    console.log(this);
    var innerFunc = () => {
      console.log(this);
    }
    innerFunc();
  }
}

obj.outer();
  • 이렇게 하면 구지 변수로 this를 우회하거나 call/apply/bind를 사용할 필요가 없다. (ES6 한정)

 

3.2.6 별도의 인자로 this를 받는경우 (콜백 함수 내에서의 this)

  • 콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this를 인자로 지정할 수 있는 경우가 있다.
  • 배열메서드에 많이 포진되어 있으며 ES6의 map, set에서도 일부 존재한다.
var report = {
  sum: 0,
  count: 0,
  add: function () {
    var args = Array.prototype.slice.call(arguments);
    args.forEach(function (entry) {
      this.sum += entry;
      ++this.count;
    }, this);
  },
  average: function() {
    return this.sum / this.count;
  }
}

report.add(60, 85, 95);
console.log(report.sum, report.count, report.average()); // 240 3 80
  • 9번째 줄에서 전달해준 this가 바인딩 되면서, 콜백함수에서도 sum,count를 참조할 수 있게 된다.

- (참고) 콜백함수와 함께 thisArg를 인자로 받는 메서드들

Array.prototype.forEach(callback[, thisArg])
Array.prototype.map(callback[, thisArg])
Array.prototype.filter(callback[, thisArg])
Array.prototype.some(callback[, thisArg])
Array.prototype.every(callback[, thisArg])
Array.prototype.find(callback[, thisArg])
Array.prototype.findIndex(callback[, thisArg])
Array.prototype.flatMap(callback[, thisArg])
Array.prototype.from(arryLike[, callback[, thisArg])
Map.prototype.forEach(callback[, thisArg])
Set.prototype.forEach(callback[, thisArg])

 

3.3 정리

  • 다음 규칙은 명시적 this 바인딩이 없는 한 늘 성립합니다. this를 예측하는 연습을 해봅시다.
    1. 전역공간에서 this는 전역객체 (window or global)을 참조한다
    2. 어떤 함수에서 메소드로 this를 호출한경우 호출 주체 (메서드앞 객체) 를 참조한다.
    3. 어떤 함수를 함수로서 호출할경우 this는 전역객체를 참조한다.
    4. 콜백함수 내부에서의 this는 해당 콜백함수의 제어권을 넘겨받은 함수가 정의 한 바에 따르며, 정의하지 않으면 전역객체를 참조
    5. 생성자 함수에서 this는 생성 될 인스턴스를 참조
  • 다음은 명시적 this 바인딩이다. 위 규칙의 부합하지 않을 경우 다음 내용을 바탕으로 this를 예측가능하다.
    1. call, apply 메서드는 this를 명시적으로 지정하면서 함수 또는 메서드를 호출.
    2. bind 메서드는 this 및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만듬
    3. 요소를 순회하면서 콜백 함수를 반복호출하는 일부 메서드는 this를 인자로 넘겨 줄 수 있다.

02 실행컨텍스트

1.실행컨텍스트란?

  • 자바스크립트는 어떤 실행 컨텍스트가 활성화 되는 시점에 선언된 변수를 hosting 하고 외부환경정보, this 값을 정하는등의 동작을 수행함.
  • 실행컨텍스트란 실행할 코드에 제공할 환경 정보들을 모아놓은 객체이다.
  • 동일한 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성하여 이를 Call Stack에 쌓음.
  • es5에서는 하나의 실행 컨텍스트를 구성할 수 있는 방법에는 전역공간, eval() 함수, 함수 등이 있다.
var a = 1; // --------------------- (1)
function outer() {
    function inner() {
        console.log("inner", a); //undefined
        var a = 3; 
    }
    inner();//------------- (2)
    console.log("outer", a); 
}
outer(); // -----------------(3)
console.log("global", a);

// 실행결과
// inner undefined
// outer 1
// global 1

 

순서 1 2 3 4 5 6 7
        inner      
      outer outer outer    
    전역컨텍스트 전역컨텍스트 전역컨텍스트 전역컨텍스트 전역컨텍스트  

 

  • (2) 에선 전역 컨텍스트가 콜스택에 담긴다. 전역컨텍스트 또한 일반적 실행 컨텍스트이다.
  • (3)에서 outer 함수를 호출하면 자바스크립트 엔진은 outer에 대한 환경정보를 수집후 컨텍스트를 콜스택에 올린다. 
  • inner 내부에서 변수 a값을 출력하고 나면, inner 함수가 종료되면서 콜스텍에서 컨텍스트가 제거된다.
  • 나머지 컨텍스트도 순차적으로 Call Stack에서 제거 될 것이다.
  • 즉 스택 맨위에 쌓이는 순간이 곧 현재 실행할 코드에 관여하게 되는 시점이다.
  • 실행 컨텍스트에 담기는 정보는 다음과 같다.
    1. VariableEnvironment: 현재 컨텍스트 내의 식별자들에 대한정보 + 외부환경정보, 선언시점의 LexicaEnvironment의 스냅샷으로, 변경사항은 반영되지 않음
    2. LexicalEnvironment: 처음에는VariableEnvironment와 같지만, 변경사항이 실시간으로 반영됨
    3. ThisBinding: this 식별자가 바라봐야할 대상객체.

 

2 VariableEnvironment

  • VariableEnvironment의 담기는 정보는 LexicalEnvironment와 타지만, 최초 실행시의 스냅샷을 유지한다는 점이 다르다.
  • 주로 실행컨텍스트를 생성할때  VariableEnvironment에 정보를 먼저 담은뒤, 그대로 복사해서 LexicalEnvironment를 만들고 LexicalEnvironment정보를 활용함

3 LexicalEnvironment

  • lexical environment대한 번역은 '어휘적 환경', '정적 환경' 이나 둘다 애매하므로 '사전적인' 이라는 느낌으로 외우면 편하다
  • 예를 들면 a,b,c 와 같은 식별자가 내부컨텍스트에 있고, 그 외 정보는 D를 참조하도록 구성돼 있다 이런식으로 컨텍스트를 구성하는 환경정보들을 사전에서 접하는 느낌으로 모아놓은 것이다.

3.1 envicronmentRecord와 호이스팅

  • environmentRecord 에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장됨.
    • 컨텍스트를 구성하는 함수에 지정된 매개변수 식별자(전역은 x)
    • 선언한 함수가 있을경우 그 함수 자체
    • var로 선언된 변수의 식별자
    • 컨텍스트 처음부터 끝까지 한줄한줄 읽어나가며 순서대로 수집함.
 전역 실행 컨텍스트는 변수 객체를 생성하는 대신, 구동환경이 별도로 제공하는 객체 즉 global object를 활용한다. browser에 window 또는 nodejs의 global이 이에 해당됨.
 이들은 nativce object가 아닌 host object로 분류됨. 

 

  • 변수정보를 수집하는 과정을 모두 마쳤더라도, 코드가 실행되기 전의 상태이다.
  • 그러므로 자바스크립트 엔진은 이미 해당 환경에 속한 변수명들을 모두 알고있다.
  • '자바 스크립트 엔진은 식별자들을 최상단으로 끌어올려놓은 다음 코드를 실행한다'
  • 여기서 호이스팅이라는 개념이 등장한다. (실제로 끌어올리지는 않지만 편의상 끌어올린다 표현)

 

3.2 호이스팅 규칙

  • environmentRecord에는 매개변수의 이름, 함수 선언, 변수명 등이 담긴다고 했다.
fuction a (x) {     // 수집대상 1(매개변수)
    console.log(x); // (1)
    var x;          // 수집대상 2(변수선언)
    console.log(x); // (2)
    var x = 2;      // 수집대상 3(변수선언)
    console.log(x); //(3)
}
a(1)
  • 호이스팅이 되지 않을때 (1) (2) (3) 에서 어떤값이 나올지 예상해보자.
  • (1) 에는 1이 호출되고
  • (2) 에는 undefined에가 출력되고
  • (3) 에는 아마도 2가 호출될 것 같다... 과연 그럴까?!

 

- 설명을 위해 우리가 자바스크립트 엔진이라 생각하고 위의 코드를 Hoisting 되게 바꿔보자. 

fuction a () {     
  var x; // 수집대상 1(매개변수)
  var x; // 수집대상 2(변수선언)
  var x; // 수집대상 2(변수선언)
  
  x = 1; // (매개변수 대입)
  console.log(x); // (1)    
  console.log(x); // (2)
  x = 2;      // 값 대입
  console.log(x); //(3)
}
a(1)
  • 위와 같이 호이스팅 된 결과이다.
  • 호이스팅이 끝나고 코드가 실행될 차례이다 (스코프체인 수집 및 this 할당과정은 이후 단원에서 나옴)
  • 실제로는 1, 1, 2 라는결과가 나왔다. hoisting 이라는 개념 때문이다.

 

- 한가지 호이스팅 되는 예를 더 살펴보자.

fuction a () {     
  console.log(b);  // (1)
  var b = 'bbb';   // 수집대상 1 (변수선언)
  console.log(b);  // (2)
  function b () {} // 수집대상 2 함수선언
  console.log(b);  // (3)
}
a();
  • 호이스팅에 대해 잘 모른다면 결과는 undefined / bbb / fuction... 이 예상된다.

 

- 그러나 실제로는 아래의 예시처럼 될 것이다.

fuction a () {     
  var b;           // 수집대상 1.변수는 선언부만 끌어올림
  var b = function b () { }// 수집대상 2.함수 선언은 저체를 끌어올림
                           // 호이스팅이 긑난 상태에서 함수 선언문은 
                           // 함수명으로 선언한 변수에 함수를 할당했다고 볼 수있다.
  
  console.log(b); // (1)
  b = 'bbb';
  console.log(b); // (2)
  console.log(b); // (3)
}
a();
  •  위와같이 호이스팅이 예상되며, 실행 결과는 b 함수 / 'bbb' / 'bbb' 가 될 것이다.

 

- 함수 선언문과 함수 표현식

  • 함수 선언문: function의 정의부만 존재. 반드시 함수명이 정의되어 있어야함.
  • 함수 표현식: function을 별도의 변수에 할당하는 것. 함수명이 없어도 됨
  • 함수를 정의한 함수 표현식을 '기명 함수 표현식' 이라하며, 일반적으로 함수 표현식을 '익명함수 표현식' 이라고 한다.
// 함수를 정의하는 3가지 방식
function a() {/* .... */}        //함수 선언문 a가 곧 변수명
a() //실행 ok

var b = function () {/* .... */} //함수 표현식 변수명 b가 곧 함수명
b() //실행 ok

var c = function d () {/* .... */} // 기명함수 표현식, 변수명은 c, 함수명은 d.
c(); // 실행 ok
d(); // 실행 error

 

 

- 둘의 차이는 무엇인지 함수 선언문과 함수 표현식의 차이를 알아보자.

console.log(sum(1,2));
console.log(mul(3,4));

//함수 선언문
function sum (a, b) {
  return a+b;
}


//함수 표현식
var mul = function (a, b) {
 return a*b;
}
  • 예상하건데 우리가 호이스팅에서 배웠듯이 결과는 3 / 12가 나올것같다.
  • 그러나 함수 표현식은 호이스팅이 다르다.

 

//함수 선언문은 전체를 호이스팅 함.
var sum = function (a, b) {
  return a+b;
}

//함수 표현식은 변수 선언부만 끌어올린다.
var mul 

console.log(sum(1,2));
console.log(mul(3,4));


//함수 표현식
mul = function (a, b) {
 return a*b;
}
  • 함수 표현식의 호이스팅 방식때문에 위의 결과는 3 / mul is not function 이라는 결과가 나올것이다.
  • 함수 선언문은 아무 문제없이 잘 실행되지만, 반대로 큰 혼란의 원인이 되기도 한다.

 

- 혼란 상황을 가정해보자. 회사에서 협엽을 하는데 A대리가 이미 SUM이라는걸 만들었다. 근데 만약 B라는 사원이 logging을 위해서 다음과 같이 SUM을 재정의 한 것이다.

console.log(sum(3,4)); // (1)

var sum = function (a, b) {
  return a+b;
}

.......

var a = sum(1,2);    //(2)

.......

function sum (x,y) {
  return x + "+" + y + " : " + x+y;
}

var c = sum(1,2);
console.log(c);     //(3)
  • 첫번째 console. 함수의 결과는 문자열을 반환 할 것이다.
  • 두번째 var a도 원래 의도는 number type의 값이 들어가야 맞지만, string 값이 들어가 있을것이다. (자바스크립트는 형변환이 없기에)
  • 결국, 소스코드의 오류는 없지만 전혀 다른 기대값이 들어가므로 굉장히 찾기 어려운 오류를 야기 할 것이다.
  • 그러므로 되도록 함수 표현식을 사용하는게 낫다.

 

3.3 스코프, 스코프 체인, outerEnvironmentReference

  • 스코프란 식별자(idnetifier)에 대한 유효범위 이다.
  • 어떤 함수 A의 외부에서 선언한 변수는 외부 scope 뿐만 아니라 A내부에서도 접근 가능하다.
  • 반면 A의 scope 내에서 선언된 변수는 A안에서만 접근 가능하다.
  • ES5는 function scope 이다. (Es6 부터는 bracket 스코프도 가능)
  • 식별자의 유효범위를 안에서부터 바깥으로 차례로 검색하는 것을 Scope Chain 이라한다.
  • 이게 가능한 이유는 LexicalEnvironment의 두번째 수집자료인 outerEnvironmentRefrence 이다.

- Scope Chain

  • outerEnvironmentRefrence는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조한다.
  • '선언될 당시' 라는 점을 유의해야한다.
  • '선언하다' 라는 행위가 일어나는 시점은 Call Stack 상에서 실행 컨텍스트가 활성화 된 상태일 때뿐이다.

 

var a = 1;
var outer = function() {
  var inner = function () {
    console.log(a);
    var a = 3;
  }
  inner();
  console.log(a);
}

outer();
console.log(a);
  • 위의 예에서 풀어서 이야기해보자.
    1. Start: 전역컨텍스트가 활성화 된다. 전역컨텍스트의 environmentRecode에는 {a, outer} 식별자를 저장한다. 전역컨텍스트는 선언시점이 없으므로 outerEnvironmentReference에는 아무것도 담기지않음 (ths: 전역객체)
    2. 2번째줄 outer 실행 컨텍스트의 environmentRecord에 { inner } 식별자를 저장함. outerEnvironmentReference에는 outer 함수가 선언될 당시의 LexicalEnvironment가 담긴다. outer는 전역공간에 선언 되었으므로, 전역공간의 LexicalEnvironment를 참조복사한다. 이를 [ GLOBAL, { a , outer } ] 라고 표현하자 (this: 전역객체)
    3. 3번째줄 inner 실행 컨텍스트의 environmentRecord에 { a } 에 식별자를 저장함. outerEnvironmentRefernce에는 outer 함수의 LexicalEnvironment 즉, [ outer, { inner }] 를 참조복사 할 것이다 (this: 전역객체)
  • inner의 outerEnvironmentReference는 함수 outer의 LexcialEnvironment를 참조한다.
  • 함수 outer의 LexicalEnvironment 내부에 있는 outerEnvironmentReference는 전역 LexicalEnvironment를 참조한다.
  • 즉 무조건 스코프 체인상에서 가장 먼저 발견된 식별자에만 접근가능하다.
  • 위의 코드의 결과는 스코프체인의 특성때문에 undefined / 1 / 1 이라는 결과가 나온다.

 

코어 자바스크립트 57page 그림

 

  • 주의 해야할점은 스코프체인상에 있는 변수라고 해서 무조건 접근 가능한것은 아니다.
  • 위의 코드에서 inner 스코프의 LexicalEnvironment의 a라는 식별자가 존재하므로, 스코프체인 검색을 더이상 진행하지않고, undefined를 print 한것처럼 말이다. 이를 변수 은닉화 (variable shadowing) 라 한다.

 

참고: 크롬 브라우저 환경에서는 스코프체인중 현재 실행컨텍스트를 제외한 상위 스코프 정보들을 개발자 도구의 콘솔을 통해 간단하게 확인 할 수있다.

 

var a = 1;
var outer = function () {
  var b = 2;
  var inner = function () {
    console.dir(inner);
  }
  inner();
};
outer();

 

 

디버거를 이용하면 좀 더 제대로 된 정보를 확인 할 수 있다. 이는 모든브라우저에서 가능하다. (사파리가 가장 자세하다고함)
var a = 1;
var outer = function () {
  var b = 2;
  var inner = function () {
    console.dir(b);
    debugger;
  }
  inner();
};
outer();

 

 

 

3.4 전역변수 (global variable) 와 지역변수(local variable)

  • 여기 까지 왔다면 전역변수와 지역변수는 이해가 가능 할 것이다.
  • 전역객체에 선언된 outer, a는 전역변수이다
  • outer의 함수 스코프 내부에서 선언된 b와 inner는 지역변수이다.
  • 위에 sum이라는 함수의 재표기가 문제가 되는 경우는, 전역변수안에서 선언 했기 때문에 문제가 된것이다.
  • 코드의 안정성을 위해, 전역변수 사용을 최소하 해야한다.

 

3.5 this

  • 실행 컨텍스트가 활성화 당시에 this가 지정되지 않은 경우는 this는 전역객체를 가리키게 된다.
  • 그 밖에는 함수를 호출하는 방법에 따라서 this에 저장되는 대상이 다르다.

 

3.6 정리

  • 실행 컨텍스트는 객체 활성화되는 시점에 VariableEnvrionment, LexicalEnvironment, ThisBinding의 세가지 정보를 수집한다.
  • VariableEnvironment와 LexicalEnvironment는 동일한 내용으로 구성되지만, LexicalEnvironment는 함수 실행도중에 변경되는 사항이 즉시 반영되지만, VariableEnvironment는 초기상태를 유지한다
  • L.E 에는 매개변수명, 변수의 식별자, 선언한 함수의 수명등을 수집하는 environmentRecord와 바로 직전의 컨텍스트의 lexicalEnvironmnet정보를 참조하는 otuerEnvironmentReference로 구성된다.
  • 스코프는 변수의 유효범위를 만한다. outerEnvironmentReference는 해당 함수가 선언된 위치의 LexicalEnvironmnet를 참조한다. 코드상에서 변수에 접근하려고 할때 현재 컨텍스트 LexicalEnvironment에서 찾아보고 없으면 outerEnvironmentRecord에 담긴 LexicalEnvironment에서 찾는다. Scope Chain 에서 계속 못찾게 된다면 undefined를 반환 한다.

'To be Developer > JavaScript' 카테고리의 다른 글

Core Javascript - 5 Closure  (0) 2020.05.15
Core Javascript 3 - this  (0) 2020.05.14
Core Javascript 1 - Data Type  (0) 2020.05.12
[javascript] Closure와 필요성.  (0) 2019.11.08
[javascript] let 과 var 그리고 hoisting  (0) 2019.11.07

1. 종류

1.1 기본형 : number, string, boolean, null, undefined 1.2 참조형 : obejct, Array, Function, Date, RegExp 등 (ES6) Map, WeakMap, Set, WeakSet 등이 객체의 하위분류임.

1.3 기본형 vs 참조형?

  • 자바스크립트의 데이터타입에는 크게 두가지가 있다.
  • 기본형은 값이 담긴 주소값을 바로복제.
  • 참조형은 값이담긴 주솟값들로 이루어진 묶음을 가리키는 주소값을 복제.
  • 기본형은 불변성(Immunitability)을 가지게 된다.

 

2. 데이터 타입에 관한 배경지식

2.2 식별자와 변수

  • 변수와 식별자를 혼용해서 쓰지만, 이는 엄연히 다르다.

  • 변수란 변할 수 있는 수이다.
  • 식별자란 데이털르 식별하는데 사용하는 이름 즉 변수명이다.

 

3. 변수 선언과 데이터 할당.

3.1 변수 선언

// ex1) 변수 선언
var a;
  • 위를 해설하자면 변할 수 있는 데이터를 만든다, 이 데이터의 식별자는 a로 한다 이다.

  • 이해를 위해 간단하게 메모리에 대해 표현해보자

 

ex2) 표 간략화 메모리

주소 ... 1002 1003 1004 ...
데이터   이름: a
값:
     
  • ex1 과 같은 명령을 받은 컴퓨터는 메모리에 비어있는 공간 하나를 확보한다

 

3.2 데이터할당

  • 먼저 아래와 같은 코드가 있다고 가정해보자
 // ex3) 변수 선언과 할당
 var a; // 변수선언
 a = 'abc'; // 데이터 할당

 var a = 'abc' // 한문장으로 표현

ex4) 위 코드 데이터 할당내부 메모리 간략화

변수영역
주소 ... 1002 1003 1004 ...
데이터   이름: a
값: @5004
     
데이터 영역 주소 ... 5002 5003 5004 ...
데이터       'abc'  
  • 변수 영역에서 빈 공간 (@1003) 을 확보
  • 확보한 공간의 식별자를 a로 지정
  • 데이터 영역의 빈공간(@5004)에 문자열 'abc'를 지정
  • 변수 영역에서 a라는 식별자를 검색한다(@1003)
  • 앞서 저장한 문자열의 주소(@5004)를 @1003의 공간에 대입한다.

 왜 변수 영역에 직접 값을 대입하지않고 주소를 한번더 대입할까? 자바스크립트는 숫자형 데이터에 64비트의 공간을 확보한다. 그러나 문자열은 특별히 정해진 규칙이 없다. 
 직접 값을 대입하게 된다면, 문자열이 변할때마다, '확보된 공간을 변환된 데이터 크기에 맞게 늘리는작업' 이 필요해진다. 그러나, 메모리 앞뒤로 이미 할당된 값이 있다면?? 굉장히 복잡해질 것이다. 이런 오류를 피하기 위해 자바스크립트는 변수와 데이터를 별도의 공간에 나누어 저장하는 것이다.

  •   a에 'abcdef' 라는 문자열을 재할당 해보자 
변수영역
주소 ... 1002 1003 1004 ...
데이터   이름: a
값: @5005
     
데이터 영역 주소 ... 5002 5003 5004 5005
데이터       'abc' 'abcdef'
  • 위와 같이 'abc'가 할당된 공간을 고치지 않고 새로운 주소에 'abcdef'라는 문자열을 다시 할당한다. @5004의 데이터는 더이상 참조되지 않는다면 gc가 일어날때 수집대상이 될 것이다.

 

4. 기본형 데이터와 참조형 데이터

4.1 불변값

  • 변수와 상수를 구분하는 성질은 '변경 가능성' 이다.
  • 변수와 상수를 구분짓는 변경가능성의 대상은 변수영역의 메모리 이다.
  • 반면 불변성(Immunitablility)를 구분할때의 변경가느성의 대상은 데이터 영역의 메모리이다.
  • number, string, bollean, null, undefined, Symbol은 모두 Immunitable 함.
var a = 'abc';
a = a + 'def';

var b = 5;
var c = 5;
b = 7;

1~2번째 줄을보면 기존 'abc' 에 'def'를 더하지만 'abcdef'라는 새로운 데이터를 만들어 그 주소를 a의 변수에 저장한다.
즉 'abc'와 'abcdef'는 별도의 메모리에 독립적으로 존재하게 된다.

4.2 가변값
 
기본형 데이터는 모두 불변값 이지만, 참조형의 경우 기본적인 성질은 가변값이지만, 설정에 따라 변경 불가능한 경우도 있다.(Object.defineProperty, Object.freeze 등)

// ex 4-1) 참조형 데이터의 할당
var obj1 = {
 a: 1,
 b: 'bbb'
};

 

변수영역
주소 1001 1002 1003 1004 ...
데이터   이름: obj1
값: @5001
     
데이터 영역 주소 5001 5002 5003 5004 5005
데이터 @7103 ~ ?     1 'bbb'

 

객체 @5001의 변수영역
(Property)

주소 7103 7104 7105 7106 ...
데이터 이름: a
값: @5004
이름: b
값: @5005
     
  • 컴퓨터는 메모리의 빈공간 (@1002)에 obj1이름을 저장한다.
  • 임의의 데이터 저장공간 (@5001)에 데이터를 저장하려고보니, 여러개의 프로퍼티를 가진 데이터 그룹이다. 내부의 프로퍼티를 저장하기 위해 변수영역을 마련하고, 그 의 주소 (@7013 ~ ? ) 를 @5001에 저장한다.
  • @7103 및 @7104에 각각 a와 b 프로퍼티 이름을 저장.
  • 데이터영역에서 숫자1과 숫자 'bbb'를 검색 한 뒤, 그의 주소를 각각의 프로퍼티에 저장.

위의 순서로 볼때, 기본형 데이터와 참조형 데이터의 차이는 '객체의 Property 영역' 이 별도로 존재한다는 것이다. Property에는 언제나 다른값이 대입될 수 있으므로, 이는 'not immunitable' 한 값이다.

 

// ex 4-2) 참조형 데이터의 프로퍼티 값 할당
var obj1 = {
 a: 1,
 b: 'bbb'
};

obj1.a = 2;

 

변수영역
주소 1001 1002 1003 1004 ...  
데이터   이름: obj1
값: @5001 (안변함)
       
데이터 영역 주소 5001 5002 5003 5004 5005 5006
데이터 @7103 ~ ?     1 'bbb' 2

 

객체 @5001의 변수영역
(Property)

주소 7001 7002 7003 7004 ...
데이터 이름: a
값: @5006
이름: b
값: @5005
     

 위에서 알 수 있듯이, 참조형 데이터의 property 값을 재할당 할때는 property 변수영역이 가르키는 데이터영역의 주소만 바뀔분, 변수영역의 주소값(@5001)은 바뀌지 않는다.)

 

중첩객체(참조 in 참조) 일 경우 Flow를 살펴보자

// ex 4-3) 중첩된 참조형 객체의 프로퍼티 할당
var obj1 = {
 x: 1,
 arr: [3,4,5]
};
변수영역
주소 1001 1002 1003 1004 ...  
데이터   이름: obj1
값: @5001
       
데이터 영역 주소 5001 5002 5003 5004 5005 5006
데이터 @7103 ~ ? 3 1 @8104 ~ ? 4 5

 

객체 @5001의 변수영역
(Property)

주소 7103 7104 7105 7106 ...
데이터 이름: x
값: @5003
이름: arr
값: @5004
     

 

배열 @5004의 변수영역

주소 8104 8105 8106 8107 ...
데이터 이름: 0
값: @5002
이름:1
값:@5005
이름:2
값:@5006
   
  • 위와 같은 메모리 구조를 가지면, 참조형 변수 obj1의 property x,arr의 변수가 데이터영역의 @5003,@5004를 가리킨다
  • x는 데이터를 바로 가리키므로 1이 저장된 @5003을 가리키나, arr은 배열(참조형) 변수를 가리키므로 데이터영역 내 @5004에는 주소값이 저장되어 있다.
  • 배열의 property 0,1,2를 가리키는 @8104 ~ ? 주소로 가보면 다시 데이터영역의 number형의 주소가 저장되있으며 이를 각각 가리키게된다. 

obj1.arr[0] 을 얻어올경우 다음과 같은 flow로 동작한다

@1002 -> @5001 ->(@7103 ~ ?) -> @5004 -> (@8104~?) -> @5002 -> 3반환

 

굉장히 복잡하지만 이해가 안되는 정도는 아니다.

 

obj.arr = 'str'; 

위와 같이 arr이라는 property에 'str'이 재할당 된다면 어떻게 될까?

변수영역
주소 1001 1002 1003 1004 ...    
데이터   이름: obj1
값: @5001
         
데이터 영역 주소 5001 5002 5003 5004 5005 5006 5007
데이터 @7103 ~ ? 3 1 @8104 ~ ? 4 5 'str'

 

객체 @5001의 변수영역
(Property)

주소 7103 7104 7105 7106 ...
데이터 이름: x
값: @5003
이름: arr
값: @5006
     

 

배열 @5004의 변수영역

주소 8104 8105 8106 8107 ...
데이터 이름: 0
값: @5002
이름:1
값:@5005
이름:2
값:@5006
   

 

위와같이 @7104 주소의 arr변수가 가리키는 값은 데이터 영역의 string으로 변할 것이고, 빨간색으로 써진 기존 arr변수들은 더이상 참조하는 변수가 없으므로 gc 수거대상이 될것이다.

 

5 불변객체

5.1 객체의 가변성 <-> 불변객체

 위와같은 refrence 객체의 가변성때문에, object가 Not Immutable 하게되어 문제가 된다. 객체를 Immunitable 하게 하는것은 React, Vue, Angular 등에서 가장 중요한 기초가 되는 개념이다.
 우선 객체의 가변성에 따른 문제점부터 살펴보자.

// Object가 none Immunitable 하여 생기는 문제
var user = {
 name: 'Jaenam',
 gender: 'male',
};

var changeName = fuction (user, newName) {
 var newUser = user;
 newUser.nam = newName;
 return newName;
}

var user2 = changeName(user, 'Jung');

if(user !== user2) {
 console.log('유저 정보가 변경되었습니다.');
}

console.log(user.name, user2.name); // Jung, Jung
console.log(user === user); //true

 위 예제코드에서 우리가 원하던것과 전혀 다르게 동작한다. 이유는 object type의 특성상 주소값을 복사하게 되서, user2도 기존의 user의 name property를 가리키게 되므로, user의 네임까지 변경시켜버린다.

우리는 아래처럼 deep copy를 통해 immunitable을 보장 할 수있다.

// Object가 none Immunitable 하여 생기는 문제
var user = {
 name: 'Jaenam',
 gender: 'male',
};

var copyObject = function(target) {
 var result = {};
 for(var prop in target) {
  result[prop] = target[prop];
 }
 return result;
}

var user2 = copyObject(user);
user2.name = 'Jung';

if(user !== user2) {
 console.log('유저 정보가 변경되었습니다.');
}

console.log(user.name, user2.name); // Jaenam, Jung
console.log(user === user); //false

 

6 undefined 와 null

  •  자바스크립트에서 없음 을 나타내는 값이 두개가있다
  • undefined와 null 이다.

6.1 undefined

  • undefined는 사용자가 지정할 수 도있지만, JS엔진이 자동부여 할 때 도있다.
  • 엔진이 자동으로 부여하는 경우는 다음과 같다
    1. 값을 대입하지 않은 변수, 즉 데이터 영역의 메모리 주소를 지정하지 않은 식별자에 접근
    2. 객체 내부의 존재하지 않는 프로퍼티에 접근하려할때
    3. return 문이 없거나 호출되지 않는 함수의 실행결과
var a;
console.log(a); //(1) undefined 값을 대입하지 않은 변수에 접근

var obj = { a:1 };
console.log(obj.b); // (2) 존재하지 않는 props 에 접근

var func = function() {};
var c = func();
console.log(c); //(3) 명시적인 반환값이 없는 함수에 반환값

 

  • 주의 할점은 '비어있는 요소' 와 'undefined를 할당한 요소'는 출력결과가 다른걸 유의 해야한다.
var arr1 = [undefined, 1];
var arr2 = [];
arr[2] = 1;

arr1.forEach(function (v,i) {console.log(v,i)}); // undefined 0 / 1 1
arr2.forEach(function (v,i) {console.log(v,i)}); // 1 1

// 엔진에 의해 할당된 undefined 같은경우, 아예 없는 값으로 취급이된다. 메서드의 순환 대상에서 제외 된다는 것이다.
  • 값이 지정되지 않은 인덱스는 '아직은 존재하지 않는 프로퍼티'에 지나지 않는다.
  • 사용자가 지정한 undefined는 그 자체로의 값이다. (순회시 하나의 값으로 취급)
  • 가장 중요한것은 undefined를 직접 할당하지 않도록 주의해야한다. 
  • 비어있음을 명시적으로 표현하고싶다면 null을 삽입하도록 하자.
  • 이렇다면 undefined는 오직 '값을 대입하지 않은 변수에 접근하고자 할때 자바스크립트 엔진이 반환해주는 값' 으로만 존재할것이다

 

var n = null;
console.log(typeof n); //object (주의) 이것은 자바스크립트의 근본적 버그이다.

console.log(n == undefined); // true
console.log(n == null); // true

console.log(n === undefined); // false
console.log(n === null); // true

 

 

* 위 내용은 책 <Core javascript> - 정재남 저 - 를 개인공부를 위해 정리한 포스트입니다.

List

 

Add

Remove

Get

Contains

Data  Structure

ArrayList

 O(1)

O(n)

O(1)

O(n)

Array

LinkedList

 O(1)

O(1)

O(n)

O(n)

Linked List

CopyonWriteArrayList

 O(n)

O(n)

O(1)

O(n)

Array

Stack

O(1)
(PUSH)

O(1)
(POP)

O(1)
(Peek)

O(n)
(Search)

Vector 기반

 

Set

 

Add

Contains

Next

Data Structure

HashSet

O(1)

O(1)

O(h/n)

Hash Table

LinkedHashSet

O(1)

O(1)

O(1)

Hash Table + Linked List

EnumSet

O(1)

O(1)

O(1)

Bit Vector

TreeSet

O(log n)

O(log n)

O(log n)

Red-black tree

CopyonWriteArraySet

O(n)

O(n)

O(1)

Array

ConcurrentSkipList

O(log n)

O(log n)

O(1)

Skip List

 

Queue

 

Offer

Peak

Poll

Size

Data Structure

PriorityQueue

O(log n )

O(1)

O(log n)

O(1)

Priority Heap

LinkedList

 O(1)

O(1)

O(1)

O(1)

Array

ArrayDequeue

 O(1)

O(1)

O(1)

O(1)

Linked List

ConcurrentLinkedQueue

 O(1)

O(1)

O(1)

O(n)

Linked List

ArrayBlockingQueue

 O(1)

O(1)

O(1)

O(1)

Array

PriorirityBlockingQueue

O(log n)

O(1)

O(log n)

O(1)

Priority Heap

SynchronousQueue

O(1)

O(1)

O(1)

O(1)

None!

DelayQueue

O(log n)

O(1)

O(log n)

O(1)

Priority Heap

LinkedBlockingQueue

O(1)

O(1)

O(1)

O(1)

Linked List

 

Map

 

Get

ContainsKey

Next

Data Structure

HashMap

O(1)

O(1)

O(h / n)

Hash Table

LinkedHashMap

O(1)

O(1)

O(1)

Hash Table + Linked List

IdentityHashMap

O(1)

O(1)

O(h / n)

Array

WeakHashMap

O(1)

O(1)

O(h / n)

Hash Table

EnumMap

O(1)

O(1)

O(1)

Array

TreeMap

O(log n)

O(log n)

O(log n)

Red-black tree

ConcurrentHashMap

O(1)

O(1)

O(h / n)

Hash Tables

ConcurrentSkipListMap

O(log n)

O(log n)

O(1)

Skip List

 

 

정렬 알고리즘 비교

정렬 이름 공간복잡도 시간복잡도
최악 최선 평균 최악
Bubble Sort O(1) O(n) O(n2) O(n2)
Heapsort O(1) O(n log n) O(n log n) O(n log n)
Insertion Sort O(1) O(n) O(n2) O(n2)
Mergesort O(n) O(n log n) O(n log n) O(n log n)
Quicksort O(log n) O(n log n) O(n log n) O(n2)
Selection Sort O(1) O(n2) O(n2) O(n2)
Shell Sort O(1) O(n) O(n log n2) O(n log n2)
Smooth Sort O(1) O(n) O(n log n) O(n log n)

 

자료구조별 비교

 

자료구조이름 평균 경우 최악의경우
Search Insert Delete Search Insert Delete
Array O(n) N/A N/A O(n) N/A N/A
ArrayList O(log n) O(n) O(n) O(log n) O(n) O(n)
Linked List O(n) O(1) O(1) O(n) O(1) O(1)
Doubly Linked List O(n) O(1) O(1) O(n) O(1) O(1)
Stack O(n) O(1) O(1) O(n) O(1) O(1)
Hash table O(1) O(1) O(1) O(n) O(n) O(n)
Binary Search Tree O(log n) O(log n) O(log n) O(n) O(n) O(n)
B-Tree O(log n) O(log n) O(log n) O(log n) O(log n) O(log n)
Red-Black tree O(log n) O(log n) O(log n) O(log n) O(log n) O(log n)
AVL Tree O(log n) O(log n) O(log n) O(log n) O(log n) O(log n)

 

 

참고 Big O 그래프

 

public class ExComparator {
	public static void main(String[] args) {
		Pair[] arr = new Pair[10];
		List<Pair> list = new ArrayList<Pair>();
		
		for(int i=0; i<10; i++) {
			int a = (int)(Math.random()*10);
			int b = (int)(Math.random()*10);
			arr[i] = new Pair(a, a+b);
			list.add(arr[i]);
		}
		
		// 정석 Comparator 객체사용 (With array)
		// 오른쪽객체가 왼쪽으로올경우 오름차순
		// 반대는 내림차순
		Arrays.sort(arr, new Comparator<Pair>() {
			@Override
			public int compare(Pair o1, Pair o2) {
				return o2.min - o1.min;
			}
		});
		
		System.out.println("원본");
		System.out.println(list.toString());
		System.out.println();
		
		//2가지 항목비교
		Collections.sort(list, new Comparator<Pair>() {
			public int compare(Pair o1, Pair o2) {
				int first = o2.min - o1.min;
				int second = o2.max - o1.max;
				return first != 0? first : second;
			}
		});
		System.out.println("2개항목비교");
		System.out.println(list.toString());
		System.out.println();
		
		//람다사용 With list (Collections)
		Collections.sort(list, (o1, o2) -> o1.min - o2.min);
		System.out.println("람다로 한개만비교");
		System.out.println(list.toString());
		System.out.println();
		
		
		//Stream 사용
		List<?> sorted = list.stream()
			.sorted((o1,o2) -> o2.min - o1.min)
			.collect(Collectors.toList());
		System.out.println("Stream 사용");
		System.out.println(sorted.toString());
		System.out.println();
		
	}
}

class Pair {
	int min;
	int max;
	
	Pair(int min, int max) {
		this.min = min;
		this.max = max;
	}
	
	@Override
	public String toString() {
		return String.format("[%d, %d]", min,max);
	}
}

문제제기

- 우리가 1장에서 만들었던 게임에 대해 다시 말해보자, 1장에서 우리는 Strategy Pattern을 써서, Class 별로 무기를 사용하는 행위(Attack)을 구현했엇다.

- Attack Strategy를 구현한 코드가 아래처럼 되어있다고 가정해보자,

public class AxeAttack implements AttackBehavior{
	String[] available = {"knight","mknight"};
	
	@Override
	public void attack(String className) throws Exception {
		if(Arrays.stream(available).anyMatch(className::equals)) {
			System.out.println("도끼 공격이다!!!");
		} else {
			throw new Exception("your name is " + className);
		}
	}
}

 

- 위 코드처럼 knight 클래스와 Macgic knight (마검사) 만 axe를 쓸 수 있도록 코딩되어있다.

public class Wizard extends Person{

	public Wizard(AttackBehavior attack) {
		this.className = "wizard";
		this.attackBehav = attack;
	}
}

- Wizard 클래스가 Axe를 쓰려고 하면 어떻게 될까?

 

 

- 위와같이 오류가 날 것이다. 그러면 어떻게 이 상황을 해결할 수 있을까?

 

어댑터 패턴 개요

- 위 문제를 Adapter 패턴을 통해 우리는 해결해 볼 수 있다.

 

-일단 어댑터 패턴이 무엇인지부터 살펴보면, 말그대로 중간에 어댑터(돼지코)를 둬서, 원래의 Object를 유럽플러그에서도 사용 할 수있게 만드는 것이다.

 

조금더 복잡한 만능리모컨 설계

 -우리가 6-1에서 만들었던 간단한 만능리모컨 프로토타입이 마음에 들었는지, 이번에는 업체에서 좀더 복잡한 만능리모컨 을 설꼐해달라고 부탁했다.

- 위와같이 3쌍의 On/Off 버튼이 있는 만능리모컨을 만든다고 가정해보자,

- 우리는 먼저 테스트용도로 이 리모컨에 GarageDoor, CDplayer, Light 를 제어해보기로 했다.

 

만능 리모컨을 테스트하게 될 3가지 Receiver들

 

 

- 가장먼저 Inovoker에 해당하는 리모컨부터 객체로 디자인해보자.

public class RemoteControl {
	Command[] onCommands;
	Command[] offCommands;
	Command undoCommand;

	public RemoteControl() {
		onCommands = new Command[6];
		offCommands = new Command[6];
		
		Command noCommand = new NoCommand();
		for (int i = 0; i < 6; i++) {
			onCommands[i] = noCommand;
			offCommands[i] = noCommand;
		}
		undoCommand = noCommand;
	}
	
	public void setCommand(int slot, Command onCommand, Command offCommand) {
		onCommands[slot] = onCommand;
		offCommands[slot] = offCommand;
	}
	
	public void onButtonPushed(int slot) {
		onCommands[slot].execute();
		undoCommand = onCommands[slot];
	}
	
	public void offButtonPushed(int slot) {
		offCommands[slot].execute();
		undoCommand = onCommands[slot];
	}
	
	public void undoButtonPushed() {
		undoCommand.undo();
	}
	
	@Override
	public String toString() {
		StringBuffer st = new StringBuffer();
		st.append("\n-------- Remote Control-------------\n");
		for (int i = 0; i < onCommands.length; i++) {
			st.append("[slot"+ i + "\n"+ onCommands[i].getClass().getCanonicalName()+ " "
					+ "  "+ offCommands[i].getClass().getName()+"\n");
		}
		
		return st.toString();
	}
}

 

- 위와같이 On, Off Command를 6개씩 넣을 수 있는 배열을 내부변수로 가지고있고, 또한 undo를 할 수있는 command를 저장하였다.

- 또한 상태 확인을 위해 toString 객체 또한 Override 하여 만들어 주었다.

 

 

-이제 다음으로 Invoker에서 트리거 되는 command 객체를 만들어보자.

 

 

- 위와 같이 Receiver에 행동 하나하나가 그에 대응되는 Command 객체로 만들어진다.

- 많은 커맨드가 있지만 위 Command중 로직이 가장 복잡한 Cdplayer Command 만 만들어보자.

public class CdPlayerOnCommand  implements Command{
	CdPlayerReceiver cdPlayer;
	
	public CdPlayerOnCommand(CdPlayerReceiver cdPlayer) {
		this.cdPlayer = cdPlayer;
	}

	@Override
	public void execute() {
		cdPlayer.on();
		cdPlayer.setCd();
		cdPlayer.setVolume(11);
	}
}

 

public class CdPlayerOffCommand  implements Command{
	CdPlayerReceiver cdPlayer;
	
	public CdPlayerOffCommand(CdPlayerReceiver cdPlayer) {
		this.cdPlayer = cdPlayer;
	}

	@Override
	public void execute() {
		cdPlayer.off();
	}
}

 

- 위와같이 command내부 execute command 안에는 여러개의 Receiver에 로직이 들어갈 수 있다. (원하는 행동을 구동시키기 위하여)

- 다른 Command들은 충분히 쉬우니 생략하도록 하겠다.

 

만능리모컨 사용해보기

 

 

- 위와 같은 클래스다이어그램의 만능리모컨이 완성 되었다. 한번 사용해보도록하자

 

public class RemoteLoader {

	public static void main(String[] args) {
		RemoteControl remoteControl = new RemoteControl();
		
		LightReceiver lightReceiver = new LightReceiver();
		GarageDoorReceiver garageDoorReceiver = new GarageDoorReceiver();
		CdPlayerReceiver cdPlayerReceiver = new CdPlayerReceiver();
		
		LightonCommand lightonCommand = new LightonCommand(lightReceiver);
		LightOffCommand ligOffCommand = new LightOffCommand(lightReceiver);
		
		GrageDoorDownCommand garDoorDownCommand = new GrageDoorDownCommand(garageDoorReceiver);
		GarageDoorUpCommand garageDoorUpCommand = new GarageDoorUpCommand(garageDoorReceiver);
		
		CdPlayerOffCommand cdPlayerOffCommand = new CdPlayerOffCommand(cdPlayerReceiver);
		CdPlayerOnCommand cdPlayerOnCommand = new CdPlayerOnCommand(cdPlayerReceiver);
		
		remoteControl.setCommand(0, cdPlayerOnCommand, cdPlayerOffCommand);
		remoteControl.setCommand(1, garDoorDownCommand, garageDoorUpCommand);
		remoteControl.setCommand(2, lightonCommand, ligOffCommand);
		
		remoteControl.onButtonPushed(0);
		remoteControl.offButtonPushed(0);
		
		remoteControl.onButtonPushed(1);
		remoteControl.offButtonPushed(1);
		
		remoteControl.onButtonPushed(2);
		remoteControl.offButtonPushed(2);
		
		remoteControl.onButtonPushed(3);
		remoteControl.offButtonPushed(3);
		
	}

}

 

 

 

+ Undo 기능 만들기.

Undo 기능은 만들기 참 쉽다. Command 내의 On에 반대되는 기능만 추가해주면 된다.

 

1) 다음과같이 Command interface에 Undo method만 추가해준다.

 

2) Cdplayer command 에서 예시로 구현해 보겠다. OnCommand에 반대되게Undo를 작성해준다.

public class CdPlayerOnCommand  implements Command{
	CdPlayerReceiver cdPlayer;
	
	public CdPlayerOnCommand(CdPlayerReceiver cdPlayer) {
		this.cdPlayer = cdPlayer;
	}

	//...omit...
    
    
	@Override
	public void undo() {
		cdPlayer.off();
	}

}

 

3) Invoker 객체에서 Undo를 포함하는 객체를 다음과같이 저장할수 있게한뒤(6개나 필요없다.) 쓸수있는 Method를 정의해준다.

public class RemoteControl {
	Command[] onCommands;
	Command[] offCommands;
	Command undoCommand;

	public RemoteControl() {
		onCommands = new Command[7];
		offCommands = new Command[7];
		
		Command noCommand = new NoCommand();
		for (int i = 0; i < 7; i++) {
			onCommands[i] = noCommand;
			offCommands[i] = noCommand;
		}
		undoCommand = noCommand;
	}
	
    public void onButtonPushed(int slot) {
		onCommands[slot].execute();
		undoCommand = onCommands[slot];
	}
	
	public void offButtonPushed(int slot) {
		offCommands[slot].execute();
		undoCommand = onCommands[slot];
	}

	// ... omit ...
    
    
	public void undoButtonPushed() {
		undoCommand.undo();
	}
}

 

- 차이가 보이는가? 커맨드가 실행될때마다 슬롯을 UndoCommand에 저장시켜서 언제나 Undo method를 실행할 수 있게준비한다.

 

4) 메인에서 사용방법이다.

public class RemoteLoader {

	public static void main(String[] args) {
		RemoteControl remoteControl = new RemoteControl();
		
		GarageDoorReceiver garageDoorReceiver = new GarageDoorReceiver();

		CdPlayerOffCommand cdPlayerOffCommand = new CdPlayerOffCommand(cdPlayerReceiver);
		CdPlayerOnCommand cdPlayerOnCommand = new CdPlayerOnCommand(cdPlayerReceiver);
		
		
		remoteControl.onButtonPushed(0);
		remoteControl.undoButtonPushed();
		remoteControl.offButtonPushed(0);
		
		// ... omit ....
		
	}

}

 

- 위와같이 사용할 수 있다.

'To be Developer > DesignPatterns' 카테고리의 다른 글

6-1.Command Pattern (Simple)  (0) 2020.01.10
5.Singleton Pattern  (0) 2020.01.07
4-2 Factory Pattern (Abstract Factory Pattern)  (0) 2020.01.05
4-1.Factory Pattern (Factory Method Pattern)  (0) 2020.01.04
3.Decorator Pattern  (0) 2020.01.02

문제 제기

만능 리모컨 회사에서 여러 물품에 블루투스 리모컨 시스템을 사용할 수 있는 환경을 만들어달라고 의뢰를 해왔다.
의뢰를 들어온 물품 Object 들의 시그니쳐는 다음과 같이 제공되었다.

물론 더 많겠지만 일단 4개만 해보자.

 

- 공통된 Method가 존재하기도 하는데 대부분 겹치지않는다. 공통된 행위가 없어서 Starategy 패턴으로 뺄수도 없을것 같고 이럴땐 어떻게 해야할까?

 

커맨드 패턴 살펴보기

- 가장 먼저 커맨드 패턴의 클래스 다이어그램부터 살펴보자

 

- 클래스 다이어그램에는 가장 핵심적인 요소 4가지가 있다. Client, Invoker, Command, Receiver 이다.

- 대소 관계로 비교하기 좀그렇지만 Client는 {Invoker, Command, Receiver}를 다 가지고 있다.
그리고 클라이언트의 요청에따라 Invoker > Command > Receiver 순서로 트리거 된다. 그러나 그전에 클라이언트에서 해주어야 할 작업이 있다. 이 작업을 순서대로 풀어 써 보자면

1) Client는 가장 먼저 Receiver를 생성 후 Command에 넘겨 Command를 생성해준다.
2) 다음으로 클라이언트는 CommandInvoker에게 Set 해주고 Invoker를 생성해준다
3) client는 필요할때 Invoker에게 invoke()를 요청한다.
4) 이렇게 되면 Invoker.Invoke() -> Command.execute() -> receiver.action() 순으로 실행된다.

 


이를 그림으로 표현 한 것이다.

 

간단한 리모컨에 커맨드 패턴 끼얹기

- 자 위와 같이 간단한 On/Off 버튼이 있다고 먼저 가정해보자.

 

- 웜업으로 아까 받은 여러개의 클래스 다이어그램에서 Light의 On()과 Garage의 Up()만 구현해보자.

1) 각각 Receiver를 구현해보자.

public class GarageDoorReceiver {
	
	public void up() {
		System.out.println("garage up");
	}
	
	public void down() {
		
	}
	
	public void stop() {
		// TODO Auto-generated method stub

	}	
	
	public void lightOn() {
		
	}
	
	public void lightOff() {
		
	}
}
public class GarageDoorReceiver {
	
	public void up() {
		System.out.println("garage up");
	}
	
	public void down() {
		
	}
	
	public void stop() {
		// TODO Auto-generated method stub

	}	
	
	public void lightOn() {
		
	}
	
	public void lightOff() {
		
	}
}

 

 

2) Command Interface를 만들고, Command 구현체를 각각 만들어주자

public interface Command {
	public void execudte();
}
public class LightonCommand implements Command{
	LightReceiver light;
	
	public LightonCommand(LightReceiver light) {
		this.light = light;
	}
	
	@Override
	public void execudte() {
		light.on();
	}

}
public class GarageCommand implements Command{
	GarageDoorReceiver gar;
	
	public GarageCommand(GarageDoorReceiver gar) {
		this.gar = gar;
	}
	
	@Override
	public void execudte() {
		gar.up();
	}

}

 

 

3) 위 두가지 Command를 실행해줄 SimpleRemotecontrol Invoker 를 만들어보자

public class SimpleRemotecontrol {
	Command com;
	
	public SimpleRemotecontrol() {
		// TODO Auto-generated constructor stub
	}
	
	public void setCom(Command com) {
		this.com = com;
	}
	
	public void btnPressed() {
		com.execudte();
	}
	
}

 

4) 한번 Client (메인함수) 에서 실행시키고 결과를 보자.

public class RemoteControlTest {

	public static void main(String[] args) {
		SimpleRemotecontrol remote = new SimpleRemotecontrol();
		LightReceiver light = new LightReceiver();
		LightonCommand lightOnCommand = new LightonCommand(light);
		
		remote.setCom(lightOnCommand);
		remote.btnPressed();
		
		GarageDoorReceiver ga = new GarageDoorReceiver();
		GarageCommand garageCommand = new GarageCommand(ga);
		
		remote.setCom(garageCommand);
		remote.btnPressed();
	}

}

 

하나의 인보커로 두가지 커맨드가 잘 실행 되었다.!

 

- 간단하게 커맨드 패턴을 활용하여 두가지 Light, Garage Receiver를 구현해보았다.

 

- 위 소스의 클래스 다이어그램은 아래와 같이 그릴 수 있다.

 

커맨드 패턴의 정의

- 커맨트 패턴의 공식적인 정의는 다음과 같다.

커맨드 패턴
커맨드 패턴을 이용하면 요구사항을 객체로 캡슐화 할 수 있으며, 매개변수를 써서 여러가지 다른 요구사항을 집어넣을수도 있다. 또한 요청내역을 큐에저장하거나, 로그로 기록할 수도 있으며, 작업취소 기능도 지원합니다.

 

-간단한 커맨드 패턴에 작업취소등 추가적인 작업은 다음시간에 해보겠다.

- 결국 커맨드 패턴이란, 일련의 행동을 특정 리시버로 연결시켜 요구사항을 커맨드로 캡슐화 한것이다. 외부에서 볼때는 어떤 리시버가 실행되는지, 어떤 객체가 리시버역할을하는지, 그 리시버에서 실제로 어떤 수행을 하는지 알 수 없다. 그냥 execute()가 실행되면 리시버가 실행된다는 것만 알 수 있을뿐이다. 또한 우리는 Receiver를 Command객체로 구현하므로써, 매개변수로써 여러가지 다른 리시버들을 실행 시켰다.

Skeleton Screen ?

최근 웹페이지를 들어가면 로딩중에 저런 화면을 많이 본적이 있을 것이다.

유투브, 페이스북에서도 로딩중에 많이 사용 하는 화면인데 일명 "Skeleton Screen" 이라고 부른다.

 

CSS linear-gradient를 통한 Skeleton Screen

linear-gradient() CSS 함수는 두 개 이상의 색이 직선을 따라 점진적으로 변화하는 이미지를 생성한다. 함수의 결과는 <image>의 특별한 종류인 <gradient>자료형이다.

<gradient> <image>의 한 종류로서 <image>를 사용하는 곳에만 적용할 수 있다. 따라서 linear-gradient()를 background-color  <color> 자료형을 받는 속성에는 사용할 수 없다.

ex1) 사용법 linear-gradient([driection], #color1, #color2) (2개 이상의 색상은필수이다.) (driection 에는 top, left, right deg 등 다양한 인자를 사용 가능하다.)

.example-element {
  width:300;
  height:400;
  background: linear-gradient(0, blue, black);
}

 

정 중앙을 기준으로 두개의 색이 그라데이션됨

 

- direction 부분에 degree를 줘서 각도만큼 비틀수도 있다. ingredient는 축을기준으로 무수한 선을 긋는 개념이기때문에, 각도를 준다면 다음처럼 축을 기준으로 휘어진 선이 휘게된다.

 

ex2) 축을 45도만큼 휘게 사용

.example-element {
  width:300;
  height:400;
  background: linear-gradient(45deg, blue, black);
}

정사각형이 아니므로 정확한 대각선은 아닐수도..

 

CSS linear-gradient 을 활용하여 게시글을 표현해보자.

ex3) linear-gradient 를 이용해서 글 한줄 한줄을 표현해보자.

.example-element {
  width:300;
  height:400;

  background-image:
    linear-gradient(lightgrey 15px, transparent 0),
    linear-gradient(lightgrey 15px, transparent 0),
    linear-gradient(lightgrey 15px, transparent 0);
}

 

- 오잉? 근데 3분명히 3줄을 추가했는데 한줄만 나온다? 왜그럴까??? -> 현재 3줄의 좌표가 겹쳐있기때문이다.

background-position: [x 위치, y 위치] 를 추가해서 각각 ingredient의 좌표를 달리해보자.

 

.example-element {
  width:300;
  height:400;

  background-image:
    linear-gradient(lightgrey 15px, transparent 0),
    linear-gradient(lightgrey 15px, transparent 0),
    linear-gradient(lightgrey 15px, transparent 0);
  
  background-position:
  /* 좌측에서 5px만큼 y축으로 5px(선굵기-간격[15px-20px])씩 띄움 */
    5px 10px,
    5px 30px,
    5px 50px;
  background-size:
  /* gradient 기준으로 width height 만한 사각형을 만든다고 생각 하자*/
    100px 100px,
    150px 100px, 
    150px 100px;  
  background-repeat: repeat-y;
}

- 또한 나는 background-size 라는 속성을 주었는데, 사용법은 width 값과 height 값을주면된다. 이 값은 gradient 자체의 width와 height가 아닌 gradient 부터 시작하는 사각형을 준다고 생각하면 된다. 이를 명시하는 이유는 background-repeat: repeat-y; 라는 속성을 주면 y축으로 gradient를 계속 반복할 수있다.

 

width heigth 값은 gradient 선의 속성이 아님을 유의하자

 

 

- 이제 제법 그럴싸하지않은가?? 뭔가좀 허전하니 마지막으로, 애니메이션만 추가해보자.

See the Pen example by EdgarHan (@EdgarHan) on CodePen.

 

 

 

 

 

 

출처

https://developer.mozilla.org/ko/docs/Web/CSS/linear-gradient

https://wit.nts-corp.com/2018/11/19/5371

 

+ Recent posts