20일차 - Javascript 문법 (7) 클래스와 클로저

이상민·2024년 8월 22일

TIL

목록 보기
19/50

클래스(Class)

클래스란?

  • 클래스는 학교에서 다양한 종류의 책상을 만드는 설계도(틀) 와 비슷하다. 이 설계도를 보면 어떤 종류의 책상을 만들 수 있는지, 책상이 가지고 있는 특징(변수 or 속성, 메서드)이 무엇인지 알 수 있다.

그럼 인스턴스(Instance)는 뭐야?

  • 클래스가 책상을 만드는 설계도라면 인스턴스는 이 설계도를 보고 만들어진 실제 책상이라고 할 수 있다. 책상의 모양, 크기, 색상, 재료 등은 모두 설계도(Class)에 따라 만들어지며, 이러한 책상들은 모두 다른 인스턴스가 된다.

    즉, 이렇게 클래스는 객체를 만들기 위한 설계도라고 생각할 수 있고, 이 설계도를 바탕으로 만들어진 실제 객체들은 인스턴스라고 할 수 있다.😯

클래스 기본 개념

  • 위에서 쉬운 예시로 보았듯이 Class는 객체를 생성하기 위한 일종의 템플릿이라고 할 수 있다.
class Person {
	// constructor는 이름을 변경할 수 없어요 !!
  constructor(name, age) {
		// 이름(name)과 나이(age)가 없으면 사람이 아니죠?
		// new라는 키워드를 이용해서 인스턴스를 만들 때, 기본적으로 넣어야 하는 값들을 의미
		// 여기서 말하는 this는 만들어질 인스턴스를 의미
    this.name = name;
    this.age = age;
  }

	// 다양한 메소드를 아래와 같이 정의할 수 있어요.
	// 여기서 this.name으로 내부 값을 접근해야 함!!!
  sayHello() {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
  }
}

const person1 = new Person("Alice", 30);
const person2 = new Person("Bob", 25);

// 만든 객체를 토대로 메서드 호출해보기
person1.sayHello(); // 출력: "Hello, my name is Alice and I am 30 years old."
person2.sayHello(); // 출력: "Hello, my name is Bob and I am 25 years old."

위 코드에서 Person Class는 name과 age 속성을 가지고 있으며, sayHello 메소드를 정의한다. 그리고 new 키워드를 사용하여 Person Class의 인스턴스를 생성하고, sayHello 메소드를 호출

Class 아래 Constructor는 멍미?😳

  • Constructor는 Class의 생성자 함수이다!! 생성자 함수는 객체를 생성할 때 호출되며, 객체를 초기화하는 역할을 한다. 그럼 constructor 키워드를 사용하여 정의해보자 !
class Person {
  constructor(name, age) { // constructor
    this.name = name;
    this.age = age;
  }

  sayHello() {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
  }
}

const person = new Person('Alice', 20);

위에서 배운 Class와 Constructor 를 이용하여 연습해보자 !

class supercar {
  constructor(modelName, modelYear, type, price) {
    this.modelName = modelName;
    this.modelYear = modelYear;
    this.type = type;
    this.price = price;
  }
  // 클락션 울리는 메서드
  makenoise() {
    console.log(
      this.modelName + "는" + this.modelYear + "년도 자동차 이무니다."
    );
  }
}
// 슈퍼카 만들기
let supercar1 = new supercar("람보르기니", "2023", "e", 5000);
let supercar2 = new supercar("우루스", "1992", "G", 5000);
let supercar3 = new supercar("제네시스", "2017", "D", 5000);
supercar1.makenoise(); 
supercar2.makenoise(); 
supercar3.makenoise(); 


잘 출력이 되었따 !

Getter와 Setter ?

Class에서는 getter와 setter를 사용하여 Class의 속성에 접근할 수 있다! getter는 속성 값을 반환하는 메소드이며, setter는 속성 값을 설정하는 메소드, 이를 통해 생성한 인스턴스를 정해진 규격 안에서 자유자제로 변경 가능!

Getter와 Setter 예시)

// Getters와 Setters
// 객체지향 프로그래밍 언어 -> G, S
// 클래스 --> 객체(인스턴스)
// 프로퍼티(constructor)
// new Class(a, b, c)
class Rectangle {
  constructor(height, width) {
    this._height = height; // this.뒤 height앞에 언더바 필수 !
    this._width = width; //  this.뒤 width앞에 언더바 필수 !
  }
  get width() { // width를 위한 getter
    return this._width; // this.뒤 width앞에 언더바 필수 !
  }
  set width(value) {  // width를 위한 setter
    
    // 검증 1 : value가 음수이면 오류!
    if (value <= 0) {
      //
      console.log("[오류] 가로길이는 0보다 커야 합니다!");
      return;
    } else if (typeof value !== "number") {
      console.log("[오류] 가로길이로 입력된 값이 숫자타입이 아닙니다!");
      return;
    }
    this._width = value; // this.뒤 width앞에 언더바 필수 ! 
  }
  get height() { // height를 위한 getter
    return this._height; // this.뒤 height앞에 언더바 필수 !
  }
  set height(value) { // height를 위한 setter
    
    // 검증 1 : value가 음수이면 오류!
    if (value <= 0) {
      //
      console.log("[오류] 세로길이는 0보다 커야 합니다!");
      return;
    } else if (typeof value !== "number") { // 그냥 number 라고 입력하면 변수로 인식함
      console.log("[오류] 세로길이로 입력된 값이 숫자타입이 아닙니다!");
      return;
    }
    this._height = value;
  }
  getArea() {   // getArea : 가로 * 세로 => 넓이
    const a = this._width * this._height;
    console.log(`넓이는 => ${a}입니다.`);
  }
}
// instance 생성
const rect1 = new Rectangle(10, 7);
rect1.getArea();
// const rect2 = new Rectangle(10, 30);
// const rect3 = new Rectangle(15, 20);

위에서 배운 Getter와 Setter 를 이용하여 연습해보자 !

  • modelName, modelYear, price를 가져오는 메서드 getModelName, getModelYear, getPrice를 사용하여 작성해보자. 그리고 가격변동이 일어났을 때 사용할 수 있는 메서드인 setPrice도 작성 고고
// 클래스 연습해보기!
// [요구사항]
// 1. Car라는 새로운 클래스를 만들되, 처음 객체를 만들 때는
//    다음 네 개의 값이 필수로 입력돼야 합니다!
//    (1) modelName
//    (2) modelYear
//    (3) type : 가솔린, 전기차, 디젤
//    (4) price
// 2. makeNoise() 메서드를 만들어 클락션을 출력해주세요.
// 2-1. 해당 자동차가 몇년도 모델인지 출력하는 메서드 작성!
// 3. 이후 자동차를 3개 정도 만들어주세요(객체 생성)

// [추가 요구사항]
// 1. modelName, modelYear, price, type을 가져오는 메서드
// 2. modelName, modelYear, price, type을 세팅하는 메서드
// 3. 만든 인스턴스를 통해서 마지막에 set 해서 get 하는 로직까지
class Car {
  constructor(modelName, modelYear, type, price) {
    this._modelName = modelName; // set get 시 this. 뒤에 언더바_ 꼭 !
    this._modelYear = modelYear;
    this._type = type;
    this._price = price;
  }

  get modelName() {
    return this._modelName;
  }

  // 입력값에 대한 검증까지 가능하다
  set modelName(value) {
    if (value.length <= 0) {  // 모델명 유효성 검사
      console.log("[오류] 모델명이 입력되지 않았습니다. 확인해주세요!");
      return;
    } else if (typeof value !== "string") {
      console.log("[오류] 입력된 모델명이 문자형이 아닙니다!");
      return;
    }

    this._modelName = value; // 검증이 완료된 경우에만 setting!
  }

  get modelYear() {
    return this._modelYear;
  }

  set modelYear(value) {
    if (value.length !== 4) {  // 년도 유효성 검사
      // 연도에 대한 유효성 검증 로직 ---> googling 엄청~~~~많이 나옵니다!!
      console.log("[오류] 입력된 년도가 4자리가 아닙니다.확인해주세요!");
      return;
    } else if (typeof value !== "string") {
      console.log("[오류] 입력된 모델명이 문자형이 아닙니다!");
      return;
    }

    this._modelYear = value; // 검증이 완료된 경우에만 setting!
  }

  get type() {
    return this._type;
  }

  set type(value) {
    if (value.length <= 0) {
      console.log("[오류] 타입이 입력되지 않았습니다. 확인해주세요!");
      return;
    } else if (value !== "g" && value !== "d" && value !== "e") {
      // g(가솔린), d(디젤), e(전기차)가 아닌 경우 오류
      console.log("[오류] 입력된 타입이 잘못되었습니다. 확인해주세요!");
      return;
    }

    // 검증 완료!
    this._type = value;
  }

  get price() {
    return this._price;
  }

  set price(value) {
    if (typeof value !== "number") {
      console.log("[오류] 가격으로 입력된 값이 숫자가 아닙니다. 확인해주세요!");
      return;
    } else if (value < "1000000") {
      console.log("[오류] 가격은 100만원보다 작을 수 없습니다. 확인해주세요!");
      return;
    }

    // 검증이 완료된 경우
    this._price = value;
  }

  // 클락션을 울리는 메서드
  makeNoise() {
    console.log(this._modelName + ": 빵!");
  }

  // 해당 자동차가 몇년도 모델인지 출력하는 메서드 작성!
  printModelYear() {
    console.log(
      this._modelName + "은 " + this._modelYear + "년도의 모델입니다."
    );
  }
}

// 자동차 만들기
const car1 = new Car("Sorento", "2023", "e", 5000);
const car2 = new Car("SM5", "1999", "g", 3000);
const car3 = new Car("Palisade", "2010", "d", 4500);
// car1.makeNoise();
// car1.printModelYear();
// car2.makeNoise();
// car2.printModelYear();
// car3.makeNoise();
// car3.printModelYear();

// getter 예시1
console.log(car1.modelName);
// setter 예시1
car1.modelName = 1;
console.log(car1.modelName);

상속(Inheritance)💁‍♀️

  • Class 는 상속(Inheritance)을 통해 다른 Class 기능을 물려받을수 있다 ! 이러한 상속을 통해 받는 Class를 subclass 또는 derived class라고 하며, 상속을 하는 Class를 superclass 또는 base class라고 한다.
    예시1)
class Animal { // 동물 전체에 대한 클래스

  constructor(name) { // 이름 필수로 받기
    this.name = name;
  }

  speak() { // 메서드 짖다 !
    console.log(`${this.name} makes a fuckin noise.`);
  }
}

class Dog extends Animal { 
  // extends를 통해 동물 클래스를 상속받는 Dog클래스를 만들기
  // 이러한 부모에게서 내려받은 메서드를 재정의 하는것을 overriding 이라고 한다 !
  // overriding !!!
  speak() { // 상속받을 때, speak()를 입맛에 맞게 재정의
    console.log(`${this.name} barks.`);
  }
}

// Dog를 만들 때는 Animal의 상속을 받은 class이기 때문에 이름을 필수로 받아야 함 !!
let d = new Dog('Mitzie');

// speak는 'makes a fuckin noise'가 아니라, 'barks'가 출력되네요.
d.speak(); // "Mitzie barks."

Static Method(정적 메서드)🤐

  • Static이라는 말에서 알 수 있듯이, 인스턴스를 만들지 않고 사용할 수 있기 때문에 유틸리티 함수, 정적 속성인 경우 인스턴스 간에 복제할 필요가 없는 데이터(똑같은 것을 공유해서 쓸 때)를 만들 때 사용된다 ! 즉 굳이 인스턴스를 만들 필요가 없을 때 사용됨 !!
class Calculator {
  static add(a, b) {
    return a + b;
  }
  static subtract(a, b) {
    return a - b;
  }
}
console.log(Calculator.add(1, 2)); // 3
console.log(Calculator.subtract(3, 2)); // 1

클로저(Closure)

클로저란?

클로저란 함수와 그 함수가 선언된 렉시컬 환경과의 조합을 의미한다.. 뭔가 어려워 보이지만 JS의 강력한 기능이라니 한번 클로저에 대해 알아보도록 하자

예시 1) 함수가 선언된 렉시컬 환경

const x = 1;

function outerFunc() {
  const x = 10;
  function innerFunc() {
    console.log(x); // x는 스코프 내부에서 x값을 찾지만 없음
  } // 없는 경우 scope chain에 의해 바로 바깥쪽 scope를 찾는다. 그래서 10에 먼저 접근 // 10

  innerFunc();
}

outerFunc();

예시 2) 만약 아래와 같다면, innerFunc()에서 출력하는 x값은 어떻게 될까?

const x = 1;

// innerFunc()에서는 outerFunc()의 x에 접근할 수 없죠.
// Lexical Scope를 따르는 프로그래밍 언어이기 때문
function outerFunc() {
  const x = 10;
  innerFunc(); // 1
}

function innerFunc() {
  console.log(x); // 1
}

outerFunc();
  • JS 엔진은 함수를 어디서 '호출' 했는지가 아니라, 어디에 '정의' 했는지에 따라서 스코프(상위 스코프)를 결정한다.
  • 따라서 위 예시2) 에서는 outerFunc와, innerFunc는 서로 다른 scope를 가지고 있기 때문에 변수를 공유할 수 없어 값이 둘다 1이 나오게 된다.

렉시컬 스코프

  • 위에서 말했듯이 JS엔진은 함수를 어디서 ‘호출했는지’가 아니라 함수를 어디에 ‘정의했는지’에 따라 상위 스코프를 결정한다.
const x = 1;

function foo() {
  const x = 10;
  bar();
}

function bar() {
  console.log(x);
}

foo(); // 1
bar(); // 1
const x = 1;

function foo() {
  const x = 10;

  // 상위 스코프는 함수 정의 환경(위치)에 따라 결정된다.
  // 함수 호출 위치와 상위 스코프는 아무런 관계가 없다.
  bar();
}

// 함수 bar는 자신의 상위 스코프, 즉 전역 렉시컬 환경을 저장하여 기억한다.
function bar() {
  console.log(x);
}

foo();
bar();

클로저와 렉시컬 환경(LexicalEnvironment)

  • 외부 함수보다 중접 함수가 더 오래 유지되는 경우, 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 (여전히) 참조할 수 있다. ← 이 개념에서 중첩 함수가 바로 클로저이다.
const x = 1;

// 1
function outer() {
  const x = 10;
  const inner = function () {
    console.log(x);
  };
  return inner;
}
// outer함수를 '실행'해서 , innerFunc 담는다.
// outer함수의 return부분을 innerFunc 담는다는 뜻 !
const innerFunc = outer();
// --------------- 여기서 outer함수의 실행컨텍스는 날아간다.
innerFunc();

(2) 클로저의 활용

  • 클로저는 주로 의도치 않은 상태의 변경을 막고 ‘상태를 안전하게 변경하고 유지하기 위해 사용'된다.
    예시1) 카운터
// 카운트 상태 변경 함수 #1
// 함수가 호출될 때마다 호출된 횟수를 누적하여 출력하는 카운터를 구현한다.

// 카운트 상태 변수
let num = 0;

const increase = function () { // 카운트 상태 변경 함수  
    return ++num;  // 카운트 상태를 1만큼 증가시킨다.
};

console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1
  • num 변수는 increse 함수의 지역변수로 선언하여 변경은 방지 ! = 즉 num 함수의 변수는 오직 increse 함수만이 변경 가능
  • 하지만 increse()가 호출될 때 마다 num이 초기화되는 이상한코드가 된다..
  • 몇천 몇만번을 호출해도 결국 출력값은 언제나 1이 됨..
    이러한 의도치 않은 변경은 방지하면서 이전 상태를 유지하기 위해 우리는 Closure함수를 써야한다 !!
// 카운트 상태 변경 함수 #3
const increase = (function () {
  // 카운트 상태 변수
  let num = 0;

  // 클로저
  return function () {
    return ++num;
  }; // increse 그 자체
})();

// 이전 상태값을 유지
console.log(increase()); //1
console.log(increase()); //2
console.log(increase()); //3
  • 위 코드가 실행되면, '즉시 실행 함수'가 호출됨 !! => 함수가 반환(inner) => increse 에 할당
  • increse 변수에 할당된 함수는 자신이 정의된 위치에 의해서 결정된 상위 스코프인 즉시 실행 함수의 '렉시컬 환경'을 기억한다.
    • 쉽게말하면 클로저 -> let num = 0;을 기억한다
  • 즉시 실행함수는 즉시 소멸됨 !

결론

  • 클로저를 쓰게되면서 num은 초기화 X, 외부에서 접근할 수 없는 은닉된 값도 얻게 되었고, 의도되지 않은 변경도 걱정 ㄴㄴ --> Increse 에서만 변경할 수 있기 때문에 !! (어렵따..)

0개의 댓글