5월 29일 TIL

·2023년 5월 29일
0
post-thumbnail

▶ JavaScript 문법 종합반 5주차

▶ DOM (Document Object Model)

[1] DOM의 기본 개념
# JavaScript가 HTML 문서를 해석하기 위해서 파싱한 즉, 해석한 결과물이다.

# JavaScript가 생긴 이유
· 브라우저에서 사용하려고 만들어졌다.
· 웹 페이지를 동적으로 만들기 위해, HTML 문서를 조작하여 생명력을 불어넣기 위해

# 웹페이지가 뜨는 과정
a. 사용자가 브라우저에 주소 입력 / 사용자 = 브라우저 = 클라이언트
b. HTML 문서를 서버로부터 수신한다.
c. 브라우저가 HTML 파일을 해석한다. (parsing파싱)
- 브라우저는 기본적으로 렌더링(번역) 엔진이 있는데 이는 서버가 클라이언트에게 준 HTML문서를 렌더링 한다는 것이다.
- 렌더링이 필요한 이유 : 서버로부터 받은 HTML 문서를 JavaScript가 이해할 수 없기 때문에 알아들을 수 있게 해석하는 과정이 필요하다.
d. JavaScript가 알아들을 수 이쓴 방식으로 해석한 내용을 토대로 DOM Tree를 구성한다.!

e. DOM Tree랑 CSSOM Tree를 묶어서 Render Tree를 구성
- css도 마찬가지로 tree형태로 나오기 때문에 2가지를 묶어서 최종적으로 렌더 트리를 만든다.
- 렌더트리란 파싱 후 브라우저에 실제로 렌더링되는 최종 문서 모델을 나타내는 계층 구조이다.

# DOM은 브라우저에 기본적으로 내장되어 있는 API 중 하나이다.
· DOM API가 있다는 것은 DOM API를 통해서 브라우저에 있는 많은 것들을 컨트롤 할 수 있다는 것이다.
· API는 다른 시스템에서 제공하는 기능을 사용할 수 있도록 도와주는 중간자 역할을 한다.

# 브라우저 환경이 아닌 환경에서도 DOM이 돌아갈 수 있을까?
· DOM 환경을 테스트 해볼 수 있는 가장 좋은 방법 : DOM 객체에 가장 최상단에 있는 것을 출력해 보면 된다.
· 브라우저일 경우 : 개발자도구 console창에 document 입력 후 확인
· vscode일 경우 : node 환경에서는 정의되지 않았다. / document is not defined

# 런타임(런타임 환경)
· JavaScript에는 런타임 환경이 2가지가 있다. (브라우저, node)
· DOM은 브라우저에서 돌아간다.

# ⭐ DOM이 브라우저에 내장되어있기 때문에 우리는 HTML의 내용을 JavaScript로 접근, 제어 할 수 있다.

# 위의 것들을 하려면 모든 DOM 요소가 어떤 형태로 있는지 알아야하고 그 개념은 바로 node이다.
node는 '속성'과 '메서드'를 갖고 있다. / DOM은 node를 갖는다.
· DOM tree가 만들어지면 그 요소 하나하나가 다 node이다. 그리고 이것들은 각자 속성과 메서드를 가지고 있다는 것이다.
· 속성 : 해당 Node 하나의 고유한 속성 / 동사 형태가 아닌 것들 대부분
· 메서드 : 해당 Node를 제어할 수 있는 어떠한 것 / 동사 형태인 것
속성과 메서드 구분 예시

// 아래 코드에서 속성과 메서드를 구분해보세요.
document.getElementById("demo").innerHTML = "Hello World!";

속성 - innerHTML
메서드 - getElementById ~를 가지고 와라

[2] DOM에 접근 및 제어해보기
# DOM 요소들은 모두 위-아래로 계층구조로 되어있다 = 부모 자식 관계가 있다.

# document 관련 api
a. Finding

// 해당 id명을 가진 요소 하나를 반환합니다.
document.getElementById("id명")

// 해당 선택자를 만족하는 요소 하나를 반환합니다.
document.querySelector("선택자")

// 해당 선택자의 자식 요소를 반환합니다.
document.querySelector("선택자").children
document.querySelector("선택자").children[인덱스] // 자식 요소 중 몇 번째

// 해당 선택자의 부모 요소를 반환합니다.
document.querySelector("선택자").parentElement
document.querySelector("선택자").parentNode

// 해당 class명을 가진 요소들을 배열에 담아 인덱스에 맞는 요소를 반환합니다.
document.getElementsByClassName("class명")[인덱스]

// 해당 태그명을 가진 요소들을 배열에 담아 인덱스에 맞는 요소를 반환합니다.
document.getElementsByTagName("태그명")[인덱스]

// 해당 선택자를 만족하는 모든 요소들을 배열에 인덱스에 맞는 요소를 반환합니다.
document.querySelectorAll("선택자명")[인덱스]

// 새로운 노드를 생성합니다.
const div = document.createElement('div');
document.body.append(div);
document.body.append(div);

· getElementById보다 querySelector가 더 많이 쓰인다.
· querySelector는 선택자 앞에 id(#)인지 class(.)인지 지정 필요. 지정하지 않으면 해당 태그를 지정한다!

b. changing

/** property(=속성)을 바로 바꾼다 */

// 수업에서 사용한 예시
document.querySelector('ul').children[1].innerText = "변경할 내용"

// 이 둘은 차이가 있어요!
element.innerHTML = new html content
element.innerText = new text

// style을 바꿔요.
element.style.property = new style

// method를 통해 클래스를 추가해봐요.
element.setAttribute(attribute, value)

// 어랏? 그럼 이런것도 가능??
element.setAttribute("style", "background-color:red;");

// ....
element.style.backgroundColor = "red";

// input 필드의 변신

c. 추가적으로

// createElements 새로운 요소 추가, 생성 후 넣어줘야 함
const para = document.createElement("p");
para.innerText = "This is a paragraph";
document.body.appendChild(para);

// createTextNode(elements는 아니구여, 그냥 글자...)
let textNode = document.createTextNode("Hello World");
document.body.appendChild(textNode);

// write. 조심 또 조심!
document.write("Hello World!");

document.write("<h2>Hello World!</h2><p>Have a nice day!</p>");

// 골로 가는 코드
function myFunction() {
  document.write("Hello World!");
}

// version 01
element.addEventListener("click", myFunction);
function myFunction() {
  document.getElementById("demo").innerHTML = "Hello World";
}

// version 02
element.addEventListener("click", function() {
  document.getElementById("demo").innerHTML = "Hello World";
});

▶ Class

# 클래스 문법은 ES6에서야 도입되었다.

#(기초개념) 클래스와 인스턴스의 일반적 개념
· 클래스 : 상품을 만드는 '설계도' → 어떤 종류의 상품을 만들지, 상품이 가지고 있는 특징(변수 또는 속성과 메서드)은 무엇인지 알 수 있다. / 객체를 만들기 위한 설계도, 방법
· 인스턴스 : 설계도를 보고 만들어진 '실제 상품' / 설계도를 바탕으로 만들어진 실제 객체

# 기본 개념 잡기
(1) Class란?
실습으로 배웠기 때문에 코드 및 주석으로 내용 확인! ↓

// 클래스라는 설계도 만들기
class Person {
  // constructor : 생성자 함수
  // 매개변수 자리( )에 필수로 들어갈 값들을 넣어준다.
  // 사람의 필수 요소 : name, age
  constructor(name, age) {
    // this.name에다가 우리가 외부로부터 받아온 즉, 이 인스턴스로 만들 때 받아온 name을 넣어준다.
    // this는 우리가 만들 실체를 말한다.
    // 이렇게하면 외부로부터 받아온 정보를 가지고 실체를 만들게 되는 것이다.
    this.name = name;
    this.age = age;
  }
  // 설계도를 만들때 특성에는 name과 age처럼 명사만 있는것은 아니다.
  // 동사도 있다. ~을 한다. ← 메서드 형태로 동사를 표현한다.
  // 이렇게 작성해놓으면 sayHello가 Person이 만들어질 때 항상 같이 만들어진다.
  sayHello() {
    // 현재 인스턴스가 가지고 있는 이름에 접근할 수 있다.
    // console.log(this.name + "님 안녕하세요");
    // 백틱으로 표현
    console.log(`${this.name}님 안녕하세요`);
  }

  // 내 나이는 ~살이에요! 라고 출력하는 메서드 만들기
  myAge() {
    console.log(`내 나이는 ${this.age}살이에요!`);
  }
}

// 설계도를 통해 인스턴스를(실제 사물) 만들어보기
// new 다음에는 해당 설계도의 이름을 써줘야한다!
// 설계도 다음 괄호안에는 생성자 함수의 매개변수에 적어줬던 값을 적어줘야 한다.
// 해석 : 이름은 leehanbyeol이고 나이는 27살인 사람 하나를 만들어줘! (설계도에 근거해서)
const person1 = new Person("leehanbyeol", "27");
const person2 = new Person("홍길동", "99");

// 클래스가 필요한 이유 : 객체를 정확하고 빠르게 많이 만들어 낼 수 있기 때문이다.

// 인스턴스를 만들때 sayHello를 따로 작성하지 않아도 작동이 가능하다.
person1.sayHello();
person2.sayHello();
person1.myAge();
person2.myAge();

(2) 추가로 작성해보는 class 활용 실습 ↓

// 클래스 연습해보기

// [요구사항]
// 1. Car 라는 새로운 클래스를 만들되, 처음 객체를 만들 때는 다음 네 개의 값이 필수로 입력되어야 한다.
// (1) modelName
// (2) modelYear
// (3) type
// (4) price
// 2. makeNoise() 메서드를 만들어 클락션을 출력해주세요.
// 2-1. 해당 자동차가 몇년도 모델인지 출력하는 메서드 작성!
// 3. 이후 자동차를 3개 정도 만들어주세요. (객체 생성)

class Car {
  constructor(modelName, modelYear, type, price) {
    this.modelName = modelName;
    this.modelYear = modelYear;
    this.type = type;
    this.price = price;
  }
  makeNoise() {
    console.log(`${this.modelName}는 빵빵~!하고 소리를 내고 달리고 있어요!`);
  }
  YearOfTheYear() {
    console.log(`${this.modelName}은 ${this.modelYear}년도 모델이에요!`);
  }
}

const car1 = new Car("캐스퍼", "2022", "경형suv", "19,600,000");
const car2 = new Car("쏘나타 하이브리드", "2024", "세단", "40,340,000");
const car3 = new Car("셀토스", "2024", "소형suv", "28,850,000");
const car4 = new Car("지프 랭글러", "2023", "중형suv", "77,100,000");

car1.makeNoise();
car2.makeNoise();
car3.makeNoise();
car4.makeNoise();

car1.YearOfTheYear();
car2.YearOfTheYear();
car3.YearOfTheYear();
car4.YearOfTheYear();

(3) Getter와 Setter

// Getters Setters
// 객체지향 프로그래밍 언어 -> G, S에 대한 기본적인 개념들이 있다.
// 클래스를 통해 객체(인스턴스)를 만든다.
// 객체(인스턴스)안에는 property가 있는데 이는 constructor에서 정의한다.
// constructor에서 정의한 필수 입력값을 통해서 클래스를 만들 때 new 키워드를 통해서 만든다
// new Class(a, b, c) 형식으로 만들고 이렇게 만든 property에 접근하고 세팅하기 위한 Getters Setters를 제공한다.

// 물론 직접 값을 할당하고 가져오는 방법도 있지만
// Getters Setters를 사용하면 훨씬 더 간편하고 안전하게 값을 가져오고 세팅할 수 있다.

class Rectangle {
  constructor(height, width) {
    // 무한루프를 방지하기 위해 getters랑 setter를 쓸 때 약속을 정한다!
    // this에 접근하는 property의 이름은 항상 언더스코어(_)를 붙여주기로 약속.
    // underscore : private(은밀하고, 감춰야 할 때)
    // this._height라는 말은 해당 인스턴스 내에서만 쓰이기 위한 변수로서 분리해놓겠다는 의미이다.
    // Getters Setters라는 개념이 들어오면서 변수 이름을 동일하게 작성해버리면 겹치는 현상이 생기게 되면서
    // 언더스코어(_)로 분리시켜야 되는 필요성이 생겼다.
    this._height = height;
    this._width = width;
  }

  // 엄밀히 말하면 setter를 할 때 함수 형태로 할 수 있다는 뜻
  // 들어오는 value 값에 검증도 가능하다.
  // set width(value)와 set height(value)에 있는 value는 매개변수로 들어오는 것이기 때문에 이름이 같아도 다른 값이다.

  // 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") {
      // 이 때 그냥 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;
    }
    // 이 때 아래 구문을 아래에 써주면 조건문 통과 후 다시 set 해주기 위해 위쪽으로 올라가고
    // 내려오고 올라가고를 반복하는 무한루프에 빠지게 된다.
    this._height = value;
  }

  // gerArea : 가로*세로로 넓이를 구하는 메서드 추가
  getArea() {
    const a = this._width * this._height;
    console.log(`해당 정사각형의 넓이는 ${a}입니다!`);
  }
}

// instance 생성
const rect1 = new Rectangle(10, 20);
// const rect2 = new Rectangle(10, 30);
// const rect3 = new Rectangle(15, 20);

rect1.getArea();

Getters와 Setters를 상단의 Car class 실습 예제에 적용해보기 ↓

// Getters와 Setters를 03.js 파일의 Car class 실습 예제에 적용해보기

// [추가 요구사항]
// 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;
  }
  makeNoise() {
    console.log(`${this._modelName}는 빵빵~!하고 소리를 내고 달리고 있어요!`);
  }
  YearOfTheYear() {
    console.log(`${this.modelName}은 ${this._modelYear}년도 모델이에요!`);
  }

  // Getters Setters 적용 부분

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

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

  // type
  get type() {
    return this._type;
  }
  set type(value) {
    // 유효성 검사
    if (value.length <= 0) {
      console.log("[오류] 모델 타입이 입력되지 않았습니다. 확인해주세요.");
      return;
    } else if (value !== "g" && value !== "e" && value !== "d") {
      // g(가솔린), d(디젤), e(일렉트로닉)이 아닌 경우 오류
      console.log("[오류] 올바른 차량 type을 작성해주세요.");
      return;
    }
    this._type = value;
  }

  // price
  get price() {
    return this._price;
  }
  set price(value) {
    // 유효성 검사
    if (typeof value !== "number") {
      console.log("[오류] 가격으로 입력된 값이 숫자가 아닙니다.");
      return;
    } else if (value <= "1000000") {
      // 최소 가격을 100만원으로 두고 조건문 검사
      console.log("[오류] 최소 100만원 이상의 가격을 작성해주세요.");
      return;
    }
    this._price = value;
  }

  // carInfor() {
  //   console.log(
  //     `지금 소개해드릴 차의 이름은 ${this._modelName}이고 출시년도는 ${this._modelYear}이며, 차종은 ${this._type}, 가격은 최대 ${this._price}원 입니다. 감사합니다!`
  //   );
  // }
}

const car1 = new Car("캐스퍼", "2022", "경형suv", 19600000);
const car2 = new Car("쏘나타 하이브리드", "2024", "e", 40340000);
const car3 = new Car("셀토스", "2024", "e", 28850000);
const car4 = new Car("지프 랭글러", "2023", "d", 77100000);

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

// setter 예시 / 검증을 위한 로직
// car1.modelName = "kia";
car1.modelName = 1;
console.log(car1.modelName);

· if문 작성시 조건에 맞지 않는 경우 해당 문을 빠져나오기 위해 return; 잘 작성해주기!
· 언더스코어(_)붙여야할 부분에 잘 붙여주기!

(4) 상속 (Inheritance)
· 상속은 Class에 기능을 유산으로 내려주는 주요 기능!
· 부모 ↔ 자식 관계라는 뜻
· 부모가 가지고 있는 형질인 상속적인 것들을 밑으로 내려주는 것이 훨씬 더 효과적이라는 개념

기존에 만들었던 Car클래스를 상속시키는 예시 ↓

// 클래스 연습해보기

// [요구사항]
// 1. Car 라는 새로운 클래스를 만들되, 처음 객체를 만들 때는 다음 네 개의 값이 필수로 입력되어야 한다.
// (1) modelName
// (2) modelYear
// (3) type
// (4) price
// 2. makeNoise() 메서드를 만들어 클락션을 출력해주세요.
// 2-1. 해당 자동차가 몇년도 모델인지 출력하는 메서드 작성!
// 3. 이후 자동차를 3개 정도 만들어주세요. (객체 생성)

// [추가 요구사항]
// 1. 전기차 클래스 <= Car 클래스의 상속을 받을 것이다.

class Car {
  constructor(modelName, modelYear, type, price) {
    this.modelName = modelName;
    this.modelYear = modelYear;
    this.type = type;
    this.price = price;
  }
  makeNoise() {
    console.log(`${this.modelName}는 빵빵~!하고 소리를 내고 달리고 있어요!`);
  }
  YearOfTheYear() {
    console.log(`${this.modelName}는 ${this.modelYear}년도 모델이에요!`);
  }
}

// 전기차 클래스 정의
class ElectronicCar extends Car {
  // 현재 우리는 특정한 type인 전기차를 만들 것이기 때문에 Car class의 type이 필요가 없다. type은 항상 e
  // 그렇기 때문에 constructor를 재정의 할 것.
  constructor(modelName, modelYear, price, chargeTime) {
    // Car(부모 class)에게도 알려줘야 함. 그래야 ElectronicCar class 생성시 부모 class 기반으로 만들어진다.
    // = super 키워드 : constructor 키워드와 똑같은 것인데 이것은 현재 ElectronicCar의 constructor가 아니고
    //                  부모의 constructor입니다. 그래서 인자가 4개 들어가야한다. 고정값(전기차 'e')은 바로 지정해준다.
    super(modelName, modelYear, "e", price);
    this._chargeTime = chargeTime;
  }

  get chargeTime() {
    return this._chargeTime;
  }

  set chargeTime(value) {
    if (value <= 0) {
      console.log("[오류] 충전 시간은 최소 1초 이상이여야 합니다.");
      return;
    } else if (typeof value !== "number") {
      console.log("[오류] 충전 시간은 숫자로 작성되어야 합니다.");
      return;
    }
    this._chargeTime = value;
  }
}

const eleCar1 = new ElectronicCar("테슬라", "2023", 90000000, 60);
eleCar1.makeNoise();
eleCar1.YearOfTheYear();

console.log("-----------------------------");

// getter
console.log(
  `${eleCar1.modelName}의 충전 시간은 ${eleCar1.chargeTime}분 입니다.`
);
// setter
eleCar1.chargeTime = 100;
console.log(
  `${eleCar1.modelName}의 충전 시간은 ${eleCar1.chargeTime}분 입니다.`
);

(5) Static Method
· Static Method = 정적 메소드
· 기본적으로 클래스는 객체를 만들기 위해 사용한다. → 다량으로, 안전하고, 정확하게
· 그런데 굳이 인스턴스화를 시킬 필요가 없는, 객체를 만들지 않아도 되는 정적인 메서드들도 있다.
· 그런 것들은 static method로서 정의하고 class 자체를 호출하는 식으로 사용하는 방법이 있다.
· 클래스 자체는 일반적인 클래스인데 그 안에 들어있는 method가 static(정적)인 특징을 갖는다.
· 원래는 클래스안에 method를 사용하려면 클래스를 인스턴스화 시킨 다음 인스턴스.method로 접근해야 했다.
· 하지만 static 메소드는 그렇지 않다.

Static Method로 계산기를 만들어보는 예시 코드 ↓

// 계산기
class Calculator {
  // method안에 static이라는 키워드를 작성해준다.
  static add(a, b) {
    console.log("[계산기] 더하기를 진행합니다.");
    return a + b;
  }

  static substract(a, b) {
    console.log("[계산기] 빼기를 진행합니다.");
    return a - b;
  }
}

// 이렇게 바로 작성하고 싶다는 가정!
console.log(Calculator.add(3, 5)); // 8
console.log(Calculator.substract(3, 5)); // -2

▶ Closure

(1) 클로저의 개념
· MDN - A closure is the combination of a function and the lexical environment within which that function was declared = 클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합입니다.
· 함수가 선언된 렉시컬 환경과의 조합 → 우리가 계속 집어본 개념 outer, scope chaining
· call stack이 있다고 했을 때 각 실행 컨텍스트에는 record와 outer가 하나씩 있다고 하였고 이것들이 왜 외부에 있는 값만을 참조 할 수 있는지 그 기준이 바로 outer에 있다고 했다. 그 때 그 outer에 담긴 환경이 바로 함수가 선언된 렉시컬 환경이다.

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

// 클로저 (Closure)

// 아래 코드에서 console.log에는 어떤 값이 찍힐지 고민해보기

const x = 1; // 전역스코프

function outerFunc() {
  // 여기서의 x는 위의 x와는 scope가 다르다.
  const x = 10; //outerFunc() 내의 스코프
  function innerFunc() {
    // x는 어디서 참조할까요????
    // 제일 처음에는 innerFunc() 내부에서 찾는다.
    // 내부 스코프에는 x가 없어서 바깥으로 시선을 돌린다 => outer =>
    // => innerFunc()가 '호출'될 때가 아닌 '선언' 될 때의 렉시컬 환경(실행될 당시의 변수 정보 등이 들어있다.)
    // scope Chaining이 안에서부터 시작해서 밖으로 한 단계, 한 단계 나아갈 수 있는 이유가 이런 것에 있다.

    // ⭐ 함수가 선언된 렉시컬 환경 → 함수가 선언될 당시의 외부 변수 등의 정보!

    console.log(x); // 10
  }

  innerFunc();
}

outerFunc();

· 예시 2 ↓

const x = 1;

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

// outerFunc() 내에 innerFunc()가 '호출' 되고 있음에도 불구하고 '선언'은 밖에서 됐기 때문에 scope를 공유하지 않는다.

// [렉시컬 스코프]의 개념
// JavaScript 엔진은 함수를 어디서 '호출' 했는지가 아니라 어디에 '정의' 했는지에 따라서
// 상위 scope 또는 그냥 scope를 결정한다.

// 외부 렉시컬 환경에 대한 참조값 → outer
// 함수 정의가 평가되는 시점에 결정된다.

function outerFunc() {
  const x = 10;
  innerFunc(); // 1
}

// innerFunc()랑 outerFunc()는 각각 다른 scope를 가지고 있다!!!!!
function innerFunc() {
  console.log(x); // 1
}

outerFunc();

# 클로저와 렉시컬 환경
· 외부 함수보다 중첩 함수가 더 오래 유지되는 경우, 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 (여전히) 참조할 수 있다. ← 이 개념에서 중첩 함수가 바로 클로저

· 클로저 예시 ↓

// 클로저와 렉시컬 환경(LexicalEnvironment)
// 외부 함수보다 중첩 함수가 더 오래 유지되는 경우, 중첩함수는 종료됐는데 여전히 참조한다.

const x = 1;

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

// outer 함수를 '실행'해서, innerFunc에 담는다..!
// → outer함수의 return 부분을 innerFunc에 담는다는 뜻

// ▶ 현재 call stack 상황 : 전역 - outer

const innerFunc = outer();
// --------------------- outer 함수의 실행 컨텍스트는 여기서 없어진다.

// ▶ 현재 call stack 상황 : 전역 

// outer 컨텍스트는 생겨 있었지만 하는 일이 innerFunc에다가 return값인 inner를 담고 끝이기 때문에 여기서부터 사라지고
// 남아있는 innerFunc()에 인해서 inner라는 실행 컨텍스트가 안으로 들어오게 된다. 
// 그런데 innerFunc안에는 x라는 변수가 console.log문 안에 있고 x라는 변수는 inner 밖인 outer에 있다.
// 그렇기 때문에 원래는 inner 컨텍스트가 실행되는 시점에는 outer 컨텍스트가 사라져서 outer 안의 x라는 변수를 
// 참조할 수 없어야 정상이지만 여전히 x=10이라는 값을 출력할 수 있다. → 이것이 Closure의 개념!!!!!
 
innerFunc();
// ▶ 현재 call stack 상황 : 전역 - inner

→ 해당 예시 : 중첩함수 = inner, 외부함수 = outer

# 클로저가 어떻게 가능한가요? (feat. 똑똑한 가비지 컬렉터)
· 참조 카운트가 0인 요소는 가비지 컬렉터가 다 수거해 가면서 메모리 공간을 확보한다.
· 하지만 가비지 컬렉터는 똑똑하기 때문에 안쓰이고 참조되지 않는 것만 가져간다.
· 위의 예시에서는 outer를 inner에서 참조할 환경이 있는 것을 알고 유지해 둔 것.

(2) 클로저의 활용
# 클로저는 JavaScript의 강력한 기능으로, 필요하다면 적극 활용해야 한다.
# 클로저는 주로 '상태를 안전하게 변경하고 유지하기 위해 사용' 한다.
# 상태를 안전하게 은닉한다(특정 함수에게만 상태 변경을 허용한다) 는 표현 기억!!

· 클로저 활용 예시 ↓ (3번의 코드가 점점 closure에 맞게 수정될 것이기 때문에 이어서 봐야함)

<1번 예시>

// 클로저의 활용

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

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

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

console.log(increase());
// 은닉시켜 놓지 않았기 때문에 이런 외부에서 적용되는 값이 들어와버리면 값이 변경되어 버리는 치명적인 단점이 있어요.
// num = 100;
console.log(increase());
console.log(increase());

// 보안해야 할 사항
// 1. 카운트 상태(num 변수의 값)
//    => increase 함수가 호출되기 전까지는 변경되지 않아야 한다.
// 2. 이를 위해서 count 상태는 오직 increase 함수만이 변경할 수 있어야 한다.
// 3. 전역변수 num 요놈이 문제다! → 지역 변수로 변경....?

// 13.js에다가 수정할 것임.

<2번 예시>

// 12.js에서의 문제 해결 방법으로 num을 전역 변수에서 지역 변수로 변경 - 실패

// 카운트 상태 변경 함수 #2
const increase = function () {
  // 카운트 상태 변수
  let num = 0;

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

// 이전 상태값을 유지 못함
// increase를 호출 할 때마다 num을 0으로 초기화 시키고 1+를 해봤자 매번 1으로 return된다.
console.log(increase()); //1
console.log(increase()); //1
console.log(increase()); //1

// [최종 리뷰]
// 1. num 변수를 increase 함수의 지역 변수로 선언했다. → 바깥에서 변경할 수 없도록 방지 O
//   = num 변수는 오직 increase 함수만이 변경할 수 있었음
// 2. 하지만, increase() 가 호출될 때마다 num이 초기화되는 이상한 코드
// 3. 백번, 천번, 만번 호출해도 출력값은 언제나 1
// * 의도치 않은 변경은 방지하면서 + 이전 상태를 유지해야 한다.
// 12.js와 13.js를 합쳐놓은 뭔가가 필요한데
// 이것을 할 수 있는 것이 바로 ⭐closure이다!!!!!
// 클로저는 외부 환경의 변수를 참조할 수 있고 그렇기 때문에 아무데서나 전역 변수처럼
// 접근할 수도 없고 내부에 있는 지역 변수도 아니기 때문에 호출 시마다 초기화 되지도 않는다!

<3번 예시>

// 카운트 상태 변경 함수 #3

// increase의 모습을 보면 뒤에 ()가 열고 닫힌 것을 볼 수 있다. - 즉시 실행 함수
// → increase 함수 자체는 그 안의 내용을 수행한!!! 것이다.

// closure를 접할 때에는 괄호를 열고 닫는 모습이 많이 나오기 때문에 어색하지만 주의깊게 봐야함!
// → 이런 모습이 보이면 closure 인가? 하고 생각하는 습관 필요!

const increase = (function () {
  // 카운트 상태 변수
  let num = 0;

  // 클로저
  // return안의 함수를 뱉어낸다.
  // = increase 자체는 첫(바깥쪽) return에 있는 함수 자체이다.
  // 그래서 두번째(안쪽) return에 있는 num은 이미 외부에 있는 num을 참조하고 있는 것
  return function () {
    return ++num;
  };
})();

// 이전 상태값을 유지
// increase()를 실행하게 되면 function () {return ++num;}; 이 실행되는 것이고
// 이때 num은 외부에서 참조하기 때문에 가비지 컬렉터가 가져가지 않는다.
console.log(increase()); //1
console.log(increase()); //2
console.log(increase()); //3

// [최종 코드 설명]
// 1. 위 코드가 실행되면, '즉시 실행 함수'가 호출! → 함수가 반환된다.(이전 예시의 inner) → increase 변수에 할당
// 2. increase에 할당된 함수는 자신이 정의된 위치에 의해서 결정된 상위 스코프의 즉시 실행 함수의
//    '렉시컬 환경'을 기억하는 클로저 → let num=0;을 기억한다.
// 3. 즉시 실행함수는 즉시 소멸(이전 예시에서 outer 함수가 불리자마자 바로 call stack에서 popup된 것과 비슷)

// * [결론]
// closure를 사용함으로써 num은 초기화 되지 않았다.
// 외부에서 접근할 수 없는 은닉된 값을 얻게 되었다.
// 의도되지 않은 변경도 걱정할 필요가 없다.
// → num이라는 값은 무조건 increase에서만 변경할 수 있기 때문에!
profile
코린한별

0개의 댓글