자바스크립트 - DOM과 클래스, 클로저

윱니·2023년 10월 17일

1. DOM

(1) DOM의 기본 개념

  • Document(HTML 파일)를 Javascript가 알아먹을 수 있는 Object 형태로 Modeling 한 것.
  • DOM이 브라우저에 내장되어 있기 때문에 우리는 HTML의 내용을 javascript로 접근, 제어할 수 있음.
  • 모든 DOM의 node(웹 페이지를 구성하는 모든 HTML 태그와 텍스트, 속성 등을 하나의 블록으로 취급하는 것)들은 속성과 메서드를 가지고 있음.
  • Node 객체의 속성은 해당 객체의 특성을 나타내는 값을 가져오거나 설정하는데 사용, 메서드는 해당 객체가 수행하는 작업을 나타내는 함수.

(2) DOM에 접근 및 제어해보기

  • 항상 돔트리의 최상단 노드는 document
  • DOM은 모두 노드로 이루어져 있기 때문에 부모노드-자식노드관계로 이루어져 있음. 왔다갔다,,

2. Class

클래스(Class)는 설계도. 특징(변수 또는 속성과 메서드)이 무엇인지 알 수 있음.
인스턴스(Instance)는 설계도를 보고 만들어진 실체물건.

(1) Class란?

  • 객체를 생성하기 위한 일종의 템플릿
  • Class를 생성하기 위해서는 class키워드 사용

(2) Constructor란?

  • Constructor는 Class의 생성자 함수.
  • 생성자 함수는 객체를 생성할 때 호출되며, 객체를 초기화하는 역할.
  • constructor 키워드 사용.

(2)-1 클래스 연습해보기
<Car 라는 새로운 클래스를 만들되, 처음 객체를 만들 때는 modelName, modelYear, type, price 값이 필수로 입력돼야 함!, makeNoise() 메서드를 만들어 클락션을 출력해주세요>

// 새로운 Car class 정의
class Car {
  constructor(modelName, modelYear, type, price) {
    this.modelName = modelName;
    this.modelYear = modelYear;
    this.type = type;
    this.price = price;
  }

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

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

(3) Getter와 Setter

  • Class에서는 getter와 setter를 사용하여 Class의 속성에 접근할 수 있음.
  • getter는 속성 값을 반환하는 메서드. setter는 속성 값을 설정하는 메서드.
// Getters와 Setters
// 객체지향 프로그래밍 언어 -> G, S
// 클래스 --> 객체(인스턴스)
// 프로퍼티(constructor)
// new Class(a, b, c)
class Rectangle {
  constructor(height, width) {
    // underscore : private(은밀하고, 감춰야 할 때)
    this._height = height;
    this._width = width;
  }

  // width를 위한 getter
  get width() {
    return this._width;
  }

  // width를 위한 setter
  set width(value) {
    // 검증 1 : value가 음수이면 오류!
    if (value <= 0) {
      //
      console.log("[오류] 가로길이는 0보다 커야 합니다!");
      return;
    } else if (typeof value !== "number") {
      console.log("[오류] 가로길이로 입력된 값이 숫자타입이 아닙니다!");
      return;
    }
    this._width = value;
  }

  // height를 위한 getter
  get height() {
    return this._height;
  }

  // height를 위한 setter
  set height(value) {
    // 검증 1 : value가 음수이면 오류!
    if (value <= 0) {
      //
      console.log("[오류] 세로길이는 0보다 커야 합니다!");
      return;
    } else if (typeof value !== "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);

=> this로 접근하는 property에 underscore(_)를 사용하지 않으면 무한루프에 빠지게 됨.

(3)-1 Getter와 Setter 연습해보기

// 클래스 연습해보기!
// [요구사항]
// 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;
    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;
    }

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

  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;
    }

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

  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);

(4) 상속(Inheritance)

  • Class는 상속을 통해 다른 Class의 기능을 물려받을 수 있음.
// 동물 전체에 대한 클래스에요
class Animal {

	// 이름을 필수로 받아야 해요
  constructor(name) {
    this.name = name;
  }

	// 동물의 행동을 정의하는 메소드
  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

// 동물 클래스를 상속받는 Dog 클래스를 만들어요
class Dog extends Animal {
	// 상속받을 때, speak()를 입맛에 맞게 재정의해요.
  speak() {
    console.log(`${this.name} barks.`);
  }
}

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

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

=> 위 코드에서 Dog Class는 Animal Class를 상속받음. Dog Class에서 speak 메서드를 오버라이딩하여 Animal Class의 speak 메서드를 덮어씀.

(4)-1 상속(Inheritance) 연습해보기

  • super를 적용해보자!
// 새로운 Car class 정의
class Car {
  constructor(modelName, modelYear, type, price) {
    this.modelName = modelName;
    this.modelYear = modelYear;
    this.type = type;
    this.price = price;
  }

  // 모델명 출력하는 메서드
  makeNoise() {
    console.log(`${this.modelName}: 빵!`);
  }

  // getters
  getModelName() {
    return this.modelName;
  }
  getModelYear() {
    return this.modelYear;
  }
  getPrice() {
    return this.price;
  }

  // 새로운 가격 반영
  setPrice(newPrice) {
    this.price = newPrice;
  }
}

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

console.log(car1.getModelName());
console.log(car1.getModelYear());
console.log(car1.getPrice());

// 가격변동
car1.setPrice(6000);
console.log(car1.getPrice());

// 아래 부분이 상속에 관련된 코드에요!
class ElectronicCar extends Car {
  constructor(modelName, modelYear, price) {
    // Car에 있는 constructor를 통해 자동차를 만들었어요!
    super(modelName, modelYear, "e", price);
  }
}

const eleCar1 = new ElectronicCar("뉴아이오닉5", "2023", 7000);
console.log(eleCar1.getModelName());

=> 전기차이기 때문에 constructor 상에 type은 필요없음!

(5) Static Method

  • Class에서는 static 키워드를 사용하여 Class 레벨의 메소드를 정의할 수 있음.
    Class 레벨의 메소드는 인트턴스에서 호출할 수 없으며, Class 이름으로 직접 호출할 수 있음.
  • 인스턴스를 만들지 않고 사용할 수 있기 때문에 유틸리티 함수, 정적 속성인 경우 인스턴스 간에 복제할 필요가 없는 데이터(똑같은 것을 공유해서 쓸 때)를 만들 때 사용됨.
  • 즉! 인스턴스를 만들 필요가 없을 때 사용한다고 이해하면 편함!
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

=> 위 코드에서 Calculator Class는 add와 Subtract 메소드를 정의함. 두 메소드 모두 Class 레벨에서 호출할 수 있으며, 인스턴스에서 호출할 수 없음.

3. 클로저

(1) 클로저의 개념(정의)

  • 함수가 선언된 렉시컬환경
const x = 1;

function outerFunc() {
  const x = 10;
  function innerFunc() {
    console.log(x); // 10
  }

  innerFunc();
}

outerFunc();

=> [설명]

  • innerFunc() 내부의 console.log(x)에서 참조하고 있는 x 값은
  • 먼저 스코프 내부에서 x 값을 찾는다.
  • 없는 경우 scope chain에 의해 바로 바깥쪽 scope를 찾는다.
  • 실행컨텍스트에서 배웠던 outer를 찾는 것!
  • outer는 해당 실행컨텍스트의 생성시점의 LexicalEnvironment를 갖고 있음.
  • 그래서 10에 먼저 접근하고, console.log(x)는 10이 출력됨.
const x = 1;

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

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

outerFunc();

=> outerFunc와 innerFunc는 서로 다른 scope를 가지고 있음. 따라서 변수 공유 불가!
=> InnerFunc함수를 OuterFunc함수의 내부에서 호출한다 하더라도 outerFunc함수의 변수에 접근할 수 없음!

  • 렉시컬 스코프 : 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의 된 환경(위치)에 의해 결정됨. ('어디서 호출했는지'가 아님)
  • 클로저와 렉시컬 환경 : 외부 함수보다 중접 함수가 더 오래 유지되는 경우, 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 (여전히) 참조할 수 있다. ← 이 개념에서 중첩 함수가 바로 클로저.
const x = 1;

// 1
function outer() {
  const x = 10;
  const inner = function () {
    console.log(x);
  };
  return inner;
}

const innerFunc = outer();
innerFunc();

=>

  • outer 함수를 호출하면 중첩 함수 inner를 반환(return).
  • 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스탭에서 팝되어 제거됨(역할을 다 했으니까)
  • inner 함수는 런타임에 평가됨.
  • inner함수가 innerFunc에 전달되었는데, 이는 outer 함수의 렉시컬환경을 (여전히) 참조하고 있음.
  • 즉, outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer 함수의 렉시컬 환경까지 소멸하는 것은 아님.

(2) 클로저의 활용

  • 클로저는 JS의 강력한 기능!
  • 주로 상태를 안전하게 변경하고 유지하기 위해 사용.
  • 상태를 안전하게 은닉한다(특정 함수에게만 상태 변경을 허용)

<예제>

// 카운트 상태 변경 함수 #1
// 함수가 호출될 때마다 호출된 횟수를 누적하여 출력하는 카운터를 구현해요!

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

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

console.log(increase());
// num = 100; // 치명적인 단점이 있어요.
console.log(increase());
console.log(increase());

=> [보완해야 할 사항]

  • 카운트 상태(num 변수의 값)는 increase 함수가 호출되기 전까지 변경되지 않고 유지돼야 함.
  • 이를 위해 카운트 상태(num 변수의 값)는 increase 함수만이 변경할 수 있어야 함.
  • 전역변수인 num이 문제 -> 지역변수로 바꿔볼까?
// 카운트 상태 변경 함수 #2
const increase = function () {
  // 카운트 상태 변수
  let num = 0;

  // 카운트 상태를 1만큼 증가시킨다.
  return ++num;
};

// 이전 상태값을 유지 못함
console.log(increase()); //1
console.log(increase()); //1
console.log(increase()); //1

=> [리뷰]

  • num 변수는 increase 함수의 지역변수로 선언됐기 때문에 의도치 않는 변경은 방지됐음!
  • 즉, num 변수의 상태는 increase 함수만이 변경할 수 있음.
  • 하지만, increase()가 호출될 때마다 num이 초기화됨.
  • 백번, 천번 호출해도 언제 1인 increase...
  • 의도치 않은 변경은 방지하면서, 이전 상태를 유지해야 함!
  • 클로저를 사용해보자!
// 카운트 상태 변경 함수 #3
const increase = (function () {
  // 카운트 상태 변수
  let num = 0;

  // 클로저
  return function () {
    return ++num;
  };
})();

// 이전 상태값을 유지
console.log(increase()); //1
console.log(increase()); //2
console.log(increase()); //3

=> [코드설명]

  • 위 코드가 실행되면 즉시 실행 함수가 호출되고 즉시 실행 함수가 반환한 함수가 increase 변수에 할당됨.
  • increase 변수에 할당된 함수는 자신이 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저.
  • 즉시 실행 함수는 호출된 이후 소멸되지만, 즉시 실행 함수가 반환한 클로저는 increase 변수에 할당되어 호출됨.
  • 이때 즉시 실행 함수가 반환한 클로저는 자신이 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하고 있음.
  • 따라서 즉시 실행 함수가 반환한 클로저는 카운트 상태를 유지하기 위한 자유 변수 num을 언제 어디서 호출하든지 참조하고 변경할 수 있음.
  • num은 초기화되지 않을 것이며, 외부에서 직접 접근할 수 없는 은닉된 private 변수이므로, 전역 변수를 사용했을 때와 같이 의도되지 않은 변경을 걱정할 필요도 없음.

결론!! 클로저는 상태(state)가 의도치 않게 변경되지 않도록 안전하게 은닉(information hiding) 하고 특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경하고 유지하기 위해 사용함.

profile
코린이 탈출을 기원하는 코린이

0개의 댓글