자바스크립트의 클래스에 대해 공부해보기로 했다. 공부를 결심한 계기는 다음과 같다.
우테코 프리코스에서 2주 차 미션을 받았는데, App.js
에 다음과 같이 적혀있었다.
class App {
play() {}
}
module.exports = App;
class가 뭔지, class를 어떻게 쓰는지 몰랐던 나는 검색 그리고 검색을 통해 'class를 실행시키는 것'에 성공했다. 하지만 이게 뭔지, 어떻게 동작하는지는 잘 몰랐고.. 그게 코드리뷰에서 드러났다.
class에서 메서드를 쓸 때 this를 사용하지 않은 경우 static으로 선언해주는 것을 지향
한다고 한다. this는 어디선가 들어봤고 static은 들어본 적도 없다...! 그래서 이번 포스팅을 통해 알아보기로 했다.
민재 님이 첨부해주신 URL에 가보면 Airbnb 자바스크립트 컨벤션 9.7 절에서 다음과 같이 설명하고 있다. 클래스 메소드는 this를 사용하거나 static을 사용해야 한다
. this는 왜 쓴 것인지 조금 이해가 가는데, static은 정말 생전 처음 보는 것이다. 그래서 학습할 때 static를 유의해서 보기로 했다.
학습은 모던 자바스크립트 튜토리얼에서 했다.
민재 님이 첨부해주신 Airbnb 자바스크립트 컨벤션에 나오는 '9.3 정적 메서드(아마 static)'가 모던 자바스크립트 튜토리얼에 나오는 걸 보고 이 자료로 학습하면 될 것 같다고 생각했다.
class MyClass {
// 여러 메서드를 정의할 수 있음
constructor() { ... }
method1() { ... }
method2() { ... }
method3() { ... }
...
}
class User {
constructor(name) {
this.name = name;
}
sayHi() {
alert(this.name);
}
}
// 사용법:
let user = new User("John");
user.sayHi();
constructor는 new를 선언할 때 생긴다. 다시 이 코드를 보자.
constructor에 name이 있으므로 new User() 안에 꼭 name을 써줘야 한다.
class User {
constructor(name) {
this.name = name;
}
sayHi() {
alert(this.name);
}
}
// 사용법:
let user = new User("John");
user.sayHi();
하지만 인수 없이 new User()라고만 쓰는데, constructor를 쓸 수 있는 경우도 있다.
아래의 코드와 같이 constructor에서 name을 선언하되 지정해주지 않고, 아래에서 this.name = 'John'
이라고 대입해주면 된다.
class User {
constructor(name) {
this.name;
}
sayHi() {
this.name = 'John';
alert(this.name);
}
}
// 사용법:
let user = new User();
user.sayHi();
그런데 그러면 아래처럼 constructor 없이 변수를 선언해주는 것과 뭐가 다른 건지 모르겠다.
class User {
#name;
sayHi() {
this.#name = 'John';
console.log(this.#name);
}
}
// 사용법:
let user = new User();
user.sayHi();
그리고, 변수와 constructor 둘 다 선언해주는 아래의 코드와 뭐가 다른지도 잘 모르겠다.
class User {
#name;
constructor(name) {
this.#name = name;
}
sayHi() {
console.log(this.#name);
}
}
// 사용법:
let user = new User('John');
user.sayHi();
일단 #name이 없어도 코드가 돌아가는 데는 문제가 없지만(constructor에서 this.#name = name
이라고 정해주기 때문)
는 의도가 있다고 한다!
this가 뭔지 잘 몰라서 찾아봤다.
let user = {
name: "John",
age: 30,
sayHi() {
// 'this'는 '현재 객체'를 나타냅니다.
alert(this.name);
}
};
user.sayHi(); // John
let user = {
name: "John",
age: 30,
sayHi() {
alert(user.name); // 'this' 대신 'user'를 이용함
}
};
let user = {
name: "John",
age: 30,
sayHi1() {
console.log(this.name);
},
sayHi2() {
console.log(user.name);
},
};
let admin = user;
user = null; // user를 null로 덮어씀
admin.sayHi1(); // 'John'
admin.sayHi2(); // 'error'
this
로 인해 무조건 user 객체를 참고하기 때문이다.user.
으로 인해 엉뚱한 객체인 null을 참고하기 때문이다.let User = class {
sayHi() {
alert("안녕하세요.");
}
};
먼저 클래스 Animal
을 만들어보자.
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed = speed;
alert(`${this.name} 은/는 속도 ${this.speed}로 달립니다.`);
}
stop() {
this.speed = 0;
alert(`${this.name} 이/가 멈췄습니다.`);
}
}
let animal = new Animal("동물");
또다른 클래스 Rabbit
을 만들어보자. 동물 관련 메서드가 담긴 Animal
을 확장해서 만들어야 한다.
class Rabbit extends Animal {
hide() {
alert(`${this.name} 이/가 숨었습니다!`);
}
}
let rabbit = new Rabbit("흰 토끼");
rabbit.run(5); // 흰 토끼 은/는 속도 5로 달립니다.
rabbit.hide(); // 흰 토끼 이/가 숨었습니다!
드디어 궁금했던 static
에 대해 설명하고 있는 부분이다.
"prototype"이 아닌 클래스 함수 자체에 메서드를 설정할 수 있다.
이런 메서드를 정적(static) 메서드라고 부른다.
정적 메서드는 아래와 같이 클래스 안에서 static 키워드
를 붙여 만들 수 있다.
(예 1)
class User {
static staticMethod() {
alert(this === User);
}
}
User.staticMethod(); // true
class User { }
User.staticMethod = function() {
alert(this === User);
};
User.staticMethod(); // true
class Article {
static publisher = "Ilya Kantor";
}
alert( Article.publisher ); // Ilya Kantor
시니하 님은 직접 코드를 짜서 보여주셨는데!! 바로 Counter 예시였다.
아래와 같은 main.js를 실행시키면 class로 만든 Counter.wrapped가 5개 실행된다.
// main.js
import { Counter } from "./Counter";
const app = document.getElementById('app');
if (app) {
for (let i = 0; i < 5; i++) {
const counter = Counter.wrapped(app);
counter.render();
}
}
실행 화면은 다음과 같다. 개별적인 counter가 잘 동작한다. 이와 같이 어떠한 기능을 한번만 쓰는게 아니라 여러번 사용할 때 class가 유용한 것 같다.
그럼 이 Counter.wrapped가 뭔지 알아보자. wrapped는 Counter.js에 정의되어있다. Counter.js의 내용은 아래와 같다.
// Counter.js
export class Counter {
#parent;
#previousEl = null;
#count;
constructor(parent, initialCount = 0) {
this.#parent = parent;
this.#count = initialCount;
}
static wrapped(parent, initialCount = 0) {
const wrapper = document.createElement("div");
wrapper.className = "wrapper";
parent.append(wrapper);
return new Counter(wrapper, initialCount);
}
count() {
return this.#count;
}
action(command) {
switch (command.type) {
case "increment": {
this.#count++;
this.render();
break;
}
}
}
render() {
if (this.#previousEl) {
this.#parent.removeChild(this.#previousEl);
}
const el = document.createElement("div");
el.className = "counter";
el.append(`Count: ${this.#count}`);
const button = document.createElement("button");
button.append("+");
button.addEventListener("click", () => {
this.action({ type: "increment" });
});
el.append(button);
this.#parent.append(el);
this.#previousEl = el;
}
}
wrapped는 counter 기능을 하는 div를 감싸는 div이다.
따라서 counter 기능과는 연관이 없다. 이렇듯 class 안의 메서드에 특별하게 영향을 미치는 것이 아니라 전체적으로 영향을 미칠 때 static을 사용한다.
하지만, 그것은 이 wrapped를 class 안에 선언할 때 사용하는 것이라서 class 안에서 static으로 wrapped를 선언하는 대신 class 밖에서 일반 함수로 정의하는 방법도 있다.
아래의 코드는 위에 쓴 Counter.js의 일부분을 그대로 가져온 것이다. 이처럼 wrapped를 class 밖에서 일반 함수로 선언하여도 정상적으로 쓸 수 있다. 그래서 static을 사용해야 하는 경우는 흔하지 않다고 한다.
export const wrapped = (parent, initialCount = 0) => {
const wrapper = document.createElement("div");
wrapper.className = "wrapper";
parent.append(wrapper);
return new Counter(wrapper, initialCount);
};
export class Counter {
...
}
객체 지향 프로그래밍에서 가장 중요한 원리 중 하나는 '내부 인터페이스와 외부 인터페이스를 구분 짓는 것’이다.
내부 인터페이스와 외부 인터페이스를 구분하는 방법을 ‘반드시’ 알고 있어야 한다.
커피머신에 내부/외부 인터페이스를 비유한 글
커피 머신은 꽤 믿음직한 기계입니다. 수년 간 사용할 수 있고, 중간에 고장이 나도 수리를 받으면 됩니다.
외형은 단순하지만 커피 머신을 신뢰할 수 있는 이유는 모든 세부 요소들이 기계 내부에 잘 정리되어 숨겨져 있기 때문입니다.
커피 머신에서 보호 커버를 제거하면 사용법이 훨씬 복잡해지고 위험한 상황이 생길 수 있습니다. 어디를 눌러야 할지 모르고 감전이 될 수도 있기 때문입니다.
앞으로 학습하겠지만, 프로그래밍에서 객체는 커피 머신과 같습니다.
프로그래밍에서는 보호 커버를 사용하는 대신 특별한 문법과 컨벤션을 사용해 안쪽 세부 사항을 숨긴다는 점이 다릅니다.
내부 인터페이스(internal interface) – 동일한 클래스 내의 다른 메서드에선 접근할 수 있지만, 클래스 밖에선 접근할 수 없는 프로퍼티와 메서드
외부 인터페이스(external interface) – 클래스 밖에서도 접근 가능한 프로퍼티와 메서드
(예) 커피 머신은 보호 커버에 둘러싸여 있기 때문에 보호 커버를 벗기지 않고는 커피머신 외부에서 내부로 접근할 수 없다. 밖에선 세부 요소를 알 수 없고, 접근도 불가능하다. 내부 인터페이스의 기능은 외부 인터페이스를 통해야만 사용할 수 있다.
이런 특징 때문에 외부 인터페이스만 알아도 객체를 가지고 무언가를 할 수 있다. 객체 안이 어떻게 동작하는지 알지 못해도 괜찮다는 점은 큰 장점으로 작용한다.
이건 이번 우테코 프리코스 3주 차 미션 파일에 등장하는 개념이라서 주의깊게 보자.
#
으로 시작한다. #
이 붙으면 클래스 안에서만 접근할 수 있다.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
우테코 프리코스 2주 차 미션을 수행하며 처음 접한 class가 무엇인지 정확하게 파악하지 못하고 문제를 풀었다. 그래서 3주 차인 지금이라도 class에 대한 개념을 잡고 싶어서 포스팅을 하게되었다.(특히 class 안에서 메서드를 쓸 때 this나 static을 어떻게 써야하는지가 궁금했다)
사실 인터넷에서 공부한 내용만으로 공부하여 포스팅을 한번 했었는데, static을 왜 쓰는지 이해가 안가서 시니하 님께 질문하게 되었다. 시니하 님이 직접 코드를 짜서 설명해주신 뒤에야 궁금증이 시원하게 해결된 것 같다!!! 그래서 새롭게 알게 된 내용을 포스팅에 추가했다. 감사합니다 감사합니다..🥺🥺
그리고 코드 리뷰에서 스터디 팀원이 지적해주지 않았다면 절대 찾아서 공부하지 못했을 것 같다. 무엇을 모르는지도 모르는 상태였으니까...! 이부분을 콕 집어서 리뷰해준 팀원에게 감사의 말씀을 전하고 싶다😎
캬