-
자바스크립트(JS)의 클래스CS 2024. 8. 16. 01:00
JavaScript로 개발을 하다 보면, 비슷한 기능을 가진 객체를 여러 번 생성해야 하는 경우가 많다. 이때마다 매번 동일한 코드를 반복해서 작성하는 것은 번거롭고 오류 발생 가능성도 높이는 문제점들이 생긴다. JavaScript 클래스는 이러한 문제를 해결하기 위함이다. 클래스를 사용하면 객체를 생성하기 위한 틀을 만들고, 이를 바탕으로 다양한 객체를 생성할 수 있다.
이러한 JavaScript의 클래스에 대해서 자세하게 살펴보도록 하자.
❓ 클래스(Class) ❓
JavaScript의 클래스는 객체를 생성하기 위한 템플릿이다.
클래스를 사용하면 객체를 쉽게 만들고 관리할 수 있다.
ES6 이전에는 비슷한 종류의 객체를 많이 만들어내기 위해 생성자 함수를 사용했다.
클래스는 이 생성자 함수의 역할을 더 직관적이고 명확하게 수행할 수 있도록 해준다.
이 클래스라는 개념이 추가되었다고 하지만, 그렇다고 클래스 기반의 언어가 된 것은 아니다.
🧐 왜 클래스 개념이 추가됐을까? 🧐
처음 JavaScript 클래스를 듣고 생각한 것은 "왜 프로토타입 기반의 언어에 클래스의 개념이 추가된 것일까"였다.
프로토타입 기반의 언어에서 객체를 생성하고 상속하는 방식은 코드의 가독성과 유지보수 측면에서 다소 복잡하다.
ES6에서부터 추가된 이 클래스라는 개념은 JavaScript를 조금 더 Java처럼 객체지향적으로 표현하기 위해 추가된 문법인데, 객체지향 언어에 익숙한 개발자들이 직관적인 코드 작성을 하는데 도움을 주고, 간결한 코드 작성을 도와준다,.
🚨 중요 🚨
다만, 여기서 중요한 것은 생김새만 클래스 구조이고, 엔진 내부적으로는 프로토타입 방식으로 작동된다는 점을 알고 가야한다.⭐️ 프로토타입 문법과 클래스 문법 비교⭐️
ES6 이전 프로토타입 문법
// 생성자 function Pet({name, age}) { this.name = name; this.age = age; } Pet.prototype.say = function() { return `나는 ${this.name}(이)야. 내 나이는 ${this.age}살이야.`; }; const pet = new Pet({name: '꽁이', age: 3}); console.log(pet.say()); // 나는 꽁이(이)야. 내 나이는 3살이야.
ES6 클래스 문법
// 클래스 class Pet { // 이전에서 사용하던 생성자 함수는 클래스 안에 'constructor'이라는 이름으로 정의 constructor({name, age}) { // name과 age는 프로퍼티 // 생성자: 인스턴스 생성 시, 가장 먼저 자동 호출 this.name = name; this.age = age; // this는 본인 객체를 의미. 클래스 내에서 메소드끼리 소통하기 위함 } // 객체에서 메소드를 정의할 때 사용하던 문법을 그대로 사용하면, 메소드가 자동으로 'Pet.prototype'에 저장 say() { // 메소드 정의 return `나는 ${this.name}(이)야. 내 나이는 ${this.age}살이야.`; } } const pet = new Pet({name: '꽁이', age: 3}); // 인스턴스 생성 console.log(pet.say()) // 나는 꽁이(이)야. 내 나이는 3살이야.
프로토타입 문법과 클래스 문법의 차이는 위 두 코드만 봐도 알 수 있을 것이다.
프로토타입 문법의 경우 생성자 함수와 프로토타입 체인을 통해 메소드를 정의했다.
클래스 문법에서는 생성자와 메소드를 클래스 내부에 함께 정의하기에, 확실히 프로토타입 문법보다는 코드가 더 간결하고 가독성이 좋다.
📌 클래스 문법의 특징 📌
문법적 설탕(Syntactic Sugar)
클래스는 사실 JavaScript의 기존 프로토타입 기반 상속을 감싸는 문법적 설탕이다.
이는 즉, 내부적으로는 여전히 프로토타입을 사용하지만, 코드 작성과 이해가 더 쉽도록 문법적으로 간소화한 것이다.
이로 인해 클래스 문법을 사용하는 개발자는 프로토타입을 직접 다루지 않아도 객체지향적인 코드를 작성할 수 있다.
단일 생성자(Constructor)
클래스는 하나의 'constructor' 메소드를 가진다.
이 생성자는 new 키워드를 사용하여 클래스로부터 객체를 생성할 때 자동으로 호출된다.
'constructor' 메소드는 클래스의 인스턴스를 초기화하고, 초기 속성을 설정하는 역할을 하는데
하나의 클래스마다 하나의 constructor 메소드만 존재할 수 있고, 2개 이상이 될 경우 오류가 발생한다.
클래스는 함수
JavaScript에서 클래스는 특별한 종류의 함수이며, 클래스 선언은 함수 선언과 유사하게 동작한다.
클래스도 호이스팅이 되지만, 함수 선언과는 달리 클래스는 선언 이전에 사용할 수 없다.
클래스 내부에 생성자 함수는 함수이므로, 함수 선언문으로 정의된 경우 함수 호이스팅이 발생하고, 함수 표현식으로 정의된 경우 변수 호이스팅이 발생한다. 하지만 클래스 자체는 호이스팅이 되지 않는다.
엄격 모드(Strict Mode)
클래스 내의 모든 코드는 자동으로 엄격모드로 실행된다. 이는 실수를 방지하고 더 안전한 코드를 작성할 수 있도록 도와준다.
엄격 모드는 해제가 불가능하며, 변수 선언시 var 키워드 생략 금지, 중복된 매개변수 이름 금지 등의 제약을 의미한다.
내부적으로 [[IsCalssConstructor]] 속성을 가짐
클래스는 일반 함수와 구분되는 특별한 [[IsCalssConstructor]] 내부 속성을 가진다.
이는 클래스를 new 키워드 없이 호출할 수 없도록 보장한다. 그러므로 new 키워드가 없이 클래스를 호출하려하면 클래스에서는 에러를 반환한다.
📌 클래스의 기본 구성 요소 📌
클래스 선언
// 클래스 class Pet { weight = 3; // 인스턴스 변수 constructor(name, age) { // name과 age는 프로퍼티 // 생성자: 인스턴스 생성 시, 가장 먼저 자동 호출 this.name = name; this.age = age; // this는 본인 객체를 의미. 클래스 내에서 메소드끼리 소통하기 위함 } } const pet = new Pet('꽁이', 3); // 인스턴스 생성 console.log(pet.name); // 꽁이 console.log(pet.age); // 3 console.log(pet.weight); // 3
클래스를 정의하는 기본 문법으로, 클래스 이름은 대문자로 시작해야 한다.
생성자 메소드
// 클래스 class Pet { constructor(name, age) { // name과 age는 프로퍼티 // 생성자: 인스턴스 생성 시, 가장 먼저 자동 호출 this.name = name; this.age = age; // this는 본인 객체를 의미. 클래스 내에서 메소드끼리 소통하기 위함 } }
이 'constructor' 메소드는 클래스를 통해 객체를 생성할 때 호출되는 메소드로, 인스턴스를 생성하고 클래스 필드를 초기화하기 위한 메소드이다.
반드시 각 클래스 안에 1개만 존재할 수 있고, 2개가 존재할 경우 에러가 발생한다.
constructor 내부에서 클래스 필드의 선언과 초기화가 실시되는데, 'constructor' 내부에 선언한 클래스 필드는 클래스가 생성할 인스턴스에 바인딩된다.
여기 이 클래스 필드는 인스턴스의 프로퍼티가 되며, 인스턴스를 통해 클래스 외부에서 언제나 참조될 수 있다.
클래스 필드와 초기화
// 클래스 class Pet { name = '꽁이'; constructor(age) { this.age = age; } } const pet = new Pet(3); console.log(pet.name); // '꽁이' console.log(pet.age); // 3
클래스 필드는 클래스 선언 시 바로 정의할 수 있다. 이럴 경우 'constructor'에서 초기화할 필요가 없다.
메소드
// 클래스 class Pet { constructor(name, age) { // name과 age는 프로퍼티 // 생성자: 인스턴스 생성 시, 가장 먼저 자동 호출 this.name = name; this.age = age; // this는 본인 객체를 의미. 클래스 내에서 메소드끼리 소통하기 위함 } } const pet = new Pet('꽁이', 3); // 인스턴스 생성 console.log(pet.name); // 꽁이 console.log(pet.age); // 3
클래스 내부에 정의된 함수는 메소드라고 부르는데, 이는 인스턴스에 의해 호출된다.
이는 즉, 객체에 행동을 부여하는 역할을 한다.
정적 프로퍼티와 메소드
class MathUtils { static pi = 3.14159; // 클래스 자체에 속하는 프로퍼티 static areaOfCircle(radius) { // 클래스 자체에서 호출되는 정적 메소드 return MathUtils.pi * radius * radius; } } console.log(MathUtils.pi); // 3.14159 console.log(MathUtils.areaOfCircle(5)); // 78.53975
'static' 키워드를 사용하여 정적 프로퍼티와 메소드를 정의할 수 있는데, 이렇게 정의된 메소드는 클래스 자체에서 호출되며, 클래스의 인스턴스에는 접근할 수 없다.
Private
클래스에서는 '#'을 사용해서 private 필드와 메소드를 정의할 수 있다. 이들은 클래스 외부에서는 접근할 수 없다는 특징이 있다.
// 클래스 class Pet { #name; constructor(name) { this.#name = name; } #hashSay() { return `내 이름은 ${this.#name}(이)야.`; } getSay() { return this.#hashSay(); } } const pet = new Pet('꽁이'); console.log(pet.getSay()); // `내 이름은 꽁이(이)야.` console.log(pet.#name); // SyntaxError
클래스와 모듈
클래스는 JavaScript 모듈과 함께 사용될 때, import와 export를 구문을 사용해서 다른 파일에서 클래스를 불러와 사용할 수 있다.
// Pet.js class Pet { constructor(name, age) { this.name = name; this.age = age; } } // app.js import { Pet } from './Pet.js'; const pet = new Pet('꽁이', 3); // 인스턴스 생성 console.log(pet.name); // 꽁이 console.log(pet.age); // 3
Getter와 Setter
class Student { constructor(name) { this._name = name; } get name() { return this._name; } set name(newName) { if (newName.length > 0) { this._name = newName; } else { console.log('Name cannot be empty'); } } }
'get'과 'set' 키워드를 사용하여 클래스의 속성 값을 안전하게 접근하고 수정할 수 있도록 한다.
getter는 속성 값을 반환하고, setter는 속성 값을 설정할 때 추가 로직을 적용할 수 있다.
상속
// 클래스 class Pet { // 이전에서 사용하던 생성자 함수는 클래스 안에 'constructor'이라는 이름으로 정의 constructor(name, age) { // name과 age는 프로퍼티 // 생성자: 인스턴스 생성 시, 가장 먼저 자동 호출 this.name = name; this.age = age; // this는 본인 객체를 의미. 클래스 내에서 메소드끼리 소통하기 위함 } // 객체에서 메소드를 정의할 때 사용하던 문법을 그대로 사용하면, 메소드가 자동으로 'Pet.prototype'에 저장 say() { // 메소드 정의 return `나는 ${this.name}(이)야. 내 나이는 ${this.age}살이야.`; } } class Dog extends Pet { say() { super.say(); 부모 클래스의 메소드 호출 console.log(`${this.name}`(이)야.); } } const kkong = new Dog('꽁이', 3); // 인스턴스 생성 console.log(kkong.say()) // 나는 꽁이(이)야.
JavaScript는 단일 상속만을 지원한다. 'extends' 키워드를 사용하여 한 클래스가 다른 클래스를 상속받을 수 있다.
상속을 통해 코드 재사용성을 높이고, 부모 클래스의 기능을 자식 클래스에서 사용할 수 있다.
상속받은 클래스는 부모 클래스의 메소드를 오버라이드(재정의)하거나, 'super' 키워드를 통해 부모 클래스의 생성자와 메소드에 접근할 수 있다.
'super'는 자식 클래스의 생성자에서 부모 클래스의 생성자를 호출할 때 사용된다. 만약 super을 사용하지 않으면 오류가 난다.
프로퍼티 어트리뷰트 [[Enumerable]]
클래스 constructor, 프로토타입 메소드, 정적 메소드 모두 프로퍼티 어트리뷰트 [[Enumerable]]의 값이 false이다.
이 뜻은 열거형이 아니라는 의미로, for ... in 루프나 Object.keys() 등으로 열거될 수 없다.
이는 클래스의 내부 구현에 대한 노출을 방지하고, 객체지향 프로그래밍의 캡슐화 원칙을 따르기 위함이다.
✅ 클래스 표현식 ✅
클래스도 함수처럼 다른 표현식 내부에서 정의, 전달, 반환, 할당이 가능하다.
let Pet = class { say() { alert("멍멍"); } }
이렇게 클래스 표현식을 만들 수 있는데, 기명 함수 표현식과 유사하게 클래스 표현식에도 이름을 붙일 수 있다.
let Pet = class MyClass { say() { alert(MyClass); // MyClass라는 이름은 오직 클래스 안에서만 사용할 수 있습니다. } }; new Pet.say(); // 원하는대로 MyClass의 정의를 보여준다. alert(MyClass); // Error
위처럼 클래스 표현식에 이름을 붙이면, 해당 이름은 오로지 클래스 내부에서만 사용이 가능하다.
또한 클래스를 동적으로 생성하는 것도 가능하다.
function myClass(name) { return class { say() { alert(name); }; }; } let Pet = myClass("꽁이"); new Pet().say(); // 꽁이
🚨 클래스 사용 시 주의할 점 🚨
'this' 키워드의 문맥
클래스 메소드 내부에서 'this'는 기본적으로 해당 메소드가 속한 클래스 인스턴스를 가리킨다.
하지만, 메소드를 콜백 함수로 전달하거나 비동기 코드에서 사용할 때 'this'의 문맥이 달라질 수 있다.
이러한 문제를 해결하기 위해 'bind()' 메소드를 사용하거나, 화살표 함수를 사용하는 것이 일반적이다.
화살표 함수는 'this'를 자신이 선언된 클래스 문맥으로 유지한다.
// 클래스 class Pet { // 이전에서 사용하던 생성자 함수는 클래스 안에 'constructor'이라는 이름으로 정의 constructor(name, age) { // name과 age는 프로퍼티 // 생성자: 인스턴스 생성 시, 가장 먼저 자동 호출 this.name = name; this.age = age; // this는 본인 객체를 의미. 클래스 내에서 메소드끼리 소통하기 위함 } // 객체에서 메소드를 정의할 때 사용하던 문법을 그대로 사용하면, 메소드가 자동으로 'Pet.prototype'에 저장 say() { // 메소드 정의 return `나는 ${this.name}(이)야. 내 나이는 ${this.age}살이야.`; } delayedSay() { setTimeout(() => { this.say(); // 화살표 함수 사용으로 'this'가 클래스 문맥을 유지 }, 1000); } } const pet = new Pet('꽁이', 3); // 인스턴스 생성 console.log(pet.say()) // 나는 꽁이(이)야. 내 나이는 3살이야.
상속 시 'super' 호출 순서
상속받은 클래스에서 'constructor'를 사용할 때, 'super()'를 먼저 호출해야 this를 사용할 수 있다.
// 클래스 class Pet { constructor(name) { this.name = name; } } class Dog extends Pet { constructor(name, age) { super(name); // 반드시 먼저 호출 this.age = age; } }
⭐️ 결론 ⭐️
JavaScript의 클래스 문법은 프로토타입 기반으 상속 방식을 개선하여, 더 직관적이고 간결한 코드 작성을 가능하게 한다.
상황에 따라 클래스와 프로토타입 중 더 적절한 방법을 선택하여 사용하는 것이 중요하다.
'CS' 카테고리의 다른 글
SEO(Search Engine Optimization, 검색 엔진 최적화) (0) 2024.08.18 Webpack vs Vite (0) 2024.08.16 자바스크립트(JS)의 prototype (0) 2024.08.09 자바스크립트(JS)의 Closure (1) 2024.08.09 자바스크립트(JS)의 this (0) 2024.08.08