자바스크립트 중급 #1 클래스

자바스크립트 중급 시리즈 첫 글입니다. 기초 7편을 마쳤다면 이미 작은 도구나 스크립트는 자신 있게 짤 수 있을 텐데, 중급은 그 위에 모던 자바스크립트의 표현력을 얹는 단계입니다.

총 7편으로 구성됩니다.

  • #1 클래스 ← 이번 글
  • #2 비동기 입문 — Promise, async/await
  • #3 이터레이터와 제너레이터
  • #4 디스트럭처링/spread/rest 깊이
  • #5 옵셔널 체이닝과 nullish 병합
  • #6 fetch API와 에러 처리
  • #7 JSON 다루기와 직렬화

이번 글에서는 — 자바스크립트의 클래스 문법을 처음부터 끝까지 정리합니다.

클래스 기본 #

첫 클래스
class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log(`안녕, ${this.name} (${this.age})`);
  }
}

const u = new User('커티스', 30);
u.greet();   // 안녕, 커티스 (30)

class 키워드로 시작하고, **constructor**가 인스턴스를 만들 때 호출되는 초기화 함수입니다. 본문 안의 this는 새로 만들어지는 인스턴스를 가리킵니다.

new 없이 호출하면 에러가 나요 — 클래스는 항상 new와 함께 씁니다.

new 필수
User('커티스', 30);   // ✗ TypeError
new User('커티스', 30);  // OK

메서드와 this #

메서드 안의 this는 호출하는 인스턴스를 가리키는 게 기본입니다. 다만 함수를 떼어 호출하면 this가 사라질 수 있습니다.

this가 사라지는 경우
class User {
  constructor(name) {
    this.name = name;
  }
  greet() {
    console.log(`안녕, ${this.name}`);
  }
}

const u = new User('커티스');
u.greet();             // 안녕, 커티스

const fn = u.greet;     // 떼어내기
fn();                    // 안녕, undefined  ← this 사라짐

이게 자바스크립트 클래스의 가장 흔한 함정입니다. 콜백으로 메서드를 넘기거나 이벤트 핸들러로 등록할 때 자주 만납니다. 해결법은 다음 두 가지.

1) 화살표 함수로 묶기 #

콜백에 화살표 함수
button.addEventListener('click', () => u.greet());

2) bind 또는 클래스 필드로 화살표 함수 #

필드 화살표 — this가 묶인 채로 굳어짐
class User {
  constructor(name) {
    this.name = name;
  }
  greet = () => {
    console.log(`안녕, ${this.name}`);
  };
}

const u = new User('커티스');
const fn = u.greet;
fn();     // 안녕, 커티스 — 떼어내도 this 살아 있음

이 패턴은 리액트 콜백에서 자주 봤을 것입니다. 다만 메서드가 인스턴스마다 새로 만들어지는 비용이 있어 무거운 클래스에는 권장되지 않습니다.

getter / setter — 속성처럼 보이는 메서드 #

getter / setter
class Temperature {
  constructor(celsius) {
    this._celsius = celsius;
  }

  get fahrenheit() {
    return this._celsius * 9 / 5 + 32;
  }

  set fahrenheit(value) {
    this._celsius = (value - 32) * 5 / 9;
  }
}

const t = new Temperature(25);
console.log(t.fahrenheit);   // 77 — 메서드 호출이 아니라 속성처럼 접근
t.fahrenheit = 100;
console.log(t._celsius);     // 약 37.7

get / set 키워드로 정의하면 호출 시 괄호 없이 속성처럼 읽고 쓸 수 있습니다. 계산해서 반환하거나 설정 시 검증 하고 싶을 때 적합합니다.

정적 멤버 — static #

인스턴스가 아니라 클래스 자체에 붙는 메서드나 속성입니다.

static
class MathUtil {
  static PI = 3.14159;

  static square(n) {
    return n * n;
  }
}

MathUtil.PI;          // 3.14159
MathUtil.square(5);   // 25

const m = new MathUtil();
m.square(5);          // ✗ 인스턴스에는 없음

유틸 함수 모음, 또는 인스턴스 생성을 돕는 팩토리 메서드(예: User.fromJSON(...))에 적합합니다.

static 블록 — ES2022 #

정적 멤버를 만들 때 복잡한 초기화 로직이 필요하면 static { ... } 블록을 쓸 수 있습니다.

static 블록
class Config {
  static defaults = {};

  static {
    Config.defaults = JSON.parse(loadConfigFile());
    Config.defaults.timestamp = Date.now();
  }
}

흔히 쓰진 않지만, 라이브러리 초기화에서 가끔 만나요.

Private 필드 — # #

ES2022에 추가된 정식 private 문법. 외부에서 접근 자체가 불가능 해집니다.

private 필드
class BankAccount {
  #balance = 0;

  deposit(amount) {
    this.#balance = this.#balance + amount;
  }

  get balance() {
    return this.#balance;
  }
}

const acc = new BankAccount();
acc.deposit(100);
console.log(acc.balance);     // 100
console.log(acc.#balance);    // ✗ SyntaxError — 외부 접근 불가

이름 앞에 #을 붙입니다. 컨벤션이 아니라 언어 차원에서 강제 되는 진짜 private 입니다. 옛 자바스크립트의 “관습적 private (_balance)” 와 달리, 외부에서 접근 자체가 막힙니다.

_언더스코어 와의 차이 #

옛 코드에 자주 보이는 _balance 같은 표기는 약속일 뿐입니다.

_언더스코어 — 외부에서 접근 가능
class OldStyle {
  constructor() {
    this._balance = 0;       // 컨벤션: 접근하지 마세요
  }
}

const o = new OldStyle();
o._balance = 999;            // 사실 접근 가능

새 코드에는 #을 쓰는 게 안전합니다.

상속 — extendssuper #

상속
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log(`${this.name} 가(이) 소리를 냅니다`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);             // 부모 constructor 호출
    this.breed = breed;
  }
  speak() {
    super.speak();           // 부모 메서드 호출
    console.log(`${this.name} 가(이) 짖습니다 (멍멍)`);
  }
}

const d = new Dog('보리', '시바');
d.speak();
// 보리 가(이) 소리를 냅니다
// 보리 가(이) 짖습니다 (멍멍)

extends로 부모 클래스를 지정하고, 자식 constructor 안에서는 super(...)를 먼저 부른 다음에야 this를 쓸 수 있습니다. 이걸 까먹으면 ReferenceError가 납니다.

super.method()로 부모의 메서드를 호출할 수 있고, 자식이 같은 이름의 메서드를 정의하면 부모 것을 가립니다(오버라이드).

상속 — 깊게 쓸 가치가 있나? #

상속은 자바스크립트뿐 아니라 OOP 전반에서 남용하면 코드가 빠르게 깨지는 도구입니다. 모던 자바스크립트는 다음을 권합니다.

  • 단일 클래스로 충분한 경우에 일부러 상속 만들지 말기
  • 상속 대신 합성(composition) — 객체에 다른 객체를 들고 있게 함
  • 인터페이스/공통 시그니처가 필요하면 함수와 객체로도 충분

리액트가 클래스 컴포넌트 → 함수 컴포넌트로 옮긴 것도 비슷한 맥락입니다. 클래스가 필수인 경우는 의외로 많지 않습니다.

instanceof — 클래스 체크 #

instanceof
const d = new Dog('보리', '시바');

d instanceof Dog;       // true
d instanceof Animal;    // true (부모도 true)
d instanceof Object;    // true
d instanceof User;       // false

상속 체인을 따라 검사합니다. 클래스로 만든 인스턴스인지 확인할 때 자주 써요.

클래스 vs 객체 리터럴 #

같은 일을 하려고 할 때, 클래스와 그냥 객체 중 어느 쪽이 적합한지 가이드.

상황적합한 쪽
같은 모양의 인스턴스 여러 개 만들기클래스
행동(메서드)이 데이터에 강하게 묶여 있음클래스
단일 인스턴스, 설정 객체 같은 경우객체 리터럴
함수형으로 변환만 거듭하는 경우그냥 함수 + 객체
상속이 필요할 만큼 계층이 깊음클래스 (또는 합성 재고려)

처음에는 클래스를 안 쓰고도 충분한 경우가 많습니다. 정말 클래스가 적합한 상황을 만났을 때 꺼내 쓴다고 생각하면 됩니다.

마무리 #

이번 글에서 정리한 내용:

  • classnew, constructor 안에서 this 초기화
  • 메서드를 떼어내면 this가 사라짐 — 화살표 함수 또는 필드 화살표
  • get/set으로 속성처럼 보이는 메서드
  • static으로 클래스 자체에 붙는 멤버, static {} 블록 (ES2022)
  • #필드로 진짜 private (_언더스코어는 약속일 뿐)
  • extends로 상속, super(...) 먼저 호출
  • 상속 남용 주의 — 모던 컨벤션은 합성 우선
  • 클래스가 적합한 경우 vs 객체 리터럴

다음 글(#2 비동기 입문)에서는 자바스크립트의 가장 큰 특징 중 하나인 비동기 — Promise와 async/await를 처음부터 정리합니다.

X