-
[Javascript] Class(클래스) 심화Front-End(Web)/Javascript 2021. 2. 24. 04:20반응형
클래스를 포스팅하려고 했다가, 프로토타입을 먼저 공부하고 돌아온 참이다!! 🤒🤒
처음엔 클래스를 공부하는데 '프로토타입 기반' 이란 말이 자꾸 반복되길래 혼동이 오던 참이었는데!
이제는 무언가 감을 잡은 기분이다 ㅎㅎ!! 자바스크립트의 클래스는... 클래스지만 클래스가 아니다!! (읭???)
📒 Class 기본
1. Prototype 기반 언어(프로그래밍)
자바스크립트는 프로토타입 기반 언어이다. 이는, Java와 같은 클래스 기반 언어와 결을 달리 한다는 의미이다.
둘 모두 객체 지향 프로그래밍(OOP, Object Oriented Programming) 에 목적을 둔 문법이지만 그 방법에 있어 약간의 차이가 있다.
클래스 기반은 클래스라는 개체(일종의 틀)를 만든다. 그리고, 이 틀로 객체들을 만들어내며 이 관계를 '상속'이라고 한다.
프로토타입 기반은 조금 다르다. 프로토타입 객체라고 하는 원본 객체를 만든다.
그리고, 객체는 이와 연결(링크)되어서 만들어지고 참조되며, 이는 '상속' 을 모방하는 동작에 가깝다.
자바스크립트는 일종의 '객체에서 객체를 만드는' 프로토타입 기반 언어인 것이다. 자세한 내용은 아래 포스팅을 참고해달라!
* 포스팅(링크) : Javascript - 프로토타입(abangpa1ace.tistory.com/104)
2. 객체지향(OOP, Object-Oriented Programming)
객체지향 프로그래밍이란, 프로그램을 객체(Object)들로 구성하고 서로 상호작용을 통해 구현하는 방법을 의미한다.
객체에 변수나 함수를 저장한 뒤 이를 반복적으로 사용하고자 하는 개념으로, 이 객체를 편리하게 생성하는 템플릿이 바로 Class인 것이다.
3. Class in Javascript
그렇다면, 자바스크립트에서 클래스는 어떠한 의의를 가질까?
ES5 문법까지는, 자바스크립트에 클래스가 없었다. 객체 지향 프로그래밍을 위해 생성자 함수(Function) 과 프로토타입을 통해 클래스를 모방하는 로직만 있었을 뿐이다.
ES6 버전부터 자바스크립트는 Class를 도입했다. 하지만, 그렇다고 자바스크립트가 클래스 기반 언어가 된 것은 아니다!
가장 큰 차이는, 자바스크립트의 클래스는 "특별한 함수" 인 것이다. 이는 즉, 클래스는 자바스크립트에서 객체인 것이다.
클래스는 다른 클래스 기반 언어처럼 형식을 위한 개체가 아닌, 프로토타입에 비해 명료하게 클래스를 만들기 위한 일종의 템플릿인 것이다.
즉, 기존의 프로토타입 기반 패턴에 클래스 스러운 문법이 추가된 것으로, 이렇게 쉽게 읽기 위해 추가된 문법을 '문법적 설탕(Syntactic Sugar)' 라고 부른다.
4. Class 기본 문법
class User { // 클래스명 constructor(name) { // 생성자 함수 this.name = name; } sayHi() { // 메소드(함수) alert(this.name); } } let user = new User("John"); // 인스턴스 user.sayHi();
1) 클래스명 : User 에 해당하는 부분. 표현식의 경우 변수명으로 대체될 수 있다. (클래스명은 반드시 대문자로 시작, CammelCase)
2) constructor(생성자) 함수 : constructor(name) { this.name = name; } 에 해당하는 부분.
클래스 내 함수(메서드)에서 사용할 인자(argument)들을 설정하는 부분이다.
왼쪽은 this(클래스로 생성된 인스턴스)의 'name' property를 의미, 오른쪽은 인스턴스에서 입력받을 생성자값을 의미한다.
3) Method(메서드) : sayHi() { alert(this.name); } 에 해당하는 부분. 생성자를 활용한 다양한 기능을 구현하는 함수들을 의미한다.
4) Instance(인스턴스) : let user = new User('John') 에 해당하는 부분. 클래스를 통해 생성된 객체를 의미한다.
'user' 가 객체명이 되고, new 클래스명(키워드) 로 선언할 수 있다. ('John' 키워드 -> constructor 함수 -> 'name' property에 할당)
📒 Class 문법
1. 클래스 정의
class 키워드로 정의한다. 클래스는 함수이므로 동일하게 class 선언과 class 표현식 두 가지 문법으로 클래스를 정의할 수 있다.
- Class 선언
class Rectangle { constructor(height, width) { this.height = height; this.width = width; } }
class 키워드와 클래스명, 그리고 {}(중괄호)로 묶인 본문으로 클래스 선언문을 작성할 수 있다.
클래스가 함수와 다른 점은 호이스팅이 되지 않는다는 것이다. 즉, 클래스는 정의를 해준 뒤에 참조가 가능하다는 것이다.
* 호이스팅이 완전히 발생하지 않는 것은 아니다. 호이스팅은 var, let, const, function, function*(제너레이터), class 선언문에 적용된다.
다만, class의 호이스팅은 let, const 와 유사하다고 한다.
- Class 표현식
Class 표현식은 이름을 가질수도 있고, 갖지 않을수도 있다. 클래스 이름이 있다면, 이는 클래스 본문의 local scope에서 유효하다.
// unnamed let Rectangle = class { constructor(height, width) { this.height = height; this.width = width; } }; console.log(Rectangle.name); // 출력: "Rectangle" // named let Rectangle = class Rectangle2 { constructor(height, width) { this.height = height; this.width = width; } }; console.log(Rectangle.name); // 출력: "Rectangle2"
2. Class body 와 메서드 정의
Class body(본문)은 중괄호로 묶여 있는 안쪽 부분이다. 이곳에선, 메서드(클래스의 함수)나 constructor와 같은 class 멤버를 정의한다.
클래스 본문은 strict mode 에서 실행된다.('use strict') * 참조링크 : MDN 공식문서 strict mode
즉, 본문의 코드는 성능향상을 위해 엄격한 문법이 적용되며, 어길 시 오류가 발생한다.
- Constructor(생성자)
constructor() 메서드는 class로 생성된 new 객체(인스턴스) 생성 및 클래스 필드 초기화를 위한 특수한 메서드이다.
- constructor() 메서드는 클래스 당 1개만 가질 수 있음
- 생성자를 지정하지 않으면 기본 생성자를 생성해줌 (constructor() { })
- super() 을 통해 부모 클래스의 생성자를 호출할 수 있음
class Person { constructor(name) { // this는 클래스가 생성할 인스턴스를 가리킨다. // _name은 클래스 필드이다. this._name = name; } } // 인스턴스 생성 const me = new Person('Lee'); console.log(me); // Person {_name: "Lee"}
- 프로토타입 메서드
객체에서 발생되어, 참조를 통해 사용되는 함수를 메서드라고 한다. 클래스 내에선 아래와 같이 메서드를 설정할 수 있다. (const 불필요)
class Rectangle { constructor(height, width) { this.height = height; this.width = width; } // 메서드: Getter get area() { return this.calcArea(); } // 메서드: 일반 메서드 calcArea() { return this.height * this.width; } } const square = new Rectangle(10, 10); console.log(square.area); // 100
- 정석 메서드와 속성
Static Method(정적 메서드)는 클래스의 인스턴스화 없이 호출이 가능하다. 또한, 클래스 인스턴스는 정적 메서드를 사용할 수 없다.
클래스 내에서, static 키워드를 통해 생성할 수 있다.
class Point { constructor(x, y) { this.x = x; this.y = y; } // 정적 메서드 static displayName = "Point"; static distance(a, b) { const dx = a.x - b.x; const dy = a.y - b.y; return Math.hypot(dx, dy); } } const p1 = new Point(5, 5); const p2 = new Point(10, 10); p1.displayName; // undefined p1.distance; // undefined p2.displayName; // undefined p2.distance; // undefined console.log(Point.displayName); // "Point" console.log(Point.distance(p1, p2)); // 7.0710678118654755
정적 메서드는 인스턴스로 호출할 수 없다. 이는 즉, 정적 메서드는 this를 사용할 수 없다는 것이다. (this는 클래스 인스턴스를 가리킴)
다시 말하면, 메서드 내부에서 this를 사용할 필요가 없는 메서드는 정적 메서드로 만들 수 있다. (전역 유틸리티 함수, 고정값 등)
var Foo = (function () { // 생성자 함수 function Foo(prop) { this.prop = prop; } Foo.staticMethod = function () { return 'staticMethod'; }; Foo.prototype.prototypeMethod = function () { return this.prop; }; return Foo; }()); var foo = new Foo(123); console.log(foo.prototypeMethod()); // 123 console.log(Foo.staticMethod()); // staticMethod console.log(foo.staticMethod()); // Uncaught TypeError: foo.staticMethod is not a function
코드와 프로토타입 체인을 정리해보았다.
함수 객체만이 가지는 prototype 프로퍼티는 함수 객체가 생성자로 사용될 때, 이 함수로 생성된 객체의 부모 프로토타입 객체를 가리킨다.
Foo() 생성자 함수 역시, prototype 프로퍼티로 Foo.prototype 객체를 가리키는 것이다. (이것이, foo 객체의 부모 [[Prototype]])
또한, Foo.prototype 객체의 constructor() 함수는 Foo() 생성자 함수와 연계되어 있다.
정적 메서드인 staticMethod() 는 Foo() 생성자 함수의 메서드이기 때문에, foo 객체가 호출할 방법이 없다.
일반 메서드인 prototypeMethod() 는 Foo.prototype 프로토타입 객체의 메서드이기 때문에, foo 객체가 참조할 수 있는 것이다.
3. Getter, Setter
getter, setter는 클래스의 프로토타입 속성이며, 클래스 인스턴스에 상속된다. 클래스에 새로운 기능이 아닌, 접근자 프로퍼티이다.
- Getter
getter는 클래스 필드에 접근할 때마다 클래스 필드값을 조작하는 행위가 필요할 때 사용한다. get 키워드를 사용해 정의한다.
getter는 호출보다는, 어떠한 반환값을 취득하기 위해 사용되므로 반드시 return 값이 필요하다.
class Foo { constructor(arr = []) { this._arr = arr; } // getter: get 키워드 뒤에 오는 메소드 이름 firstElem은 클래스 필드 이름처럼 사용된다. get firstElem() { // getter는 반드시 무언가를 반환해야 한다. return this._arr.length ? this._arr[0] : null; } } const foo = new Foo([1, 2]); console.log(foo.firstElem); // 1
- Setter
setter는 클래스 필드에 값을 할당할 때마다 클래스 필드값을 조작하는 행위가 필요할 떄때 사용한다. set 키워드를 사용해 정의한다.
setter 역시 호출보다는, 필드에 어떠한 값을 할당하기 위해 사용되는 프로퍼티다.
class Foo { constructor(arr = []) { this._arr = arr; } // getter: get 키워드 뒤에 오는 메소드 이름 firstElem은 클래스 필드 이름처럼 사용된다. get firstElem() { // getter는 반드시 무언가를 반환하여야 한다. return this._arr.length ? this._arr[0] : null; } // setter: set 키워드 뒤에 오는 메소드 이름 firstElem은 클래스 필드 이름처럼 사용된다. set addElem(elem) { // ...this._arr은 this._arr를 개별 요소로 분리한다 this._arr = [elem, ...this._arr]; } } const foo = new Foo([1, 2]); // 클래스 필드 addElem에 값을 할당하면 setter가 호출된다. foo.addElem = 100; console.log(foo.firstElem); // getter -> 100
4. Class Field
- Public Field
기존에는 클래스 본문에는 메서드만 선언할 수 있었다. 클래스 필드를 본문에 선언하면 에러가 발생하므로, 필드선언과 초기화는 constructor() 에서 실시한다.
(최신 브라우저 혹은 최신 Node.js 에선 정상 동작한다. 이는, Field Declarations를 최신 환경이 구현했기 때문)
class Rectangle { height = 0; width; constructor(height, width) { this.height = height; this.width = width; } }
- Private Field
클래스 바깥에서 private 필드는 접근할 수 없다. 이는, 클래스 내부에서만 읽고 쓰기가 가능하다. #문법을 사용한다.
class Rectangle { #height = 0; #width; constructor(height, width) { this.#height = height; this.#width = width; } }
5. 클래스 상속(Class Inheritance)
클래스 상속은 코드 재활용 측면에서 매우 유용하다. 새로운 클래스가 기존 클래스와 유사하다면, 상속을 통해 다른 점만 구현하면 된다.
- extends
extends 키워드는 부모 클래스를 상속받는 자식 클래스로 정의할 때 사용된다.
class Vehicle{ foo(){ console.log("Vehicle"); } } // Sub class class Car extends Vehicle{ // extends 상속 foo(){ console.log("Car"); } } var car = new Car(); car.say(); // "Car"
- super
super 키워드는 자식 클래스가 부모 클래스를 참조하거나, 부모 클래스의 constructor() 를 호출할 때 사용된다.
- super 키워드를 통해, 부모의 속성 필드를 삭제할 수 없다.
- super.prop 를 통해 non-writable인 속성을 덮어쓸 수 없다.
- 자식클래스의 생성자에 필수적으로 super()을 먼저 호출해야 한다. 이전엔, this를 사용할 수 없다.
// Base Class class Vehicle{ constructor(){ console.log("Vehicle constructor"); } } // Sub class class Car extends Vehicle{ constructor(){ // console.log(this); // error! super() 를 먼저 호출해야한다. super(); console.log("Car constructor"); console.log(this); } } var car = new Car(); // "Car constructor" // Car {}
- Static 메서드 상속, Prototype 메서드 상속
자식 클래스는, 부모 클래스의 static 메서드도 상속받아 사용할 수 있다. (상속보다는 프로토타입 체인을 타고 참조하는 것)
즉, 자식 클래스의 정적 메서드 내부에서도, 부모 클래스의 정적 메서드를 super를 통해 호출할 수 있다.
하지만, 자식 클래스의 일반 메서드 내부에서는, 부모 클래스의 정적 메서드를 super를 통해 호출할 수 없다.
이는, 자식 클래스의 인스턴스는 프로토타입 체인을 타기 때문에(Child.prototype), 부모 클래스의 정적 메서드에 접근이 불가한 것이다.
공부를 하면서 뭔가 익숙하다 싶었더니... 이전에 클래스에 대해서 간단하게 정리한 적이 있었다!
허탈함보다는, 그만큼 내가 제대로 이해하고 있지 않았다는 사실에 다시 공부하게 된 것이 무척 뿌듯했다.
이전에 비해 포스팅 질도 올라갔을 뿐만 아니라, 프로토타입에 대해선 별도로 공부하고 시작하다보니 더 수월하게 클래스가 이해됬다.
완벽하게 정리된 건 아니지만, 이를 기반으로 Typescript의 클래스는 또 어떻게 다른지 공부해봐야겠다.
[출처]
- MDN 공식문서 Classes : developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Classes
- PointmaWeb : poiemaweb.com/es6-class
- heecheolman 님의 블로그 : heecheolman.tistory.com/29
- 블로그 포스팅 : Javascript 클래스 기본 abangpa1ace.tistory.com/32
반응형'Front-End(Web) > Javascript' 카테고리의 다른 글
[Javascript] 객체의 복사 (깊은 복사, 얕은 복사) (0) 2021.03.12 [Javascript] Infinite Scroll (Height 속성) (0) 2021.03.07 [Javascript] Prototype(프로토타입) (0) 2021.02.24 [Javascript] 비동기 - #3. Async & Await (0) 2021.02.14 [Javascript] 비동기 - #2. Promise (0) 2021.02.01