[javascript] 스터디 4주차 - 자바스크립트 코딩의 기술 8장 요약

5 minute read

8. 클래스로 인터페이스를 간결하게 하라

8.1. 읽기 쉬운 클래스를 만들어라.

ES6 부터 자바스크립트에서 클래스 문법을 사용할 수 있게 됐다!

자바스크립트에서 클래스를 작성하는 방법을 살펴보도록 하겠다.

// Coupon 클래스를 작성한다.
class Coupon{
    // 생성자
    constructor(price, expiration){
        this.price = price;
        this.expiration = expiration || '2주';
    }
    // 메서드
    getPriceText(){
        return `$${this.price}`;
    }
    getExpirationMessage(){
        return `이 쿠폰은 ${this.expiration} 후에 만료됩니다.`;
    }
}
// 인스턴스 생성
const coupon = new Coupon(5);
coupon.getPriceText();  // '$5'
coupon.getExpirationMessage(); // '이 쿠폰은 2주 후에 만료됩니다.'

자바스크립트에서 생성자는 constructor를 통해 지정할 수 있다. 또한 자바스크립트 클래스는 모든 속성이 public이다. (따로 설정하지 않는다.)

클래스의 메서드를 클래스의 인스턴스에서 호출하면 예외적인 경우를 제외하고 this문맥에 완전하게 접근할 수 있다.

8.2. 상속으로 메서드를 공유하라.

초기 버전 자바스크립트에서 클래스 상속을 구현하려면 복잡한 절차가 필요했으나, 클래스를 사용하면 상속이 간단해진다. 간단해졌지만 사용할 시에 주의가 필요하다고 한다.

이전에 만든 Coupon 클래스를 상속받는 FlashCoupon을 만드는 예제를 살펴보자.

import Coupon from './extend';
class FlashCoupon extends Coupon{
    constructor(price, expiration){
        super(price);   // 부모 클래스 생성자 호출
        this.expiration = expiration || '2시간';    // expiration 값 재정의
    }
    // 오버라이딩
    getExpirationMessage(){
        return `이 쿠폰은 한정쿠폰이며 ${this.expiration} 후에 만료됩니다.`;
    }
}
const flash = new FlashCoupon(10);
flash.getPriceText();   // '$10'
flash.getExpirationMessage(); // '이 쿠폰은 한정쿠폰이며 2시간 후에 만료됩니다.'

Coupon클래스에 메서드를 추가하고 오버라이딩 해보자.

class Coupon{
    ...
    // user의 추가할인 조건 체크
    isRewardEligible(user){
        // 최근 접속자이며 받을 자격이 있는 경우
        return user.rewardsEligible && user.active;
    }
    // 사용자 정보를 받는 메서드
    getReward(user){
        if(this.isRewardEligible(user)){    // 메서드 호출
            this.price = this.price * 0.9;
        }
    }
}
export default Coupon;
import Coupon from './extend';
class FlashCoupon extends Coupon{
    ...
    // 메서드 오버라이딩
    isRewardEligible(user){
        return super.isRewardEligible(user) && this.price > 20;
    }
    getReward(user){
        if(this.isRewardEligible(user)){
            this.price = this.price * 0.8;
        }
    }
}

상속받은 FlashCouponCoupon이 가진 모든 요소를 가지고있으며, 재정의 할 수 있다. 그러나 자바스크립트는 프로토타입 기반 언어인 점을 명심해야 한다.

8.3. 클래스로 기존의 프로토타입을 확장하라.

기존의 프로토타입과 함께 클래스를 사용하는 방법을 살펴보도록 한다.

그전에 전통적인 객체지향 언어와 자바스크립트의 차이점을 확인해보자.

프로토타입 언어와 객체지향 언어의 차이점

전통적인 객체지향 언어 - ruby

클래스가 객체를 위한 청사진이되며, 인스턴스를 생성하면 새로운 객체에 모든 속성과 메서드가 복제된다.

프로토타입 언어 - 자바스크립트

새로운 인스턴스를 생성할 때 메서드를 복사하지 않고, 프로토타입에 대한 연결을 생성한다. (__proto__) 그래서 객체의 인스턴스에 있는 메서드를 메서드를 호출할 때, 프로토타입에 있는 메서드를 호출하게 된다.

🔗 [참고] 프로토타입 기반 언어, 자바스크립트 - TOAST UI

결과적으로 자바스크립트에서 class가 새로생긴게 아니라 프로토타입을 편하게 사용하는 방법이 추가된 것으로 이해하면 되겠다.

그렇기 때문에 프로토타입을 이용해서 생성한 레거시 코드도 클래스를 사용할 수 있는데, 이전에 예제로 보았던 Coupon이 프로토타입 이었더라도, 클래스문법을 적용해 extends를 통해 확장할 수 있다는 뜻이다!

8.4. get/set 으로 인터페이스를 단순하게 만들어라.

자바스크립트 클래스에 공개범위를 설정할 수 없으므로, getset을 이용하여 의도치 않은 값이 들어가지 않도록 하는 방법을 알아본다.

게터는 get 을 사용하고 세터는 set을 사용하여 만들어주는데, 생성자에서 사용되는 원본 속성은 _를 붙여 가교역할을 하도록 만들고 게터와 세터에 동일한 이름을 사용하여 메서드를 정의하면 실제로 호출할 때는 아래의 예제코드 처럼 =를 통해 세터를 호출해서 할당하고, 게터에 명시한 속성명에 직접 접근했을때 값을 조회할 수 있다.

class Coupon{
    constructor(price, expiration){
        this._price = price;    // 비공개라는 점을 명시 
        this.expiration = expiration || '2주';
    }
    // getter
    get priceText(){
        return `$${this._price}`;
    }
    get price(){
        return this._price;
    }
    // setter
    set price(price){
        // 정수 외 문자를 제거
        const newPrice = price.toString().replace(/[^\d]/g, '');
        this._price = parseInt(newPrice, 10);
    }
    ...
}

const coupon = new Coupon(5);
coupon.price;   // 5 , getter 
coupon.price = '$10';   // setter
coupon.price;   // 10, getter
coupone.priceText;  // '$10'

게터와 세터의 장점은 복잡도를 숨길 수 있다는 점이다.

8.5. 제네레이터로 이터러블 속성을 생성하라.

제네레이터를 이용해 복잡한 데이터 구조를 iterable로 변환하는 방법을 살펴본다.

객체에는 내장된 이터레이터가 없어 직접적으로 순회할 수 없다. 그러나 제네레이터라는 함수를 사용하면 데이터를 단순한 구조로 변환하여 데이터를 배열처럼 쉽게 다룰 수 있다 한다. 또한 제네레이터는 클래스에만 국한되지 않는 특화된 함수이다.

제네레이터

제네레이터를 생성하려면 function 키워드 뒤에 *를 추가한다. 그러면 함수의 일부를 반환하는 next() 메서드를 쓸 수 있고, 함수 몸체 안에서는 yield키워드를 이용해 정보를 반환한다. 정보는 객체로 반환되는데, value 속성에 값이 들어가고, done에 마지막 항목인지 여부(boolean)가 들어간다.

https://github.com/gilbutITbook/007030/blob/master/classes/generators/simple.js

또한 제네레이터 함수를 펼침연산자를 사용해 배열에 담거나, for...of를 사용하여 담을 수 있다.

class FamilyTree{
    constructor(){
        this.fmaily = {
            name: 'Doris',
            child: {
                name: 'Martha',
                child: {
                    name: 'Dyan',
                    child: {
                        name: 'Bea',
                    },
                },
            },
        };
    }
    // getMembers(){
    //     const family = [];
    //     let node = this.family;
    //     // child가 존재하는 동안 반복
    //     while(node){
    //         family.push(node.name);
    //         node = node.child;
    //     }
    //     return family;
    // }
    
    // 클래스의 이터러블에 제네레이터를 연결한다.
    * [Symbol.iterator](){
        let node = this.family;
        while(node){
            // 반복되는 매 회마다 yield로 값을 넘겨준다!
            yield node.name;
            node = node.child;
        }
    }
}

const family = new FamilyTress();
// family.getMembers();    // ['Doris', 'Martha', 'Dyan', 'Bea'];

// 이터러블이 있어서 바로 호출이 가능하다.
[...family];    // ['Doris', 'Martha', 'Dyan', 'Bea'];

8.6. bind()로 문맥 문제를 해결하라.

7장 화살표 함수로 문맥혼동을 피하라 에서 언급되었던 내용이다.

문맥 문제를 예방하는 기법을 살펴보게 되는데, 클래스 문법과 사용하는 것이 일반적이며 리액트나 앵귤러 같은 라이브러리를 사용하게 되면 흔히 볼 수 있다 한다.

class Validator{
    constructor(){
        this.message = '가 유효하지 않습니다.';
    }
    setInvalidMessage(field){
        return `${field}${this.message}`;
    }
    setInvalidMessages(...fields){
        // 콜백에 setInvalidMessage를 전달
        return fields.map(this.setInvalidMessage);
    }
}
const validator = new Validator();
validator.setInvalidMessages('도시');
// TypeError: Cannot read property 'message' of undefined

이전에 다루어봤던 예제를 클래스형식으로 바꿨다. 이 때도 콜백에 메서드를 전달할 경우 문맥 오류가 생기게된다.

이때 해결책 첫번째는 화살표 함수를 사용하는 것이다.

class Validator{
    constructor(){
        this.message = '가 유효하지 않습니다.';
        // 콜백에 전달되는 메서드를 화살표함수로 바꾸고 속성으로 옮겨야한다.
        this.setInvalidMessage = field => `${field}${this.message}`;
    }
    setInvalidMessages(...fields){
        return fields.map(this.setInvalidMessage);
    }
}

위 코드 처럼 속성에 메서드를 옮겨서 화살표함수로 선언하면 문맥 문제가 발생하지않는다. 그러나 메서드가 생성자 내부에도 외부에도 정의되는 문제가 있기 때문에 bind()를 이용한 해결책을 권장한다.

https://github.com/gilbutITbook/007030/blob/master/classes/bind/bind.js

bind()를 이용하면 특정한 객체를 명시적으로 this로 설정할 수 있다.

const sayAlert = sayMessage.bind(alert);
sayAlert();     // sayMessage 메서드에 쓰이는 this 객체를 alert으로 지정.

앞서 본 예제를 bind()를 이용하여 문맥을 설정하면 다음과 같다.

class Validator {
  constructor() {
    this.message = '가 유효하지 않습니다.';
    // 문맥을 생성자에서 설정
    this.setInvalidMessage = this.setInvalidMessage.bind(this);
  }

  setInvalidMessage(field) {
    return `${field}${this.message}`;
  }

  setInvalidMessages(...fields) {
    return fields.map(this.setInvalidMessage);
  }
}
const validator = new Validator();
validator.setInvalidMessages.bind('도시');
// '도시가 유효하지 않습니다.';

추후에는 생성자 밖에서 클래스 속성을 설정하는 명세가 도입될 것이라고 하는데, 바벨 플러그인을 사용하면 이 기능을 사용할 수 있다고 한다. 현재는 지원되는 버전이 없어서 실행해볼 수 없다.

요약

  • ES6+부터 자바스크립트에서도 클래스문법을 사용할 수 있으나, 여전히 프로토타입 기반이라는 점에 유의하자. (+기존 문법으로 만들어진 클래스도 확장할 수 있다.)
  • 생성자는 constructor를 사용하여 선언하며, JS 클래스의 속성은 생성자에서 정의한다.*
  • extends를 사용하여 클래스 상속을 할 수 있으며 메서드 재정의를 할 수 있다.
  • getset으로 게터와 세터를 구현할 수 있으며, 원본 속성은 _를 붙여 숨긴다.
  • 제네레이터 * [Symbol.iterator]()를 사용하면 클래스의 속성에 대한 이터러블을 만들 수 있다.
  • bind()를 사용하면 명시적으로 특정 객체를 this로 설정한다.

Categories:

Updated:

Comments