[프론트엔드 스쿨 6기] 🗓️ 7월12일

유동균·2023년 7월 12일
0

프론트엔드 스쿨 6기

목록 보기
30/44
post-thumbnail

가바지 컬렉션

자바스크립트는 눈에 보이지 않는 곳에서 메모리 관리를 수행합니다.

원시값, 객체, 함수 등 우리가 만드는 모든 것은 메모리를 차지합니다.

가비지 컬렉션 기준

자바스크립트는 도달 가능성(reachability) 이라는 개념을 사용해 메모리 관리를 수행합니다.

‘도달 가능한(reachable)’ 값은 쉽게 말해 어떻게든 접근하거나 사용할 수 있는 값을 의미합니다. 도달 가능한 값은 메모리에서 삭제되지 않습니다.
1. 아래 소개해 드릴값들은 그 태생부터 도달 가능하기 때문에, 명백한 이유 없이는 삭제되지 않습니다.

예시:

현재 함수의 지역 변수와 매개변수
중첩 함수의 체인에 있는 함수에서 사용되는 변수와 매개변수
전역 변수
기타 등등
이런 값은 루트(root) 라고 부릅니다.

  1. 루트가 참조하는 값이나 체이닝으로 루트에서 참조할 수 있는 값은 도달 가능한 값이 됩니다.

전역 변수에 객체가 저장되어있다고 가정해 봅시다. 이 객체의 프로퍼티가 또 다른 객체를 참조하고 있다면, 프로퍼티가 참조하는 객체는 도달 가능한 값이 됩니다.

자바스크립트 엔진 내에선 가비지 컬렉터(garbage collector)가 끊임없이 동작합니다. 가비지 컬렉터는 모든 객체를 모니터링하고, 도달할 수 없는 객체는 삭제합니다.

// user엔 객체 참조 값이 저장됩니다.
let user = {
  name: "John"
};

// 이제 John은 도달할 수 없는 상태가 되었습니다. John에 접근할 방법도, John을 참조하는 것도 모두 사라졌습니다
// 가비지 컬렉터는 이제 John에 저장된 데이터를 삭제하고, John을 메모리에서 삭제합니다.
user = null;

mark-and-sweep

메서드와 this

메서드

객체 프로퍼티에 할당된 함수를 메서드(method) 라고 부릅니다.

let user = {
  name: "John",
  age: 30
};

user.sayHi = function() {
  alert("안녕하세요!");
};

// user에 할당된 sayHi가 메서드
user.sayHi(); 

단축 구문을 사용해야하는 이유
1. this도 내장하고 있고
2. 컨스트럭터를 내장하고 있지 않아 가볍다

메서드와 this

let user = {
  name: "John",
  age: 30,

  sayHi() {
    // 'this'는 '현재 객체'를 나타냅니다.
    alert(this.name);
  }

};

user.sayHi(); // John
  • this를 사용하지 않고 외부 변수를 참조해 객체에 접근하는 것도 가능합니다.
    • 그런데 이렇게 외부 변수를 사용해 객체를 참조하면 예상치 못한 에러가 발생할 수 있습니다. user를 복사해 다른 변수에 할당(admin = user)하고, user는 전혀 다른 값으로 덮어썼다고 가정해 봅시다. sayHi()는 원치 않는 값(null)을 참조할 겁니다.
let user = {
  name: "John",
  age: 30,

  sayHi() {
    alert( user.name ); // Error: Cannot read property 'name' of null
  }

};


let admin = user;
user = null; // user를 null로 덮어씁니다.

admin.sayHi(); // sayHi()가 엉뚱한 객체를 참고하면서 에러가 발생했습니다.

결제 금액 계산하기

const shopOrder = {
  date: '2023. 7. 11',
  tableIndex: 5,
  menu: [
    { name: '통 새우 돈까스', price: 13000, count: 2 },
    { name: '치즈 돈까스', price: 10000, count: 1 },
  ],
};
// 1번째 방법
const totalPrice =
  shopOrder.menu[0].price * shopOrder.menu[0].count +
  shopOrder.menu[1].price * shopOrder.menu[1].count;

// 2번째 방법
let totalPrice = 0;
for (const key in shopOrder) {
  if (Object.hasOwnProperty.call(shopOrder, key)) {
    if (typeof shopOrder[key] === 'object') {
      for (const value of shopOrder[key]) {
        totalPrice += value.price * value.count;
      }
    }
  }
}

// 3번째 방법
let totalPrice = 0;
shopOrder.menu.forEach((e) => (totalPrice += e.price * e.count));

// 4번째 방법
let totalPrice = 0;
shopOrder.menu.map((e) => (totalPrice += e.price * e.count));

객체 안에서 처리하기

const shopOrder = {
  total: 0,  // 전체 가격 구하기
  date: '2023. 7. 11',
  tableIndex: 5,
  menu: [
    { name: '통 새우 돈까스', price: 13000, count: 2 },
    { name: '치즈 돈까스', price: 10000, count: 1 },
    { name: '고구마 치즈 돈까스', price: 15000, count: 3 },
    { name: '뼈해장국', price: 17000, count: 5 },
  ],
  totalPice() {
    //1. .forEach()를 이용한 방법
    this.menu.forEach((e) => (this.total += e.price * e.count));
    
    //2. .reduce()를 이용한 방법
    this.total = this.menu.reduce((acc, cur) => acc + cur.price * cur.count, 0);
  },
};
shopOrder.totalPice();

프로토타입

프로토타입 상속

개발을 하다 보면 기존에 있는 기능을 가져와 확장해야 하는 경우가 생깁니다.

사람에 관한 프로퍼티와 메서드를 가진 user라는 객체가 있는데, user와 상당히 유사하지만 약간의 차이가 있는 admin과 guest 객체를 만들어야 한다고 가정해 봅시다. 이때 "user의 메서드를 복사하거나 다시 구현하지 않고 user에 약간의 기능을 얹어 admin과 guest 객체를 만들 수 있지 않을까?"라는 생각이 들 겁니다.

자바스크립트 언어의 고유 기능인 프로토타입 상속(prototypal inheritance) 을 이용하면 위와 같은 생각을 실현할 수 있습니다.

[[Prototype]]

자바스크립트의 객체는 명세서에서 명명한 [[Prototype]]이라는 숨김 프로퍼티를 갖습니다. 이 숨김 프로퍼티 값은 null이거나 다른 객체에 대한 참조가 되는데, 다른 객체를 참조하는 경우 참조 대상을 '프로토타입(prototype)'이라 부릅니다.
[[Prototype]] 프로퍼티는 내부 프로퍼티이면서 숨김 프로퍼티이지만 다양한 방법을 사용해 개발자가 값을 설정할 수 있습니다.

아래 예시처럼 특별한 이름인 __proto__을 사용하면 값을 설정할 수 있습니다.

__proto__[[Prototype]]용 getter·setter입니다.
__proto__[[Prototype]]과 다릅니다. __proto__[[Prototype]]의 getter(획득자)이자 setter(설정자) 입니다.

하위 호환성 때문에 여전히 __proto__를 사용할 순 있지만 비교적 근래에 작성된 스크립트에선 __proto__ 대신 함수 Object.getPrototypeOf나 Object.setPrototypeOf을 써서 프로토타입을 획득(get)하거나 설정(set)합니다. 근래엔 왜 __proto__를 쓰지 않는지와 두 함수의 자세한 설명에 대해선 이어지는 챕터에서 다룰 예정입니다.

__proto__는 브라우저 환경에서만 지원하도록 자바스크립트 명세서에서 규정하였는데, 실상은 서버 사이드를 포함한 모든 호스트 환경에서 __proto__를 지원합니다. [[Prototype]]보다는 __proto__가 조금 더 직관적이어서 이해하기 쉬우므로, 본 튜토리얼의 예시에선 __proto__를 사용하도록 하겠습니다.

let animal = {
  eats: true,
  walk() {
    alert("동물이 걷습니다.");
  }
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

let longEar = {
  earLength: 10,
  __proto__: rabbit
};

// 메서드 walk는 프로토타입 체인을 통해 상속받았습니다.
longEar.walk(); // 동물이 걷습니다.
alert(longEar.jumps); // true (rabbit에서 상속받음)

프로토타입 체이닝엔 두 가지 제약사항이 있습니다.

  • 순환 참조(circular reference)는 허용되지 않습니다. __proto__를 이용해 닫힌 형태로 다른 객체를 참조하면 에러가 발생합니다.
  • __proto__의 값은 객체나 null만 가능합니다. 다른 자료형은 무시됩니다.
  • 객체엔 오직 하나의 [[Prototype]]만 있을 수 있다는 당연한 제약도 있습니다. 객체는 두 개의 객체를 상속받지 못합니다.

프로토타입은 읽기 전용이다

프로토타입은 프로퍼티를 읽을 때만 사용합니다.

프로퍼티를 추가, 수정하거나 지우는 연산은 객체에 직접 해야 합니다.

객체 rabbit에 메서드 walk를 직접 할당해 보겠습니다.

let animal = {
  eats: true,
  walk() {
    /* rabbit은 이제 이 메서드를 사용하지 않습니다. */
  }
};

let rabbit = {
  __proto__: animal
};

rabbit.walk = function() {
  alert("토끼가 깡충깡충 뜁니다.");
};

rabbit.walk(); // 토끼가 깡충깡충 뜁니다.

rabbit.walk()를 호출하면 프로토타입에 있는 메서드가 실행되지 않고, 객체 rabbit에 직접 추가한 메서드가 실행됩니다.

for...in

let animal = {
  eats: true
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

// Object.keys는 객체 자신의 키만 반환합니다.
alert(Object.keys(rabbit)); // jumps

// for..in은 객체 자신의 키와 상속 프로퍼티의 키 모두를 순회합니다.
for(let prop in rabbit) alert(prop); // jumps, eats

obj.hasOwnProperty(key)를 이용하면 상속 프로퍼티를 순회 대상에서 제외할 수 있습니다. 이 내장 메서드는 key에 대응하는 프로퍼티가 상속 프로퍼티가 아니고 obj에 직접 구현되어있는 프로퍼티일 때만 true를 반환합니다.

obj.hasOwnProperty(key)를 응용하면 아래 예시에서처럼 상속 프로퍼티를 걸러낼 수 있고, 상속 프로퍼티만을 대상으로 무언가를 할 수도 있습니다.

let animal = {
  eats: true
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

for(let prop in rabbit) {
  let isOwn = rabbit.hasOwnProperty(prop);

  if (isOwn) {
    alert(`객체 자신의 프로퍼티: ${prop}`); // 객체 자신의 프로퍼티: jumps
  } else {
    alert(`상속 프로퍼티: ${prop}`); // 상속 프로퍼티: eats
  }
}

위 예시의 상속 관계를 그림으로 나타내면 다음과 같습니다. rabbit은 animal을, animal은 Object.prototype을, Object.prototype은 null을 상속받고 있습니다. 참고로 animal이 Object.prototype를 상속받는 이유는 animal을 객체 리터럴 방식으로 선언하였기 때문입니다.

그림을 보면 for..in 안에서 사용한 메서드 hasOwnProperty가 Object.prototype.hasOwnProperty에서 왔다는 것을 확인할 수 있습니다.

엇? 그런데 상속 프로퍼티인 eats는 얼럿 창에 출력되는데, hasOwnProperty는 출력되지 않았습니다. 무슨 일이 있는 걸까요?

이유는 간단합니다. hasOwnProperty는 열거 가능한(enumerable) 프로퍼티가 아니기 때문입니다. Object.prototype에 있는 모든 메서드의 enumerable 플래그는 false인데, for..in은 오직 열거 가능한 프로퍼티만 순회 대상에 포함하기 때문에 hasOwnProperty는 얼럿창에 출력되지 않습니다.

Object.definedProperty()로 enumerable하지 않은 프로퍼티를 생성할 수 있다.

함수의 prototype 프로퍼티

우리는 리터럴 뿐만 아니라 new F()와 같은 생성자 함수로도 새로운 객체를 만들 수 있다는 걸 배운 바 있습니다.
const obj = new Object(), const num = new Number(1)...

생성자 함수로 객체를 만들었을 때 리터럴 방식과 다른점은 생성자 함수의 프로토타입이 객체인 경우에 new 연산자를 사용해 만든 객체는 생성자 함수의 프로토타입 정보를 사용해 [[Prototype]]을 설정한다는 것입니다.

class

class :클래스는 객체 지향 프로그래밍에서 특정 객체를 생성하기 위해 변수와 메소드를 정의하는 일종의 틀로, 객체를 정의하기 위한 상태(멤버 변수)와 메서드(함수)로 구성된다.

실무에선 사용자나 물건같이 동일한 종류의 객체를 여러 개 생성해야 하는 경우가 잦습니다.

이럴 때 new 연산자와 생성자 함수에서 배운 new function을 사용할 수 있습니다.

여기에 더하여 모던 자바스크립트에 도입된 클래스(class)라는 문법을 사용하면 객체 지향 프로그래밍에서 사용되는 다양한 기능을 자바스크립트에서도 사용할 수 있습니다.

  • 기본형
class MyClass {
  // 여러 메서드를 정의할 수 있음
  constructor() { ... }
  method1() { ... }
  method2() { ... }
  method3() { ... }
  ...
}
// 이렇게 클래스를 만들고, new MyClass()를 호출하면 내부에서 정의한 메서드가 들어 있는 객체가 생성됩니다.
// 객체의 기본 상태를 설정해주는 생성자 메서드 constructor()는 new에 의해 자동으로 호출되므로, 특별한 절차 없이 객체를 초기화 할 수 있습니다.
  • 예시
class User {

  constructor(name) {
    this.name = name;
  }

  sayHi() {
    alert(this.name);
  }
 
}

// 사용법:
let user = new User("John");
user.sayHi();

new User("John")를 호출하면 다음과 같은 일이 일어납니다.

새로운 객체가 생성됩니다.
넘겨받은 인수와 함께 constructor가 자동으로 실행됩니다. 이때 인수 "John"이 this.name에 할당됩니다.
이런 과정을 거친 후에 user.sayHi() 같은 객체 메서드를 호출할 수 있습니다.

0개의 댓글