본문 바로가기

JavaScript, HTML, CSS

[JS, 코드 스테이츠]05. Closure, Parameter, new 키워드와 객체지향 JavaScript

코드 스테이츠 Pre 코스 다섯째 주 수강 후기.

 이번 주는 새로운 개념을 많이 배워서 쓸 게 엄청 많다. 이걸 쓰는 데 걸리는 시간도 길어지겠지. ㅎ..

 클로저 부분에서 특히 더 오래 걸렸다. 신기하긴 한데 이걸 왜 써야 하는지 이해가 잘 되지 않아서 이것저것 찾아봤다. 결국 이해를 하긴 했는데 정확하게 이해한 것이 맞는지에 대한 확신이 없다. 어렵다 어려워.

 


목차
1. 클로저 Closure
2. 매개변수 Parameter
3. new키워드와 객체지향 JavaScript

 

1. Closure

Lexical Scoping(어휘적 범위 지정)

이 부분은 자세히 아는 것이 아니기에 추후에 더 공부해서 추가할 예정

function init() {
  let value = "Mozilla"; // value는 init에 의해 생성된 지역 변수.
  function displayName() { // displayName() 은 내부 함수.
    console.log(value); // 부모 함수에서 선언된 변수를 사용.
  }
  displayName();
}
init();

 위 코드를 실행하면 displayName() 함수 내의 console문이 부모 함수에서 정의한 변수 value의 값을 성공적으로 출력한다(displayName() 함수는 자신 바깥의 스코프에 선언되어 있는 변수 value에 접근할 수 있다). 이는 lexical scoping의 한 예이다.

 lexical scoping(어휘적 범위 지정)을 할 때에는, 변수를 어느 범위까지 사용할 수 있는지 결정하기 위해 소스 코드 내에서 변수가 선언된 위치정보를 사용한다.


클로저(Closure)

function makeFunc() {
  var value = "Mozilla";
  function displayName() {
    console.log(value);
  }
  return displayName;
}

let myFunc = makeFunc();
//myFunc변수에 displayName을 리턴
//유효범위의 Lexical environment을 유지
myFunc(); //리턴된 displayName 함수를 실행(value 변수에 접근)
console.log(myFunc.value); //undefined
//이미 실행이 끝난 함수 내부의 변수는 클로저에서만 접근 가능

 이 코드는 바로 전의 예제와 동일한 결과가 실행된다. 하지만 이 코드는 displayName() 함수가 실행되기 전에 외부 함수인 makeFunc()로부터 리턴되어 myFunc 변수에 저장된다.

 몇몇 프로그래밍 언어에서, 함수 안의 지역 변수들은 그 함수가 처리되는 동안에만 존재한다(C언어 같은). 그렇기에 makeFunc() 실행이 끝나면 value 변수에 더 이상 접근할 수 없게 될 것으로 예상했다.

 하지만 자바스크립트는 함수를 리턴하고, 리턴하는 함수가 클로저를 형성하기 때문에 그렇지 않다. 클로저는 함수와 함수가 선언된 lexical(어휘적) 환경의 조합이다. 이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성된다. myFunc makeFunc이 실행될 때 생성된 displayName 함수의 인스턴스에 대한 참조다. displayName의 인스턴스는 변수 value가 있는 lexical 환경에 대한 참조를 유지한다. 이런 이유로 myFunc가 호출될 때 변수 value는 사용할 수 있는 상태로 남게 되고 "Mozilla"가 console에 전달된다.

 하지만 lexical 환경에 대한 참조를 유지하는 것이기 때문에 makeFunc()의 실행이 끝나면 value는 외부에서 접근할 수 없게 된다. 이 코드에서 value에 대한 접근은 오직 클로저로만 할 수 있다.

 

 클로저는 lexical 환경을 저장하기 때문에, 이를 사용한다면 for loop 안쪽에서 callback이 실행될 때 발생할 수 있는 문제를 해결할 수 있다.


클로저의 사용

 클로저는 어떤 데이터(lexical environment)와 그 데이터를 조작하는 함수를 연관시켜주기 때문에 그것을 이용할 수 있다. 이것은 객체가 어떤 데이터와(그 객체의 속성) 하나 혹은 그 이상의 메서드들을 연관시킨다는 점에서 객체지향 프로그래밍과 맥락이 같다.

 결론적으로 오직 하나의 메서드를 가지고 있는 객체를 일반적으로 사용하는 모든 곳에 클로저를 사용할 수 있다.

function makeAdder(x) {
  let y = 1;
  return function(z) {
    y = 100;
    return x + y + z;
  };
}

let add5 = makeAdder(5);
let add10 = makeAdder(10);
//클로저에 x와 y의 환경이 저장됨

console.log(add5(2));  // 107 (x:5 + y:100 + z:2)
console.log(add10(2)); // 112 (x:10 + y:100 + z:2)
//함수 실행 시 클로저에 저장된 x, y값에 접근하여 값을 계산

 단일 인자 x를 받아서 새 함수를 반환하는 함수 makeAdder(x)를 정의했다. 반환되는 함수는 단일 인자 z를 받아서 x와 y와 z의 합을 반환한다.

 makeAdder는 함수를 만들어낼 수 있다. makeAdder함수가 특정한 값을 인자로 가질 수 있는 함수들을 리턴하기 때문이다. 위의 예제에서 add5, add10 두 개의 새로운 함수들을 만들기 위해 makeAdder를 사용했다. 하나는 매개변수 z에 5를 더하고 다른 하나는 매개변수 z에 10을 더한다.

 add5와 add10은 둘 다 클로저이다. 이들은 같은 함수 정의를 공유하지만 서로 다른 lexical 환경을 저장한다. 함수 실행 시 add5의 lexical 환경에서 클로저 내부의 x는 5이지만 lexical 환경에서 x는 10이다. 또한 리턴되는 함수에서 초기값이 1로 할당된 y에 접근하여 y값을 100으로 변경한 것을 볼 수 있다. 이는 클로저가 리턴된 후에도 클로저 내부에서 외부 함수의 변수들에 접근 가능하다는 것을 보여주며, 변수가 단순히 값 형태로 전달되는 것이 아니라는 것을 의미한다.


클로저로 프라이빗 메서드(private method) 흉내내기

 자바와 같은 몇몇 언어들은 메서드를 프라이빗으로 선언할 수 있는 기능을 제공한다. (프라이빗 메서드: 같은 클래스 내부의 다른 메서드에서만 프라이빗 메서드들을 호출할 수 있다.)

 자바스크립트는 프라이빗 메서드를 제공하지 않지만 클로저를 이용하여 프라이빗 메서드를 흉내 내는 것이 가능하다. 프라이빗 메서드는 코드에 제한적인 접근만을 허용한다는 점뿐만 아니라 전역 네임 스페이스를 관리하는 방법을 제공하여 불필요한 메서드가 공용 인터페이스를 혼란스럽게 만들지 않도록 한다.

아래 코드는 프라이빗 함수와 변수에 접근하는 퍼블릭 함수를 정의하기 위해 클로저를 사용하는 방법을 보여준다. 이렇게 클로저를 사용하는 것을 모듈 패턴이라 한다.

let makeCounter = function() {
  let privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }  
};

let counter1 = makeCounter();
let counter2 = makeCounter();
console.log(counter1.value()); /* 0 */
counter1.increment();
counter1.increment();
console.log(counter1.value()); /* 2 */
counter1.decrement();
console.log(counter1.value()); /* 1 */
console.log(counter2.value()); /* 0 */
console.log(counter1.privateCounter); /* undefined */
console.log(counter1.changeBy); /* undefined */

 이 코드는 counter.increment, counter.decrement, counter.value 세 함수에 의해 공유되는 하나의 lexical 환경을 만든다. 이 lexical 환경은 정의되는 즉시 실행되는 익명 함수 안에서 만들어진다. 이 lexical 환경은 두 개의 프라이빗 요소를 포함한다. 하나는 변수 privateCounter이고 나머지 하나는 함수 changeBy이다. 둘 다 익명 함수 외부에서 접근할 수 없다. 대신에 반환된 세 개의 퍼블릭 함수를 통해서 접근할 수 있다.

 위의 세 가지 퍼블릭 함수는 같은 환경을 공유하는 클로저다. 자바스크립트의 lexical 유효 범위 덕분에 세 함수는 privateCounter 변수와 changeBy 함수에 접근할 수 있다.

 

아래는 클로저 이해에 도움이 된 포스팅들이다.

[생활코딩] 클로저
[TOAST Meetup] 자바스크립트의 스코프와 클로저
[TOAST Meetup] 클로저, 그리고 캡슐화와 은닉화
[MDN] 클로저
 내용 상 MDN의 클로저만 이해해도 괜찮을 것 같은데 처음부터 MDN을 읽으면 무슨 말인지 하나도 모르겠음. 위의 포스팅들을 한 번 쭉 읽어보고 보는 것을 추천.

2. Parameter

매개변수(parameters)

function timeToGoHome(speed, distance) {
  //speed, distance: 매개변수(parameters)
  let time = distance / speed;
  return time;
}
timeToGoHome(20, 100); //20, 100: 전달인자(arguments)

 

매개변수에 기본값 넣기

 ES6  Default Parameter를 할당. String, Number, Object 등 모든 타입 가능.

function timeToGoHome(speed, distance = 100) {
  let time = distance / speed;
  return time;
}
console.log(timeToGoHome(20)); //5

function timeToGoHome(speed = 20, distance) {
  let time = distance / speed;
  return time;
}
console.log(timeToGoHome(undefined, 1000)); //50

function timeToGoHome(speed = 20, distance = 100) {
  let time = distance / speed;
  return time;
}
console.log(timeToGoHome()); //5

전달인자(arguments)의 길이가 유동적일 경우

 ES6  Rest Parameter를 이용해 매개변수를 지정해주면 매개변수가 배열의 형태로 전달됨.

function getMaxNum(...nums) {
  console.log(nums); // [3, 5, 8, 10]
  return nums.reduce(function(acc, val) {
    if(acc > val){
      return acc;
    }
    return val;
  });
}
getMaxNum(3, 5, 8, 10); //10

 

 ES5  arguments키워드 이용

function getMaxNum() {
  console.log(arguments); // {0:3, 1:5, 2:8, 3:10}
  console.log(arguments[0]); //3
  console.log(arguments[1]); //5
  console.log(arguments[2]); //8
  console.log(arguments[3]); //10
  console.log(arguments.forEach); //undefined
  //arguments객체는 배열처럼 보이지만 배열이 아님(유사배열)
  //배열의 method를 사용할 수 없음
}
getMaxNum(3, 5, 8, 10);

 


3. new 키워드와 객체지향 JavaScript

객체 지향 프로그래밍

 하나의 모델이 되는 class를 만들고, 그 모델(class)을 바탕으로 하나의 instance를 만드는 프로그래밍 패턴.

 ES5  클래스는 함수로 정의할 수 있음

function Car(brand, name, color) {
  //인스턴스가 만들어질 때 실행되는 코드
}

 

 ES6  class키워드를 이용해서 정의

class Car {
  constructor(brand, name, color) {
    //인스턴스가 만들어질 때 실행되는 코드
  }
}

new 키워드를 통해 클래스의 인스턴스를 만들어낼 수 있음

let avante = new Car('hyundar', 'avante', 'black');
let mini = new Car('bmw', 'mini', 'white');

속성과 메서드

클래스에 속성과 메서드를 정의하고, 인스턴스에서 이용함.

속성 메소드

brand, name, color 등 공통적으로 가지고 있는 속성들

refuel(), setSpeed(), drive() 등의 행동(함수)

속성과 메서드의 정의

 ES5  

function Car(brand, name, color) {
  //속성의 정의
  this.brand = brand;
  this.name = name;
  this.color = color;
}

//메서드의 정의
Car.prototype.refuel = function() {
  //연료 공급 구현 코드
}
Car.prototype.drive() {
  //운전 구현
}

 

 ES6  

class Car {
  constructor(brand, name, color) {
    //속성의 정의
    this.brand = brand;
    this.name = name;
    this.color = color;
  }
  
  //메서드의 정의
  refuel() {
  }
  drive() {
  }
}

인스턴스에서의 사용

let avante = new Car('hyundar', 'avante', 'black');
console.log(avante.color); //black
avante.drive(); //아반떼가 운전을 시작

let mini = new Car('bmw', 'mini', 'white');
console.log(mini.brand); //bmw
mini.refuel(); //미니에 연료 공급

 

prototype class를 만들 때 쓰는 원형 객체(original form)
constructor 인스턴스가 초기화될 때 실행하는 생성자 함수
this 함수가 실행될 때, 해당 scope마다 생성되는 고유한 실행 context(execution context)