본문 바로가기
기타 대외활동/투두몰 서포터즈

[구글 앱스 스크립트] 4주차 첫 번째 : 자바스크립트 클래스와 객체

by bri9htstar 2023. 7. 19.

드디어 올 것이 왔다.

클래스와 객체.

파이썬 배우면서도 벅차서 억지로 우겨넣었던 그 놈의 객체 지향 개념.

이번에 제대로 배워보자 …

 

이너피스 유지하고 들어갑시다


메서드

먼저 객체는 뭘까? 객체란 다음과 같이 속성과 값이 조합된 집합이다.

 

{속성 1: 값 1, 속성 2: 값 2, …}

// 객체 속성에 접근하는 법
객체.속성

 

함수는 함수 리터럴로 표현함으로써 변수에 대입할 수 있었던 것처럼, 객체의 값으로 함수를 갖도록 할 수 있다. 객체 요소로 함수를 갖는 경우에는 그 요소를 속성이라 부르지 않고 메서드라 부른다. 즉 다음과 같은 요소가 객체에 포함되어 있다면 이를 메서드라고 부른다.

 

메서드 : function(파라미터 1, 파라미터 2, …) {
  // 처리
}

// 메서드를 호출하는 법
객체.메서드(인수 1, 인수 2, …)

 

다음 예제를 보자. 객체 greeting에 sayHello라는 메서드를 만들고 이를 호출한다.

 

function myFunction() {
  const greeting = {
    sayHello: function() {
      return 'Hello!';
    }
  };

  console.log(greeting.sayHello()); //Hello!
}

 

객체는 '정보'로서의 속성뿐만 아니라 메서드라는 형태로 함수, 즉 '기능'을 가질 수 있다. 그리고 속성과 메서드를 합쳐 객체의 멤버라 부른다.

 

메서드 대입과 추가

속성과 마찬가지로 메서드도 대입할 수 있다. 객체에 존재하지 않는 메서드를 대입하면 해당 메서드가 추가된다.

 

객체.메서드 = function (파라미터 1, 파라미터 2, …) {
  // 처리
}

 

위의 예제에서 객체 greeting에 다른 메서드 sayGoodBye를 추가한 처리를 한 것이다.

 

function myFunction() {
  const greeting = {
    sayHello: function() {
      return 'Hello!';
    }
  };
  
  greeting.sayGoodBye = function() {
  	return 'Good bye.';
  };

  console.log(greeting.sayHello()); //Hello!
  console.log(greeting.sayGoodBye()); //Good bye.
}

 

실행하면 로그에는 Good bye. 도 출력되어, 추가한 메서드 sayGoodBye도 동작한다.

 

실행 결과

 

메서드 정의

객체에 메서드를 추가할 때 기존 함수 리터럴을 사용하면 코드양이 크게 증가하기 때문에 읽기 어려워진다. 그래서 객체 안의 메서드 정의를 간략화해서 기술하는 메서드 정의 구문을 제공한다. 메서드 정의를 이용해 메서드를 객체의 요소로 할 때는 객체 안에서 다음과 같이 입력한다.

 

메서드(파라미터 1, 파라미터 2, …) {
  // 처리
}

 

객체 greeting 예제를 메서드 정의로 다시 정리해보면 다음과 같다.

 

function myFunction() {
  const greeting = {
    sayHello() {
      return 'Hello!';
    }
  };

  console.log(greeting.sayHello()); //Hello!
}

 

메서드 정의는 기존 객체에 대한 메서드 추가에는 사용할 수 없다.

 

클래스

모든 객체의 속성과 메서드를 빠짐없이 기록하다보면 스크립트가 늘어나고 내용도 장황해진다. 그리고 객체 구조나 메서드 내용이 변경되면 모든 객체를 수정해야 한다는 단점이 있다.

그래서 자바스크립트에서는 객체의 '모형'을 기반으로 같은 속성이나 메서드를 가진 개별 객체를 생성하는 구조를 제공한다.

 

  • 클래스 : 객체의 특성을 정의하는 모형
  • 인스턴스화 : 클래스를 기반으로 객체를 생성하는 것
  • 인스턴스(또는 객체) : 인스턴스화를 통해 생성된 객체

 

클래스로부터 객체를 생성하면 같은 객체를 여러 차례 기술할 필요가 없어진다. 또 스크립트를 간결하게 할 수 있고 … 더보기 … 여튼 여러모로 스크립트 가독성, 유지보수성, 안전성 등 여러 면에서 장점 밖에 없다. 왜 안 써?

 

클래스 정의와 인스턴스화

이미 클래스가 정의되어 있고 그 클래스를 인스턴스화할 때는 다음과 같이 new 연산자를 사용한다.

 

new 클래스명(인수 1, 인수 2, …)

 

지정된 클래스 정의에 기반해 생성된 인스턴스가 반환값으로 반환된다. 한편 해당 클래스를 정의할 때는 class문을 이용한다.

 

class 클래스명 {
  // 클래스 정의
}

 

클래스명은 일반적으로 그 내용을 나타내는 알파벳을 이용한 단어를 파스칼 표기법(첫 번째 문자를 대문자)으로 나타낸다. 이어서 중괄호 안에 속성 및 메서드를 정하는 코드를 입력한다. new 연산자를 이용해 인스턴스를 만들기 전에 class문을 이용한 클래스 정의가 실행되어야 하므로 기술 순서나 위치에 주의한다.

 

function myFunction() {
  
  class Person {

  }

  const p = new Person();
  console.log(p); //{}
}

 

클래스 정의가 비어있어 Person 클래스의 경우에는 로그에는 {}만 출력된다. 이는 생성된 인스턴스 p가 '빈 객체'임을 나타낸다. 클래스 정의에서 아무 것도 하지 않으면 빈 객체가 만들어진다.

 

컨스트럭터

클래스에서 생성된 인스턴스는 객체이므로 속성이나 메서드를 가질 수 있다.

클래스에 속성을 정의할 때는 컨스트럭터라는 특별한 함수를 사용한다. 컨스트럭터란 클래스를 인스턴스로 만들 때 가장 먼저 호출되는 함수이다. 그래서 컨스트럭터 처리에 속성 정의를 포함시키면 인스턴스에 속성을 가지도록 할 수 있다. 컨스트럭터를 정의할 때는 class문 안에 constructor라는 이름의 메서드를 정의한다. 이를 constructor 메서드라 부른다.

 

constructor(파라미터 1, 파라미터 2, …) {
  // 처리
}

 

constructor 메서드는 new 연산자를 이용한 인스턴스 생성할 때 호출된다.

 

컨스트럭터를 이용한 속성 정의

 

this 키워드

보통 객체에 속성을 정의할 때는 속성에 대입문을 이용한다. 점 표기법을 이용한다면 객체.속성 = 값과 같은 스테이트먼트말이다. 대상이 되는 객체를 컨스트럭터 안에서 어떻게 표현할까? '이 클래스로부터' 앞으로 생성될 것이기 때문에 현재는 변수명이나 상수명이 할당되지 않는 상태이다.

이 때 필요한 것이 this 키워드이다. 컨스터럭터 안에 this라고 작성하면 '이 클래스로부터' 생성된 인스턴스임을 의미한다. 그렇기 때문에 컨스트럭터 안에 다음과 같이 작성하면 해당 클래스로부터 생성되는 인스턴스의 속성에 값을 가지도록 할 수 있다.

 

this.속성 = 값

 

그냥 백 마디 설명보다 예제 한 번 보면 이해가 된다.

 

function myFunction() {
  
  class Person {
    constructor(name, age) {
      this.name = name;
      this.age = age;
    }
  }

  const p1 = new Person('bri9htstar', 23);
  console.log(p1); //{ name: 'bri9htstar', age: 23 }

  const p2 = new Person('todomall', 32);
  console.log(p2); //{ name: 'todomall', age: 32 }
}

 

인스턴스 멤버 변경

클래스에서 생성한 인스턴스는 객체이므로 각각 멤버값을 변경하거나 멤버를 추가할 수 있다.

 

function myFunction() {
  
  class Person {
    constructor(name, age) {
      this.name = name;
      this.age = age;
    }
  }

  const p = new Person('bri9htstar', 23);
  p.age += 5;
  p.job = 'researcher';
  console.log(p); //{ name: 'bri9htstar', age: 28, job: 'researhcer' }   
}

 

age 속성값에 5를 더하고 job 속성을 새롭게 추가했다. 쉽게 추가하고 쉽게 수정할 수 있다.

자바스크립트의 인스턴스는 각각 메버를 변경할 수 있다. 그러므로 클래스에서 생성된 인스턴스가 항상 같은 멤버로 구성된다고 할 수 없다. 인스턴스를 생성한 뒤 해당 멤버 구성을 변경하는 것은 스크립트 가독성을 손상하기도 하므로 주의.

 

내가 이전에 배웠던 스터디에서는 클래스를 이렇게 표현하더라!

클래스는 붕어빵을 찍어내기 위한 틀이라고!

 

 

그리고 인스턴스가 붕어빵? 이라고 했다. 클래스라는 붕어빵 틀을 하나 잘 만들어 놓으면 다양한 객체를 뽑을 수 있는 거다.

속성을 어떻게 정하냐에 따라 팥붕어빵이 되고, 슈크림 붕어빵이 되고, 고구마 붕어빵이 되고, 여러 개가 되는 거다.

이쯤 되면 이해가 됐을까?

 

 

그저 맛있어 보여서 가져온 사진

 

 

메서드 정의

클래스에서 생성한 인스턴스에 메서드를 갖도록 하기 위해서는 클래스 정의에 메서드 정의를 포함시켜야 한다.

 

// @ts-nocheck
function myFunction() {
  
  class Person {
    constructor(name, age) {
      this.name = name;
      this.age = age;
    }

    greet() {
      console.log('Hello! I\'m ${this.name}!');
    }

    isAdult() {
      return this.age >= 18;
    }
  }
  
  
  
  const p = new Person('bri9htstar', 23);
  p.greet(); //Hello! I'm bri9htstar!
  console.log(p.isAdult()); //true
}

 

클래스 Person에 인사말을 로그에 출력하는 greet 메서드와 age 속성이 18살 이상인가를 판정하는 is Adult 메서드를 정의했다. 스크립트를 실행하면 동작을 확인할 수 있다. (내 GAS는 여전히 $ 대입이 안 된다 … 왜지?)

 

프로토타입

그러면 인스턴스에서 속성과 메서드를 가질 때마다 메모리를 계속 확보해야 할까? 이건 낭비 아닌가?

자브스크립트에서는 모든 클래스가 prototype 속성이라는 특별한 역할을 하는 속성을 가진다. 기본적으로 prototype 속성은 빈 객체지만 거기에 멤버를 추가할 수 있다. 그리고 클래스의 prototype 속성에 추가된 멤버는 해당 클래스를 기반으로 생성된 인스턴스에서 참조할 수 있다.

그니까요 이게 뭔 소리냐면

 

 

엄청 불필요하게 계속 인스턴스 수만큼 별도의 메모리를 확보하는 게 아니라,

 

 

요로코롬 프로토타입을 이용해서 실제 인스턴스에는 greet 메서드는 존재하지 않지만 prototype 속성을 참조해서 실행할 수 있는 것이다.

클래스의 prototype 속성에 정의된 메서드를 프로토타입 메서드라 부르며 클래스문 안에 정의된 메서드가 프로토타입 메서드가 된다.

 

한번 아까 위의 코드에서 p.greet(); 부분에서 브레이크 포인트를 설정하고 디버그를 해보자.

 

 

[>]를 통해 클래스나 객체를 전개할 수 있는데, [> Person] → [> prototype]을 보면 constructor 메서드와 greet 메서드가 추가된 것을 알 수 있다. isAdult도 있다. 한편 인스턴스 p를 전개하기 위해 [>p]를 눌러보면 greet 메서드가 존재하지 않음을 알 수 있다.

 

인스턴스 메서드 변경

생성한 인스턴스에 대해 멤버를 변경할 수 있다면, 메서드도 멤버 변경을 할 수 있지 않을까? 맞다.

 

function myFunction() {
  
  class Person {
    constructor(name, age) {
      this.name = name;
      this.age = age;
    }

    greet() {
      console.log('Hello! I\'m ${this.name}!');
    }

    isAdult() {
      return this.age >= 18;
    }
  }
  
  const p = new Person('bri9htstar', 23);
  p.greet = function() {
    console.log('Good Bye! I\'m %s!', this.name);
  };
  p.greet();

  console.log(Person.prototype.greet.toString());
  console.log(p.greet.toString());
}

 

실행 결과

 

예제를 실행하면 처음 로그 출력에서 인스턴스 p에 대한 greet 메서드는 생성 후 대입한 함수에 의한 것임을 알 수 있다. 계속해서 Person의 prototype 속성 안의 greet 메서드인스턴스 p의 greet 메서드의 내용을 출력한다.

아하, 즉 생성한 인스턴스에서 메서드를 변경하는 것은 인스턴스 메서드를 직접 변경하는 거지 클래스의 prototype 속성 자체를 변경하는 것은 아니다. 그리고 인스턴스와 클래스의 prototype 속성에는 같은 이름의 메서드가 각각 존재할 수 있는데, 중복된 경우에는 인스턴스 메서드를 우선 호출한다.

 

 

여기까지 따라오면서 꿈벅꿈벅 거렸다면 당신 … 집중하고 다시 올라가셔야 합니다.

클래스 내용은 점점 끝나가거든요.

 

 

정적 멤버

지금까지 클래스에 정의해온 멤버는 생성한 인스턴스에 사용했다. 하지만 클래스에는 그와 별도로 인스턴스화를 하지 않아도 직접적으로 사용할 수 있는 속성 및 메서드를 정의할 수 있다. 이를 각각 정적 속성 및 정적 메서드라 부른다. 또한 이들을 모아 정적 멤버라 부른다. 정적 속성에 접근하거나 정적 메서드를 호출할 때는 다음 구문을 이용한다. 아니 이럴거면 이제까지 왜 대놓고 만듦?

 

클래스명.속성명
클래스명.메서드명(인수 1, 인수 2, …)

 

클래스에 정적 속성을 추가할 때는 클래스를 정의하는 class문 뒤에 다음과 같이 입력한다.

 

클래스명.속성명 = 값

 

클래스에 메서드를 추가할 때는 클래스를 정의하는 class문 뒤에 다음과 같이 static 키워드를 부여한 메서드 정의를 입력한다.

 

static 메서드명(파라미터 1, 파라미터 2, …) {
  // 처리
}

 

또한 정적 속성과 마찬가지로 다음과 같이 class문 뒤에 함수 리터럴을 대입해서 정적 메서드를 정의할 수도 있다.

 

클래스명.메서드명 = function(파라미터 1, 파라미터 2, …) {
  // 처리
}

 

다음 예시를 살펴보자.

 

function myFunction() {
  
  class Person {
    constructor(name, age) {
      this.name = name;
      this.age = age;
    }
    
    static greet(name) {
      console.log('Hello! I\'m ${name}!');
    }
  }
  
  Person.job = 'researcher';
  
  console.log(Person.job); //researcher
  Person.greet('bri9htstar'); //Hello! bri9htstar!
}

 

클래스 Person에 정적 메서드 greet를 추가했고 (static 키워드 부분),

클래스 Person에 정적 속성 job을 추가했다 (클래스명.속성명 = 값).

 

클래스와 정적 멤버를 글로벌 영역에 정의하면 정적 멤버는 프로젝트 임의 위치에서 이용할 수 있는 속성과 메서드가 된다. 이렇게 사용할 때는 글로별 변수 또는 글로벌 함수와 같다. 하지만 정적 멤버는 클래스명을 지정해 접근하므로 다른 변수와 경쟁을 피할 수 있다는 장점이 있다.

 

프로토타입 메서드 변경

정적 멤버 추가 방법을 응용해 class문을 이용한 정의 뒤에 프로토타입 메서드를 변경할 수 있다.

다음과 같이 prototype 속성 아래의 메서드를 변경한다.

 

클래스명.prototype.메서드명 = function(파라미터 1, 파라미터 2, …) {
  // 처리
}

 

예제로 확인해보자. 계속 같은 예제로 돌아가고 있음을 주의하자.

 

function myFunction() {
  
  class Person {
    constructor(name, age) {
      this.name = name;
      this.age = age;
    }
    
    static greet(name) {
      console.log('Hello! I\'m ${name}!');
    }
  }
  
  Person.prototype.greet = function() {
    console.log('Good Bye! I\'m ${this.name}!');
  };
  
  const p = new Person('bri9htstar', 23);
  p.greet(); // Good Bye! I'm bri9htstar!
  
  console.log(Person.prototype.greet.toString());
  console.log(p.greet.toString());
}

 

실행 결과

 

함수 리터럴을 대입해 Person 클래스의 프로토타입 메서드 greet를 덮어 썼다.

첫 번째 로그 출력은 변경 후의 greet 메서드가 실행되고 있음을 알 수 있다. 이어서 두 번째, 세 번째 로그 출력을 보면 클래스 Person의 prototype 속성 안의 greet 메서드, 인스턴스 p의 greet 메서드의 내용이 같음을 알 수 있다. 즉, 참조하는 프로토타입 메서드가 변경됐음을 알 수 있다.

 


다음에는 자바스크립트의 내장 객체에 대해 배울 거라, 정말정말 책 분량의 9% 정도를 차지할 만큼 크기 때문에 정말정말 GAS 프로그래밍에서 자주 사용되는 것만 블로그에 정리하겠다 ㅠ

내용이 어려워지고, 이제 새로 알게 되는 개념이 많아져서 요약하고 재밌게 전달이 어려워진 점 … 죄송합니다. 나태한 거 아닙니다. 오히려 너무 열심히 해서 어려운 겁니다; 암튼 그런 거임

 

누가 나 좀 살려줬으면 좋겠지만 이제 얼마 안 남았으니 더 열심히 달려가보자. 파이팅.

클래스와 객체 확실히 기억했길 바라며 …

 


위 글은 투두몰 서포터즈 활동의 일환으로 작성된 글입니다.

https://edu.todomall.kr/?utm_source=supporters&utm_medium=contents

 

투두몰 ㅣ 일잘러의 투두리스트를 훔치다

일잘러의 투두리스트를 훔치다! 오피스 툴을 직접 따라하며 배우고, 과제를 통해 결과물을 만들어요.

edu.todomall.kr

 

댓글