티스토리 뷰
JS 버전별 문법의 차이와 치환 방법
💡 현재 ES6를 지원하지 않는 인터넷 익스플로러는 종료되었고, 대부분의 브라우저에서는 자바스크립트 ES6를 지원한다. 그런만큼 ES6부터 등장한 기능들이 많이 사용되고 있다. 대표적으로는 class, promise, 화살표 함수, 스프레드 연산자 등이 있다. 그런데 만약 ES6가 지원되지 않는 환경에서 개발해야할 때, 어떻게 하면 이러한 개념들을 하위 버전으로 치환할 수 있을까? 저번 주제에서 비동기 처리 패턴 역사에 대해 고민하면서 promise와 async-await이 등장하기까지의 과정을 다뤘기 때문에 이번에는 class 문법에 집중해보려고 한다. class를 적용한 코드를 하위 버전으로 치환해야할 경우 어떻게 작성해야 할지에 대해 알아보도록 하겠다.
class ?
- 자바스크립트는 프로토타입 기반 객체지향 언어이다. 따라서 클래스가 없어도 생성자 함수와 프로토타입을 통해 객체지향 언어의 상속을 구현할 수 있다.
- 하지만 자바나 C#과 같은 클래스 기반 객체 지향 언어와 방식이 다르기 때문에 다른 객체지향 언어를 먼저 접한 개발자들은 자바스크립트의 학습과 적용에 어려움을 느낄 수 있다.
- 이를 해결하기 위해 ES6에서는 클래스 문법이 등장하게 되었다. 하지만 이 문법의 등장이 새로운 객체지향 모델의 등장으로 이어지는 것은 아니다.
- 클래스는 함수이며, 프로토타입 기반 패턴을 클래스 기반 패턴처럼 사용할 수 있도록 하는 syntactic sugar 가 되는 것이다.
- 다만 생성자 함수에서 제공하지 않는 기능들을 클래스가 제공하기도 한다. 예를 들어 extends나 super 가 있다.
- 이러한 기능들은 기존에 프로토타입 체인을 통해 상속받는 개념이 아니라, 클래스를 상속받아 새로운 클래스를 확장하여 정의할 수 있게 만든다.
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 프로토 타입 메서드
speak() {
console.log(`${this.name}: hello!`);
}
// 정적 메서드
static speak() {
console.log(`hello!`);
}
}
// 인스턴스 생성
const jay = new Person("jay", 100);
console.log(jay.name); // jay
console.log(jay.age); // 100
jay.speak(); // jay: hello!
- class는 연관이 있는 데이터를 한 곳에 묶어 놓는 문법으로, fields(속성)와 methods(행동)가 묶여있다.
- class안에서 내부적으로 보이는 변수와 밖에서 보일 수 있는 변수를 나눠서 캡슐화하며 상속, 다형성들의 특징을 갖는데 이것은 객체지향언어의 특징이기도 하다.
- class 자체에는 데이터가 들어있지 않으며 template만 정의해놓고 한번만 선언한다.
- class를 이용해서 새로운 instance를 생성하면 object가 된다.
생성자 함수
클래스 문법 도입 전에는 function을 이용해 template을 만들고, 이를 통해 object를 만들 수 있었다.
// ES5 생성자 함수
var Person = (function () {
// 생성자 함수
function Person(name, age) {
this.name = name;
this.age = age;
}
// 프로토타입 메서드
Person.prototype.speak = function () {
console.log(this.name + ': hello!');
};
// 정적 메서드
Person.speak = function () {
console.log('hello!');
};
// 생성자 함수 반환
return Person;
}());
// 인스턴스 생성
var jay = new Person('jay', 100);
jay.speak();
// ES5만 가능한 것
var speakObj = new Person.prototype.speak();
var staticSpeakObj = new Person.speak();
- 클래스와 생성자 함수의 정의 방식을 비교해보면 형태가 매우 유사한 것을 알 수 있다.
- 추가적으로 ES5에서 프로토타입 메서드와 정적 메서드는 new 연산자와 함께 생성자 함수로 사용할 수 있지만, ES6에서는 생성자 함수로 사용할 수 없다는 점을 유의하자.
클래스의 상속 방법
// 클래스의 상속 예제
class Shape {
constructor(width, height) {
this.width = width;
this.height = height;
}
getArea() {
return width * this.height;
};
}
class Rectangle extends Shape { }
class Triangle extends Shape {
getArea() {
return (this.width * this.height) / 2;
}
}
const rectangle = new Rectangle(20, 20);
console.log(rectangle.getArea()); // 400
const triangle = new Triangle(20, 20);
console.log(triangle.getArea()); // 200
- class에서는 extends를 이용해 다른 클래스를 연결하여 연장할 수 있다.
- extends 후 필요한 함수만 재정의해서 사용할 수도 있다.(오버라이딩)
- 함수를 재정의해서 사용하게 되면 원래 extends한 클래스의 함수는 불러와지지 않는다.
- 필요하다면 super. 을 앞에 붙여서 불러오는 방법이 있다.
class의 상속기능을 생성자 함수로 치환해볼까?
- 생성자 함수는 클래스와 같이 상속을 통해 다른 생성자 함수를 확장할 수 있는 문법이 제공되지 않는다.
- 따라서 클래스의 상속을 흉내내는 방법에는 여러가지가 있는데...
- 1. 인스턴스를 생성한 후 프로퍼티를 일일이 지운 후 더는 새로운 프로퍼티를 추가할 수 없게 하는 방법
var extendClass = function (SuperClass, SubClass, subMethods) {
// SubClass.prototype에 SuperClass의 인스턴스 할당
SubClass.prototype = new SuperClass();
// SubClass의 프로퍼티를 일일이 지운다.
for (var prop in SubClass.prototype) {
if (SubClass.prototype.hasOwnProperty(prop)) {
delete SubClass.prototype[prop]
}
}
if (subMethods) {
for (var method in submethods) {
SubClass.prototype[method] = subMethods[method];
}
}
// subClass.prototype에 속성을 추가, 제거, 변경 못하도록 함
Object.freeze(Subclass.prototype);
return SubClass;
};
var Rectangle = extendClass(Shape, function (width, height) {
Shape.call(this, width, height);
});
- 2. 서브 클래스의 prototype에 super class의 인스턴스를 직접 할당하지 않고 프로퍼티를 생성하지 않는 빈 생성자 함수(bridge)를 만든다.
→ 빈 생성자 함수의 prototype이 수퍼 클래스의 prototype을 바라보게 한다.
→ 서브 클래스의 prototype에는 빈 생성자 함수의 인스턴스를 할당하게 하는 방법
var Shape = function (width, height) {
this.width = width;
this.height = height;
};
Shape.prototype.getArea = function () {
return this.width * this.height;
};
var Rectangle = function (width) {
Rectangle.call(this, width, height)
};
var Bridge = function () { }; // 빈 생성자 함수
Bridge.prototype = Shape.prototype;
Shape.prototype = new Bridge();
Object.freeze(Rectangle.prototype);
- 위의 방법들보다 더 안전한 방법이 있는데 바로 의사 클래스 상속(pseudo classical inheritance) 패턴을 사용하는 것이다.
이를 사용하면 클래스 기반 언어처럼 상속으로 클래스를 확장하는 모양새를 만들 수 있다.
// ES5에서 의사 클래스 상속 패턴 (pseudo classical inheritance)을 써보자
var Shape = (function () {
function Shape(width, height) {
this.width = width;
this.height = height;
}
Shape.prototype.getArea = function () {
return width * this.height;
}
return Shape;
}());
// Shape 생성자 함수를 상속하여 확장한 Rectangle 함수
var Rectangle = (function () {
function Rectangle() {
// Shape 생성자 함수에게 this와 인수를 전달하면서 호출
Shape.apply(this, arguments);
}
// Rectangle.prototype을 Shape.prototype을 프로토타입으로 갖는 객체로 교체
Rectangle.prototype = Object.create(Shape.prototype);
// Rectangle.prototype.constructor가 Shape를 바라보고 있으므로 Rectangle로 교체
Rectangle.prototype.constructor = Rectangle;
return Rectangle;
}());
var Rectangle = new Shape(20, 20);
console.log(Rectangle); // Rectangle{width: 20, height:20}
console.log(Rectangle.getArea()) // 400
- apply() 메서드는 this와 인수들의 단일 배열을 받아 함수를 호출한다.
- Object.create는 객체를 만들되 생성자는 실행하지 않는다.
- 즉 Rectangle prototype의 proto가 Shape의 prototype을 바라보되 Shape의 인스턴스가 되지 않게 된다.
super
마지막 관문이다. class 문법에서 제공하는 상위 클래스 접근 수단인 super 키워드를 생성자 함수에서 구현해보자.
// ES6 이전에서 상위 클래스를 접근하는 방법
var extendClass = function (SuperClass, SubClass, subMethods) {
// SubClass.prototype을 SuperClass.prototype을 프로토타입으로 갖는 객체로 교체
SubClass.prototype = Object.create(SuperClass.prototype);
// SubClass.prototype.constructor가 SuperClass가 아니라 SubClass를 바라보도록 교체
SubClass.prototype.constructor = SubClass;
// super의 동작을 해줄 함수
SubClass.prototype.super = function (propName) {
var self = this;
// 인자가 비어있을 경우 SuperClass의 생성자 함수에 접근한다.
if (!propName) return function () {
SuperClass.apply(self, arguments);
}
// SuperClass의 prototype 속 propName에 해당하는 값
var prop = SuperClass.prototype[propName];
// prop이 함수가 아닌 경우 그대로 반환
if (typeof prop !== 'function') {
return prop;
}
return function () {
// prop이 함수일 경우 클로저를 활용해 메서드에 접근
return prop.apply(this, arguments);
}
};
// SubClass에 추가할 메서드들을 SubClass prototype에 담기도록 객체로 전달
if (subMethods) {
for (var method in subMethods) {
SubClass.prototype[method] = subMethods[method];
}
Object.freeze(SubClass.prototype);
// SubClass의 prototype을 변경할 수 없게 함
return SubClass;
};
var Shape = function (width, height) {
this.width = width;
this.height = height
};
Shape.prototype.getArea = function () {
return this.width * this.height;
}
var Triangle = extendClass{
Shape, // SuperClass
function(width, height){
this.super()(width, height);
},
function (width, height) {
getArea: function() { // SuperClass인 Shape의 getArea 메서드를 확장
console.log((this.super('getArea')()) / 2);
}
};
var semo = new Triangle(10);
semo.getArea(); // 50
console.log(semo.super('getArea')()); // 100 -> SuperClass의 메서드가 실행됨
}
마치며
클래스 문법을 생성자 함수로 치환해보면서 느낀 점은... ES6+를 사용할 수 있어서 다행이다는 것이다...ㅋㅋㅋ
자바스크립트는 프로토타입 체인을 기반으로 상속을 구현하기 때문에 자바스크립트의 클래스를 공부하다보면 프로토타입과 체이닝, 클로저에 대해서도 깊은 이해가 필요하다.
어떠한 최신 문법을 하위 문법으로 치환하려면 자바스크립트의 동작 원리를 생각보다 더더욱 깊게 이해해야한다는 것을 새삼 깨닫게 되었다.
잘 만들어진 라이브러리와 최신 문법을 사용하는 것은 좋지만, 이러한 것들을 사용 못할 경우 어떻게 해야 할 것인가에 대해 생각해보는 것이 문제 해결 능력을 키우는데 큰 도움이 된다는 것을 알게 되었다.
자바스크립트 언어는 배울수록 어렵고, 자바스크립트 공부를 게을리 하면 안된다는 것을 상기시켜주는 주제였다.
'지식 한 올' 카테고리의 다른 글
우리가 Vite를 사용하는 이유: 번들링에 대해 알아보기 (0) | 2025.02.06 |
---|---|
브라우저의 렌더링 과정과 React/Vue의 가상돔 (1) | 2024.07.30 |
함수형 프로그래밍과 자바스크립트 (1) (0) | 2024.06.26 |
프로그래밍 언어의 종류와 특성들을 알아보자 (인터프리터 언어 VS 컴파일 언어) (0) | 2024.06.01 |
자바스크립트의 비동기 처리 패턴 역사와 진화 (0) | 2024.05.20 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- 모듈
- 스파게티코드
- webpack
- await
- gitmoji
- 셀레니움
- 일급객체
- React
- 자바스크립트
- 비동기패턴
- async
- E2E테스트
- 일급함수
- 번들링
- 개발
- 인턴
- 함수형프로그래밍
- Vue
- HMR
- 가상돔
- 레거시코드
- 번들러
- 프론트엔드
- 데이터검증
- 응집도
- 결합도
- 코드리뷰
- virtualdom
- 비동기처리
- 모듈화
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
글 보관함