
사실 예전에 분명 class를 공부한 부분이 있지만
너무 쉽게 휙 넘어가버려서 매번 class 문제를 마주할 때마다 정신이 혼미하다...
그래서 준비했다. 나를 위한 class 파헤치기!!

class 외에도 이전에 다 공부했지만 쏙쏙 많이도 까먹었더라...
물론 다시 개념 좀 살펴보고 하면 되지만 class는 그것조차 잘 되지 않더라 이 말이다...!
(class 할 때마다 너무 고통스러워요...)
그럼 얘기는 여기까지 하고, 바로 class에 대하여 알아보도록 하겠다.
- Class란?
자바스크립트에서 class는 ES6부터 지원을 하기 시작했다.
자바스크립트에서 class는 함수의 한 종류이다.
익스플로러에서는 class를 지원하지 않으며, 최신 브라우저에서는 class를 지원한다.
class를 사용하는 가장 큰 이유는 재사용성이다.
복잡한 사이트를 만들게 될 경우 Class의 장점을 더 잘 알 수 있다고 한다.
클래스는 객체 지향 프로그래밍에서 특정 객체를 생성하기 위해
변수와 메소드를 정의하는 일종의 틀로,
객체를 정의하기 위한 상태(멤버 변수)와 메서드(함수)로 구성된다.
실무에선 사용자나 물건같이 동일한 종류의 객체를 여러 개 생성해야 하는 경우가 잦다.
이럴 때 new 연산자와 생성자 함수에서 배운 new function을 사용할 수 있다.
여기에 더하여 모던 자바스크립트에 도입된 클래스(class)라는 문법을 사용하면
객체 지향 프로그래밍에서 사용되는 다양한 기능을 자바스크립트에서도 사용할 수 있다.
기능은 동일하나(class라는 키워드 없이도 클래스 역할을 하는 함수를 선언할 수 있다.)
기존 문법을 쉽게 읽을 수 있게 만든 문법을 편의 문법(syntactic sugar, 문법 설탕)이라고 한다.
- 인스턴스 (Instance).
class의 현재 생성된 오브젝트'.
즉, 인스턴스는 'class를 통해 만들어진 객체'를 의미한다.
- Class 기본 문법
먼저, class 생성부터 해보겠다.
class를 선언만 해준다면 class 객체를 바로 만들 수 있다.
class Person {
}
let kim = new Person();
console.log(kim);
// Person { }
class MyClass {
constructor() { ... }
// 여러 메서드를 정의할 수 있다.
method() { ... }
method() { ... }
...
}
이렇게 클래스를 만들고, new MyClass()를 호출하면
내부에서 정의한 메서드가 들어 있는 객체가 생성된다.
객체의 기본 상태를 설정해주는 생성자 메서드 constructor()는 new에 의해 자동으로 호출되므로,
특별한 절차 없이 객체를 초기화 할 수 있다.
한 번 예시를 들어보자.
class User {
constructor (name) {
this.name =name;
}
sayHi () {
alert(this.name);
}
}
// 사용법:
let user = new User("John");
user.sayHi();
new User("John")를 호출하면 다음과 같은 일이 일어난다.
=> 새로운 객체가 생성된다.
넘겨받은 인수와 함께 constructor가 자동으로 실행된다.
이때 인수 "John"이 this.name에 할당돤다.
이런 과정을 거친 후에 user.sayHi() 같은 객체 메서드를 호출할 수 있다.
생성자 메서드가 없으면 본문이 비워진 채로 함수가 만들어진다.
sayHi 같은 class 내에서 정의한 메서드는 User.prototype에 저장한다.
new User를 호출해 객체를 만들고, 객체의 메서드를 호출하면
함수의 prototype 프로퍼티에서 설명한 것처럼 메서드를 prototype 프로퍼티를 통해 가져온다.
이 과정이 있기 때문에 객체에서 클래스 메서드에 접근할 수 있다.
class User 선언 결과를 그림으로 나타내면 아래와 같다.
class User {
constructor (name) {
this.name = name;
}
sayHi () {
alert(this.name);
}
}
// 클래스는 함수다.
alert(typeof User);
// function
// 정확히는 생성자 메서드와 동일하다.
alert(User === User.prototype.constructor);
// true
// 클래스 내부에서 정의한 메서드는 User.prototype에 저장된다.
alert(User.prototype.sayHi);
// alert(this.name);
// 현재 프로토타입에는 메서드가 두 개다.
alert(Object.getOwnPropertyNames(User.prototype));
// constructor, sayHi
⚠ 메서드 사이엔 쉼표가 없다.
쉼표를 넣으면 문법 에러가 발생한다.
클래스와 관련된 표기법은 객체 리터럴 표기법과 차이가 있다.
클래스에선 메서드 사이에 쉼표를 넣지 않아도 된다.
- Class 표현식.
함수처럼 클래스도 다른 표현식 내부에서 정의, 전달, 반환, 할당할 수 있다.
let User = class {
sayHi () {
alert("안녕하세요.");
}
};
기명 함수 표현식(Named Function Expression)과 유사하게 class 표현식에도 이름을 붙일 수 있다.
클래스 표현식에 이름을 붙이면, 이 이름은 오직 클래스 내부에서만 사용할 수 있다.
// 기명 클래스 표현식(Named Class Expression)
// (명세서엔 없는 용어이지만, 기명 함수 표현식과 유사하게 동작한다.)
let User = class MyClass {
sayHi () {
alert(MyClass);
// MyClass라는 이름은 오직 클래스 안에서만 사용할 수 있다.
}
};
new User().sayHi();
// 원하는대로 MyClass의 정의를 보여준다.
alert(MyClass);
// ReferenceError: MyClass is not defined, MyClass는 클래스 밖에서 사용할 수 없다.
아래와 같이 ‘필요에 따라’ 클래스를 동적으로 생성하는 것도 가능하다.
function makeClass(phrase) {
// 클래스를 선언하고 이를 반환.
return class {
sayHi () {
alert(phrase);
};
};
}
// 새로운 클래스를 만듦.
let User = makeClass("안녕하세요.");
new User().sayHi();
// 안녕하세요.
- Class 초기값 설정.
constructor(생성자)를 이용하면 class 객체의 초기 값을 설정해 줄 수 있다.
class 내부에서 constructor는 한 개만 존재할 수 있으며, 2번 이상 사용 시 Syntax Error가 발생할 수 있다.
Constructor를 이용하여 Person 클래스에 초기 값을 설정해보도록 하자.
class Person {
constructor (name,age, city) {
console.log('construtor');
this.name = name;
this.age = age;
this.city = city;
}
}
let kim = new Person('kim', '24', 'seoul');
console.log(kim);
// construtor
// Person {name: 'kim', age: '24', city: 'seoul'}
이처럼 Constructor는 새로운 class를 생성할 때 가장 처음 실행되면서 초기값을 설정해준다.
- Class 메서드 사용하기
일반 메서드를 프로토타입 메서드라고 한다.
class에서 설정한 초기값에 접근해 특정 기능을 하는 메서드를 만드는 것도 가능하다.
간단한 메서드 하나를 만들어보겠다.
class 안에 function 형식으로 만들어준 뒤 해당 메서드를 호출하기만 하면 된다.
너무 당연하지만 내년에 해당 사람이 한 살 더 먹는다는 메서드를 class안에 정의한 뒤 호출해봤다.
class Person {
constructor (name,age, city) {
this.name = name;
this.age = age;
this.city = city; }
// 메서드 생성 => class에서 메서드 구조 기억하기.
nextYearAge() {
return Number(this.age) + 1;
}
}
let kim = new Person('kim', '24', 'seoul');
console.log(kim.nextYearAge());
// 25
class는 javascript 상 객체의 형태이므로 생성된 class 객체에
class 밖에서 새로운 메서드를 넣는 것도 가능하다.
다음 예시를 보자.
class Person {
constructor (name,age, city) {
this.name = name;
this.age = age;
this.city = city;
}
// 메서드생성
nextYearAge() {
return Number(this.age) +1;
}
}
let kim = new Person('kim', '24', 'seoul');
kim.eat = function () {
return 'apple'
}
console.log(kim.nextYearAge());
// 25
console.log(kim.eat());
// apple
class밖에서 추가한 eat이라는 메서드도 정확히 작동한다.
하지만, 이렇게 밖에서 추가한 class는
추후 새로운 new Person class로 새로운 객체를 만들었을 때는 호출하여 사용할 수 없다.
class Person {
constructor (name,age, city) {
this.name = name;
this.age = age;
this.city = city; }
// 메서드생성
nextYearAge() {
return Number(this.age) +1;
}
}
let kim = new Person('kim', '24', 'seoul');
kim.eat = function () {
return 'apple'
}
console.log('김씨 내년에는 몇살인가요 ?' + kim.nextYearAge());
// 김씨 내년에는 몇살인가요 ? 25
console.log('김씨가 먹은건? '+ kim.eat());
// 김씨가 먹은건? apple
let park = new Person('park', '31', 'busan');
console.log('박씨 내년에는 몇살인가요?' + park.nextYearAge());
// 박씨 내년에는 몇살인가요 ? 32
console.log('박씨가 먹은건?' + park.eat());
// TypeError: park.eat is not a function
- 상속(extends).
class에서 상속 개념을 이용할 수 있다.
class에서 상속을 이용하면 기존의 class의 값을 모두 접근하여 사용할 수 있다.
상속은 extends를 써서 이용할 수 있다.
다음 예제를 통해서 introducePerson클래스에서 Person을 상속받았기 때문에
this.city와 this.name을 모두 사용할 수 있는 것을 확인할 수 있다.
class Person {
constructor (name,age, city) {
this.name = name;
this.age = age;
this.city = city; }
//메서드생성
nextYearAge() {
return Number(this.age) +1;
}
}
class introducePerson extends Person {
introduce () {
return `저는 ${this.city}에 사는 ${this.name} 입니다.`
}
}
let kim = new introducePerson('kim', '24', 'seoul');
console.log(kim.introduce())
// 저는 seoul에 사는 kim 입니다.
- super.
introducePerson 하위 클래스에서 기존 class의 값을 가져다 쓰는 건 좋았지만,
추가적으로 introducePerson이라는 하위 클래스에서만 사용하고 싶은 값이 있을 수도 있다.
이때 이용하는 것은 super라는 키워드이며
이는 객체의 부모가 가지고 있는 메서드를 호출할 수 있다.
자식 쪽의 추가적으로 사용할 초기값이 필요할 경우
constructor에 super로 부모 초기값을 세팅한 뒤 자식 class에서만 사용할 초기값을 따로 지정하는 것도 가능하며
super 기능을 이용해서 자식 class에서 부모 메서드를 호출할 수도 있다.
class Person {
constructor (name,age, city) {
this.name = name;
this.age = age;
this.city = city; }
//메서드생성
nextYearAge() {
return Number(this.age) +1;
}
}
class introducePerson extends Person {
constructor(name, age, city, futureHope) {
super (name, age, city);
this.futureHope = futureHope
}
introduce () {
return `저는 ${this.city}에 사는 ${this.name} 입니다.
내년엔 ${super.nextYearAge()}살이며,
장래희망은 ${this.futureHope} 입니다.`
}
}
let kim = new introducePerson('kim', '24', 'seoul', '개발자');
console.log(kim.introduce())
// 저는 seoul에 사는 kim 입니다.
// 내년엔 25살이며,
// 장래희망은 개발자 입니다.
- getter와 setter.
리터럴을 사용해 만든 객체처럼 클래스도 getter나 setter, 계산된 프로퍼티(computed property)를 지원한다.
get과set을 이용해 user.name을 조작할 수 있게 해보자.
class User {
constructor(name) {
// setter를 활성화.
this.name = name;
}
get name() {
return this._name;
}
set name(value) {
if (value.length < 4) {
alert("이름이 너무 짧습니다.");
return;
}
this._name = value;
}
}
let user = new User("보라");
alert(user.name);
// 보라
user = new User("");
// 이름이 너무 짧습니다.
getter와 setter는 User.prototype에 정의된다.
setter를 추가한 순간부터는 값을 할당하려고 시도하면
프로퍼티의 값에 직접 할당하지 않고 setter를 호출한다.
새로운 user 객체를 만들면서 this.name으로 "보라"를 할당하려고 할 때 set name(value)를 호출한다.
=> value의 길이가 4보다 큰 값인지 검사하고 이후 _name에 value를 할당한다.
그리고 getter를 추가한 순간부터는 값을 사용하려고 시도하면
메모리에 저장된 값을 불러오지 않고 getter를 호출한다.
this.name을 사용하려 시도할 때 get name()을 호출한다.
=> setter로 할당된 값인 this._name을 리턴한다.

- 여기서 잠깐, _name을 사용하는 이유가 뭘까?
setter에서 this.name에 value를 할당해주고
getter에서 this.name을 return하면 무한 반복이 발생하면서
최대 호출 스텍 크기를 초과했다는 error가 발생하게 된다.
[ setter 무한 반복 ]
user 객체를 만들면서 name에 값을 할당하려 했다. ---> set name(value)을 호출.
이후 if문을 거친 후 this.name에 value를 할당하려 했다. ---> set name(value)을 호출.
또 if문을 거친 후 this.name에 value를 할당하려 했다. ---> set name(value)을 호출.
[ getter 무한 반복 ]
this.name을 사용하려 했다. ---> get name()을 호출.
this.name을 리턴하려고 this.name을 사용하려 했다. ---> get name()을 호출.
또 this.name을 리턴하려고 this.name을 사용하려 했다.---> get name()을 호출.
이렇게 무한 반복이 일어나게 된다.
무한 반복을 해결하기 위해서는 setter와 getter 안에 있는
name을 _name으로 변경해주면 된다.
=> _는 private하다 또는 내부적인 속성이다 라는 뜻으로 쓰인다.
이렇게 만든 property는 외부에서 접근이 가능하지만, (user._name)
외부에서 사용하지는 않는다. (내부적인 속성이다.)
이렇게 하면 무한 반복이 해결된다.
[ setter ]
user 객체를 만들면서 name에 값을 할당하려 했다. ---> set name(value)을 호출.
이후 if문을 거친 후 this.name에 value를 할당하려 했다. ---> 내부적인 프로퍼티인 _name에 value가 할당됨.
[ getter ]
this.name을 사용하려 했다. ---> get name()을 호출.
setter로 할당된 내부적인 프로퍼티인 _name의 값을 리턴.
- 계산된 메서드 이름.
대괄호 [...]를 이용해 계산된 메서드 이름(computed method name)을 만드는 예시를 살펴보자.
class User {
['say' + 'Hi']() {
alert("Hello");
}
}
new User().sayHi();
// Hello
계산된 메서드 이름은 리터럴 객체와 유사한 형태를 띠기 때문에 사용법을 외우기 쉽다는 장점이 있다.
- Class 필드(field).
class 필드는 최근에 더해진 기능이다.
클래스 블록 안에서 할당 연산자(=)를 이용해 인스턴스 속성을 지정할 수 있는 문법이다.
'클래스 필드(class field)'라는 문법을 사용하면 어떤 종류의 프로퍼티도 클래스에 추가할 수 있다.
class 블록은 새로운 블록 스코프를 형성하고, 이 내부에서 사용된 this는 인스턴스 객체를 가리키게 된다.
class MyClass {
a = 1;
b = this.a;
}
new MyClass().b;
// 1
이 성질을 이용하면, 화살표 함수를 통해서 메소드를 정의할 수 있다.
(화살표 함수 안에서의 this 키워드는
바로 바깥쪽 스코프에 존재하는 this와 같은 객체를 가리킨다는 사실을 떠올려보자.)
class MyClass {
a = 1;
getA = () => {
return this.a;
}
}
new MyClass().getA();
// 1
이렇게만 보면 일반적인 메소드와 별로 차이가 없어 보이지만,
사실 동작방식 측면에서 굉장히 큰 차이점이 있다.
1. 일반적인 메소드는 클래스의 prototype 속성에 저장되는 반면,
클래스 필드는 인스턴스 객체에 저장된다.
2. 화살표 함수의 this는 호출 형태에 관계없이 항상 인스턴스 객체를 가리키게 된다.
2번 성질때문에, 메소드를 값으로 다루어야 할 경우에는
일반적인 메소드 대신 화살표 함수가 사용되는 경우가 종종 있다.
다만, 일반적인 메소드와 달리, 클래스 필드를 통해 정의한 메소드는
인스턴스를 생성할 때마다 새로 생성되기 때문에 메모리를 더 차지하게 되므로 주의해서 사용해야 한다.
클래스 User에 name 프로퍼티를 추가해보자.
class User {
name = "보라";
sayHi () {
alert(`${this.name}님 안녕하세요!`);
}
}
new User().sayHi();
// 보라님 안녕하세요!
클래스를 정의할 때 '<프로퍼티 이름> = <값>'을 써주면 간단히 클래스 필드를 만들 수 있다.
클래스 필드의 중요한 특징 중 하나는
User.prototype이 아닌 개별 객체에만 클래스 필드가 설정된다는 점이다.
class User {
name = "보라";
}
let user = new User();
alert(user.name);
// 보라
alert(User.prototype.name);
// undefined
아울러 클래스 필드엔 복잡한 표현식이나 함수 호출 결과를 사용할 수 있다.
class User {
name = prompt("이름을 알려주세요.", "보라");
}
let user = new User();
alert(user.name);
// prompt 창이 뜬다. => 보라
- class 필드로 바인딩 된 메서드 만들기.
함수 바인딩 챕터에서 살펴본 것처럼 자바스크립트에서 this는 동적으로 결정된다.
따라서 객체 메서드를 여기저기 전달해 전혀 다른 컨텍스트에서 호출하게 되면
this는 메서드가 정의된 객체를 참조하지 않는다.
관련 예시를 살펴보자. 예시를 실행하면 undefined가 출력된다.
class Button {
constructor (value) {
this.value = value;
}
click () {
alert(this.value);
}
}
let button = new Button("안녕하세요.");
setTimeout(button.click, 1000); // undefined
이렇게 this의 컨텍스트를 알 수 없게 되는 문제를 '잃어버린 this(losing this)'라고 한다.
문제는 두 방법을 사용해 해결할 수 있는데 함수 바인딩에서 이 방법에 대해 알아본 바가 있다.
1. setTimeout(() => button.click(), 1000) 같이 래퍼 함수를 전달하기
2. 생성자 안 등에서 메서드를 객체에 바인딩하기
이 두 방법 말고 클래스 필드를 사용해도 우아하게 문제를 해결할 수 있습니다.
class Button {
constructor (value) {
this.value = value;
}
// 필드(field)
click = () => {
alert(this.value);
}
}
let button = new Button("안녕하세요.");
setTimeout(button.click, 1000);
// 안녕하세요.
클래스 필드 click = () => {...}는 각 Button 객체마다 독립적인 함수를 만들어주고
이 함수의 this를 해당 객체에 바인딩시켜준다.
따라서 개발자는 button.click을 아무 곳에나 전달할 수 있고, this엔 항상 의도한 값이 들어가게 된다.
클래스 필드의 이런 기능은 브라우저 환경에서 메서드를 이벤트 리스너로 설정해야 할 때 특히 유용하다.
- 요약.
아래와 같은 기본 문법을 사용해 클래스를 만들 수 있다.
class MyClass {
prop = value;
// 프로퍼티
constructor (...) {
// 생성자 메서드
// ...
}
method (...) { }
// 메서드
get something (...) { }
// getter 메서드
set something (...) { }
// setter 메서드
[Symbol.iterator]() { }
// 계산된 이름(computed name)을 사용해 만드는 메서드 (심볼)
// ...
}
MyClass는 constructor의 코드를 본문으로 갖는 함수다.
MyClass에서 정의한 일반 메서드나 getter, setter는 MyClass.prototype에 쓰인다.
- static.
static 키워드는 class의 정적 메서드나 정적 프로퍼티(static property)를 정의한다.
정적 메서드는 class의 종속적인 메서드를 의미한다.
즉. class와 해당 메서드는 연결되어 있지만, 해당 class의 특정 인스턴스와는 연결되어 있지 않다.
(정적 메서드는 인스턴스의 포로토타입 체인에 속해있지 않다.)
그렇기 때문에 클래스의 인스턴스 없이 호출이 가능하며 클래스가 인스턴스화되면 호출할 수 없다.
정적 메서드는 종종 어플리케이션의 유틸리티 함수를 만드는데 사용된다.
// 예제 유틸리티 함수
class Util {
static generateRandomNumber(min, max) {
return Math.floor(Math.random() * (max - min)) + min
}
}
console.log(Util.generateRandomNumber(1, 10));
또한, 정적 메서드는 특정 객체(class)에 저장된 데이터에 접근할 수 없다.
// 클래스 데이터 접근 예제
class Person {
constructor() {
this._name = "KIM";
}
static get name(){
return this._name
}
}
// 1. 정적 메서드를 이용해 클래스 데이터 가져오기 - 실패
console.log(Person.name)
// undefined
// 2. 클래스 인스턴스를 선언해 클래스 데이터 가져오기 - 실패
const person = new Person();
console.log(person.name)
// undefined
정적 메서드 | 프로토타입 메서드 |
class로 호출. 생성자 함수로 인스턴스를 생성하지 않아도 호출할 수 있는 메서드. => class로 호출이 가능하다. => ClassWithStaticMethod.staticMethod() |
인스턴스로 호출. |
인스턴스의 프로퍼티를 참조하지 않는다. 정적 메서드는 class로 호출되기 때문에, 인스턴스로 호출되는 class와는 내부의 this가 다를 수밖에 없다. 따라서, this를 사용해야 하는 경우에는 프로토타입 메서드로 정의해야 한다. 반면에 this로 인스턴스의 프로퍼티를 참조해야 할 필요가 없고 class 호출만으로도 충분하다면 정적 메서드로 만들면 된다. |
인스턴스의 프로퍼티를 참조한다. |
문법 : static methodName() { ... }
class ClassWithStaticMethod {
static staticProperty = 'someValue';
static staticMethod() {
return 'static method has been called.';
}
static {
console.log('Class static initialization block called');
}
}
console.log(ClassWithStaticMethod.staticProperty);
// Expected output: "someValue"
console.log(ClassWithStaticMethod.staticMethod());
// Expected output: "static method has been called."
- static을 사용하는 이유? :
자바스크립트에서 Static 키워드를 사용하는 이유는
복제가 필요 없는 데이터를 다루기에 효과적이기 때문이다.
코드에서 한 번만 사용되는데 인스턴스를 생성하면
추가적인 데이터 공간이 낭비되기도 하고, 코드 길이가 조금 더 길어진다.
여기서 static을 사용하면 따로 인스턴스를 만들지 않고 메서드를 실행할 수 있다는 점에서 좋다.
- 정적 메서드의 호출.
=> 다른 정적 메서드에서의 호출 :
동일한 class 내의 다른 정적 메서드 내에서 정적 메서드를 호출하는 경우
this를 사용할 수 있다.
class StaticMethodCall {
static staticMethod() {
return 'Static method has been called';
}
static anotherStaticMethod() {
return this.staticMethod() + ' from another static method';
}
}
StaticMethodCall.staticMethod();
// 'Static method has been called'
StaticMethodCall.anotherStaticMethod();
// 'Static method has been called from another static method'
=> class 생성자 및 다른 메서드에서의 호출 :
정적 메서드가 비정적 메서드에서 this를 써서는 직접적인 접근을 할 수 없다.
바로 호출 방법은 class 명칭을 쓰거나,(CLASSNAME.STATIC_METHOD_NAME())
혹은 그 메서드를 생성자의 한 속성으로 부르는 것이다.(this.constructor.STATIC_METHOD_NAME())
class StaticMethodCall {
constructor() {
console.log(StaticMethodCall.staticMethod());
// 'static method has been called.'
console.log(this.constructor.staticMethod());
// 'static method has been called.'
}
static staticMethod() {
return 'static method has been called.';
}
}
- static 예제.
class Orange{
//class 정적 프로퍼티 추가
static weight = 100;
static description ="I'm an Orange";
//static 메서드 추가
static joke(){
console.log("Orange를 먹은지 얼마나 오랜지");
}
}
// Orange를 상속받은 SpainOrange
class SpainOrange extends Orange{
static location ='spain';
static joke(){
// 부모에 있는 joke()를 받아옴.
super.joke();
console.log("I'm a spainOrange, Ha, Ha, Ha");
}
}
//static property 출력
console.log(Orange.weight);
//100
//static method 출력
console.log(Orange.joke());
//Orange를 먹은지 얼마나 오랜지
//static property 출력
console.log(SpainOrange.location);
//spain
//static method 출력
console.log(SpainOrange.joke());
//Orange를 먹은지 얼마나 오랜지
//I'm a spainOrange, Ha, Ha, Ha
// Orange class를 이용해 새로운 객체 만들어주기.
const orange = new Orange();
console.log(orange.joke());
//Uncaught TypeError: orange.joke is not a function
// 인스턴스로 static 메서드를 호출할 수 없다.
- private, protected 프로퍼티와 메서드.
객체 지향 프로그래밍에서 가장 중요한 원리 중 하나는
'내부 인터페이스와 외부 인터페이스를 구분 짓는 것’이다.
단순히 'hello word’를 출력하는 것이 아닌 복잡한 애플리케이션을 구현하려면,
내부 인터페이스와 외부 인터페이스를 구분하는 방법을 ‘반드시’ 알고 있어야 한다.
객체 지향 프로그래밍에서 프로퍼티와 메서드는 두 그룹으로 분류된다.
- 내부 인터페이스(internal interface) :
동일한 클래스 내의 다른 메서드에선 접근할 수 있지만, 클래스 밖에선 접근할 수 없는 프로퍼티와 메서드.
내부 인터페이스의 세부사항들은 서로의 정보를 이용하여 객체를 동작시킨다.
밖에선 세부 요소를 알 수 없고, 접근도 불가능하다.
내부 인터페이스의 기능은 외부 인터페이스를 통해야만 사용할 수 있다.
이런 특징 때문에 외부 인터페이스만 알아도 객체를 가지고 무언가를 할 수 있다.
객체 안이 어떻게 동작하는지 알지 못해도 괜찮다는 점은 큰 장점으로 작용한다.
- 외부 인터페이스(external interface) :
클래스 밖에서도 접근 가능한 프로퍼티와 메서드.
자바스크립트에는 아래와 같은 두 가지 타입의 객체 필드(프로퍼티와 메서드)가 있다.
- public :
어디서든지 접근할 수 있으며 외부 인터페이스를 구성한다.
지금까지 다룬 프로퍼티와 메서드는 모두 public이다.
- private :
클래스 내부에서만 접근할 수 있으며 내부 인터페이스를 구성할 때 쓰인다.
자바스크립트 이외의 다수 언어에서
클래스 자신과 자손 클래스에서만 접근을 허용하는 ‘protected’ 필드를 지원한다.
protected 필드는 private과 비슷하지만, 자손 클래스에서도 접근이 가능하다는 점이 다르다.
protected 필드도 내부 인터페이스를 만들 때 유용하다.
자손 클래스의 필드에 접근해야 하는 경우가 많기 때문에,
protected 필드는 private 필드보다 조금 더 광범위하게 사용된다.
자바스크립트는 protected 필드를 지원하지 않지만,
protected를 사용하면 편리한 점이 많기 때문에 이를 모방해서 사용하는 경우가 많다.
- 프로퍼티 보호하기. (protected 프로퍼티)
class CoffeeMachine {
waterAmount = 0;
// 물통에 차 있는 물의 양
constructor (power) {
this.power = power;
alert(`전력량이 ${power}인 커피머신을 만듭니다.`);
}
}
// 커피 머신 생성
let coffeeMachine = new CoffeeMachine(100);
// 물 추가
coffeeMachine.waterAmount = 200;
현재 프로퍼티 waterAmount와 power는 public이다.
손쉽게 waterAmount와 power를 읽고 원하는 값으로 변경하기 쉬운 상태인 것이다.
이제 waterAmount를 protected로 바꿔서 waterAmount를 통제해 보겠다.
예시로 waterAmount를 0 미만의 값으로는 설정하지 못하도록 만들어 볼 것이다.
protected 프로퍼티 명 앞엔 밑줄 _이 붙는다.
자바스크립트에서 강제한 사항은 아니지만,
밑줄은 프로그래머들 사이에서 외부 접근이 불가능한 프로퍼티나 메서드를 나타낼 때 쓴다.
waterAmount에 밑줄을 붙여 protected 프로퍼티로 만들어주자.
classCoffeeMachine{
_waterAmount = 0;
set waterAmount (value) {
if (value < 0) throw new Error("물의 양은 음수가 될 수 없습니다.");
this._waterAmount = value;
}
get waterAmount() {
return this._waterAmount;
}
constructor (power) {
this._power = power;
}
}
// 커피 머신 생성
let coffeeMachine = new CoffeeMachine(100);
// 물 추가
coffeeMachine.waterAmount = -10;
// Error: 물의 양은 음수가 될 수 없습니다.
// 나는 외부에서 접근할 수 없도록 procted 처리를 해줬는데
// 객체 내부에서 선언된 변수는 객체 내에서 선언된 함수에 의해서 사용될 수 있다.(setter에 의해서)
이제 물의 양을 0 미만으로 설정하면 실패한다.
- 읽기 전용 프로퍼티.
power 프로퍼티를 읽기만 가능하도록 만들어보자.
프로퍼티를 생성할 때만 값을 할당할 수 있고, 그 이후에는 값을 절대 수정하지 말아야 하는 경우가 종종 있는데,
이럴 때 읽기 전용 프로퍼티를 활용할 수 있다.
커피 머신의 경우에는 전력이 이에 해당한다.
읽기 전용 프로퍼티를 만들려면 setter(설정자)는 만들지 않고 getter(획득자)만 만들어야 한다.
class CoffeeMachine {
// ...
constructor (power) {
this._power = power;
}
get power () {
return this._power;
}
}
// 커피 머신 생성
let coffeeMachine = new CoffeeMachine(100);
alert(`전력량이 ${coffeeMachine.power}인 커피머신을 만듭니다.`);
// 전력량이 100인 커피머신을 만듭니다.
coffeeMachine.power = 25;
// Error (setter 없음)
protected 필드는 상속된다.
class MegaMachine extends CoffeeMachine로 클래스를 상속받으면,
새로운 클래스의 메서드에서 this._waterAmount나 this._power를 사용해 프로퍼티에 접근할 수 있다.
이렇게 protected 필드는 아래에서 보게 될 private 필드와 달리, 자연스러운 상속이 가능하다.
- private 프로퍼티.
최근에 추가됨. 스펙에 추가된 지 얼마 안 된 문법이다.
private 프로퍼티와 메서드는 #으로 시작합니다. #이 붙으면 클래스 안에서만 접근할 수 있다.
=> 물 용량 한도를 나타내는 private 프로퍼티 #waterLimit과
남아있는 물의 양을 확인해주는 private 메서드 #checkWater를 구현해보자.
class CoffeeMachine {
#waterLimit = 200;
#checkWater(value) {
if (value < 0) throw new Error("물의 양은 음수가 될 수 없습니다.");
if (value > this.#waterLimit) throw new Error("물이 용량을 초과합니다.");
}
}
let coffeeMachine = new CoffeeMachine();
// 클래스 외부에서 private에 접근할 수 없음
coffeeMachine.#checkWater(); // Error
coffeeMachine.#waterLimit = 1000; // Error
#은 자바스크립트에서 지원하는 문법으로, private 필드를 의미한다.
private 필드는 클래스 외부나 자손 클래스에서 접근할 수 없다.
private 필드는 public 필드와 상충하지 않는다.
private 프로퍼티 #waterAmount와 public 프로퍼티 waterAmount를 동시에 가질 수 있다.
#waterAmount의 접근자 waterAmount를 만들어보자.
class CoffeeMachine {
#waterAmount = 0;
get waterAmount () {
return this.#waterAmount;
}
set waterAmount (value) {
if (value < 0) throw new Error("물의 양은 음수가 될 수 없습니다.");
this.#waterAmount = value;
}
}
let machine = new CoffeeMachine();
machine.waterAmount = 100;
alert(machine.#waterAmount);
// Error
protected 필드와 달리, private 필드는 언어 자체에 의해 강제된다는 점이 장점이다.
그런데 CoffeeMachine을 상속받는 클래스에선 #waterAmount에 직접 접근할 수 없다.
#waterAmount에 접근하려면 waterAmount의 getter와 setter를 통해야 한다.
class MegaCoffeeMachine extends CoffeeMachine {
method () {
alert( this.#waterAmount ); // Error: CoffeeMachine을 통해서만 접근할 수 있다.
}
}
다양한 시나리오에서 이런 제약사항은 너무 엄격하다.
CoffeeMachine을 상속받는 클래스에선 CoffeeMachine의 내부에 접근해야 하는 정당한 사유가 있을 수 있기 때문이다.
언어 차원에서 protected 필드를 지원하지 않아도 더 자주 쓰이는 이유가 바로 여기에 있다.
private 필드는 this[name]로 사용할 수 없다.
private 필드는 특별하다.알다시피, 보통은 this[name]을 사용해 필드에 접근할 수 있다.
class User {
...
sayHi () {
let fieldName = "name";
alert (`Hello, ${this[fieldName]}`);
}
}
하지만 private 필드는 this[name] 으로 접근할 수 없다.
이런 문법적 제약은 필드의 보안을 강화하기 위해 만들어졌다.
객체 지향 프로그래밍에선 내부 인터페이스와 외부 인터페이스를 구분하는 것을 캡슐화라고 한다.
- 캡슐화 : 사용자가 자신의 발등을 찍지 않도록 보호한다.
커피 머신를 함께 사용하는 개발팀이 있다고 상상해보자.
"Best CoffeeMachine"이라는 회사에서 만든 이 커피 머신은 현재 잘 작동하고 있지만,
보호 커버가 없어서 내부 인터페이스가 노출되어있는 상황이다.
교양있는 팀원들은 모두 설계 의도에 맞게 커피 머신을 사용한다.
그런데 어느 날 John이라는 개발자가 자신의 능력을 과신하며 커피 머신 내부를 살짝 만지게 된다.
이틀 후, 커피 머신은 고장이 나버렸다.
커피 머신이 고장 난 건 John의 잘못이라기보다는,
보호 커버를 없애고 John이 마음대로 조작하도록 내버려 둔 사람의 잘못이다.
프로그래밍에서도 마찬가지다. 외부에서 의도치 않게 클래스를 조작하게 되면 그 결과는 예측할 수 없게 된다.
[ 지원 가능 ]
실제 개발 과정에서 일어나는 상황은 커피 머신 사례보다 훨씬 복잡하다.
커피 머신은 한번 구매하면 끝이지만 실제 코드는 유지보수가 끊임없이 일어나기 때문이다.
내부 인터페이스를 엄격하게 구분하면,
클래스 개발자들은 사용자에게 알리지 않고도 자유롭게 내부 프로퍼티와 메서드들을 수정할 수 있다.
내부 인터페이스가 엄격히 구분된 클래스를 만지고 있다면,
그 어떤 외부 코드도 내부 private 메서드에 의존하고 있지 않기 때문에
private 메서드의 이름을 안전하게 바꿀 수 있고, 매개변수를 변경하거나 없앨 수도 있다는 것을 알아 두면 된다.
사용자 입장에선 새로운 버전이 출시되면서 내부 정비가 전면적으로 이뤄졌더라도
외부 인터페이스만 똑같다면 업그레이드가 용이하다는 장점이 있다.
[ 복잡성 은닉 ]
사람들은 간단한 것을 좋아한다. 내부는 간단치 않더라도 최소한 외형은 간단해야 한다.
프로그래머들도 예외는 아니다.
구현 세부 사항이 숨겨져 있으면 간단하고 편리해진다. 외부 인터페이스에 대한 설명도 문서화하기 쉬워진다.
내부 인터페이스를 숨기려면 protected나 private 프로퍼티를 사용하면 된다.
- protected : 필드는 _로 시작한다.
_은 자바스크립트에서 지원하는 문법은 아니지만, protected 필드를 나타낼 때 관습처럼 사용된다.
개발자는 protected 프로퍼티가 정의된 클래스와 해당 클래스를 상속받는 클래스에서만 _가 붙은 필드에 접근해야 한다.
- private : 필드는 #로 시작하며, 자바스크립트에서 지원하는 문법이다.
#로 시작하는 필드는 해당 필드가 정의된 클래스 내부에서만 접근 가능하다.
모든 브라우저에서 private 필드를 지원하진 않지만 폴리필을 구현하여 사용할 수 있다.
'Javascript' 카테고리의 다른 글
[2023-12-06] JAVASCRIPT - 생성자 함수 내에서 일반 함수와 화살표 함수의 차이점 (0) | 2023.12.06 |
---|---|
논리연산자&& (AND) (0) | 2023.07.13 |
다시 시작하는 자바스크립트 - 스코프 (0) | 2023.04.27 |
다시 시작하는 자바스크립트 - 비동기 (0) | 2023.04.25 |
다시 시작하는 자바스크립트 - 주석, 에러처리, 모듈 (0) | 2023.04.24 |
github : https://github.com/dnjfht
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!