자바스크립트 객체지향 기본기

코드잇의 자바스크립트 객체지향 기본기 강의를 듣고 정리한 내용입니다.
문제가 될 시 삭제하겠습니다.

 

코드잇 자바스크립트 객체지향 기본기

1. 객체와 클래스

01. 객체 지향 프로그래밍이란 ?

  • 객체 : 객체의 상태를 나타내는 변수와 함수가 있음
  • 객체 지향 프로그래밍 : 프로퍼티와 메소드로 이루어진 각 객체들의 상호 작용을 중심으로 코드를 작성하는 것
  • 절차 지향 프로그래밍 : 변수와 함수를 가지고 작업의 순서에 맞게 코드를 작성하는 것

02. 1-1. object literal

  • Object Literal : 객체를 나타내는 문자열
  • index.html
<!DOCTYPE html>
<html lang="ko">
    <head>
        <meta charset="UTF-8">
        <title>자바스크립트 객체 지향</title>
    </head>
    <body>
        <script src="./index.js"></script>
    </body>
</html>
  • index.js
console.log('test');

const user = {
    // property 속성 : 객체의 상태를 담음 
    email: 'chris@google.com',
    birthdate: '1991-05-11',

    // method 함수
    buy(item) {
        console.log(`${this.email} buys ${item.name}`); //this 는 현재 객체의 email
    },
};

const item = {
    name: '스웨터',
    price: 30000,
};

// 객체의 property 속성과, method 함수 실행
console.log(user.email); // chris@google.com
console.log(user.birthdate); // 1991-05-11
user.buy(item); // chris@google.com buys 스웨터
  • 실행결과
test
index.js:21 chris@google.com
index.js:22 1991-05-11
index.js:12 chris@google.com buys 스웨터

03. 1-2. Factory function

  • index.js
  • 객체를 생성하는 Factory function을 만들고, 그 안에서 Object literal로 객체를 생성하여 리턴하는 방법입니다.
function createUser(email, birthdate) {
    const user = {
                // email property 와 email 파라미터가 같은 경우에는 email: email, -> email, 로 변경해도 됨
        email: email,
        birthdate: birthdate,
        buy(item) {
            console.log(`${this.email} buys ${item.name}`);
        },
    };
    return user;
}

const item = {
    name: '스웨터',
    price: 30000,
};

// Factory Function 
const user1 = createUser('minimi@google.com', '1991-05-11');
const user2 = createUser('test@google.com', '1995-01-11');
console.log(user1.email);
console.log(user2.email);

user1.buy(item);
user2.buy(item);
  • 결과값
minimi@google.com
index.js:22 test@google.com
index.js:7 minimi@google.com buys 스웨터
index.js:7 test@google.com buys 스웨터

04. Constructor function

  • 생성자 함수
// constructor function
// this는 해당 객체를 의미 
// 객체 생성용 메서드는 User 처럼 맨 앞글자를 대문자로 함(관습)
function User(email, bitrhdate) {
    this.email = email;
    this.birthdate = birthdate;
    this.buy = function (item) {
        console.log(`${this.email} buys ${item.name}`);
    };
}

const item = {
    name: '스웨터',
    price : 30000,
};

// new 를 붙여야 객체를 생성할 수 있음
const user1 = new User('minimi@gmail.com', '1991-03-21');
console.log(user1.email);
console.log(user1.birthdate);
user1.buy(item);

05. 객체 만들기3 : Class

class User {
    // this를 사용하여 변수를 할당한다.
    constructor(email, birthdate){
        this.email = email;
        this.birthdate = birthdate;
    }

    // 메소드는 constructor 안이 아닌 밖에 위치해아 한다. 
    buy(item) {
        console.log(`${this.email} buys ${item.name}`);
    }
}

const item = {
    name: '스웨터',
    price: 30000,
};

const user1 = new User('minimi@google.com', '1992-03-21');
console.log(user1.email);
console.log(user1.birthdate);
user1.buy(item);

const user2 = new User('minimi2@google.com', '1992-03-21');
console.log(user2.email);
console.log(user2.birthdate);
user2.buy(item);

2. 객체 지향 프로그래밍의 4개의 기둥

01. 추상화

추상화 : 어떤 구체적인 존재를 원하는 방향으로 간략화해서 나타내는 것

03. 캡슐화

캡슐화 : 객체의 특정 프로퍼티에 직접 접근하지 못하도록 막는 것

  • setter메소드
class User {
    constructor(email, birthdate) {
        this.email = email;
        this.birthdate = birthdate;
    }

    buy(item) {
        console.log(`${this.email} buys ${item.name}`);
    }

    get email() {
        return this._email;
    }

    set email(address) { // 그냥 email은 setter 메소드의 이름이 됨
        if (address.includes('@')) { // 값에 대한 유효성 검사
            this._email = address; // _email 에 address를 저장함
        } else {
            throw new Error('invalid email address');
        }
    }
}

const item = {
    name: '스웨터',
    price: 30000,
};

const user1 = new User('chris123@google.com', '1992-03-21')
user1.email = 'chris_robert@google.com';
console.log(user1._email); // -> getter 메소드(get email()) 메소드가 구현되어 있으면 console.log(user1.email); 로 사용 가능

04. 캡슐화 더 알아보기

1. 완벽한 캡슐화를 하는 법

이전 영상에서는 다음 코드로 캡슐화를 배웠습니다.

class User {
  constructor(email, birthdate) {
    this.email = email;
    this.birthdate = birthdate;
  }

  buy(item) {
    console.log(`${this.email} buys ${item.name}`);
  }

  get email() {
    return this._email;
  }

  set email(address) {
    if (address.includes('@')) {
      this._email = address;
    } else {
      throw new Error('invalid email address');
    }
  }
}

const user1 = new User('chris123@google.com', '1992-03-21');
user1.email = 'newChris123@google.com';
console.log(user1.email);

이제 이 코드를 보면 _email 프로퍼티에 직접 접근하지 말고, email이라는 getter/setter 메소드로만 접근해야 한다는 것이 눈에 잘 보입니다. 하지만 사실 완벽한 캡슐화가 된 상태는 아닙니다. 왜냐하면 보호하려는 프로퍼티 _email

console.log(user1._email);
user1._email = 'chris robert';

이런 식으로 여전히 직접 접근할 수는 있기 때문입니다.

사실 자바스크립트에는 캡슐화를 자체적으로 지원하는 문법이 아직 없습니다.(Java는 private이라는 키워드가 있어서 언어의 문법 차원에서 캡슐화를 지원합니다.)

하지만 JavaScript에서도 다른 방식으로 우회해서 완벽한 캡슐화를 할 수는 있는데요. 클로저(Closure)라고 하는 개념을 응용해서 적용하면 됩니다. 잠깐 아래 코드를 보세요.

function createUser(email, birthdate) {
  let _email = email;

  const user = {
    birthdate,

    get email() {
      return _email;
    },

    set email(address) {
      if (address.includes('@')) {
        _email = address;
      } else {
        throw new Error('invalid email address');
      }
    },
  };

  return user;
}

const user1 = createUser('chris123@google.com', '19920321');
console.log(user1.email);

지금 이 코드를 보면 createUser라고 하는 Factory function이 보입니다. 그런데 생성하려는 user 객체 안에 _email 프로퍼티가 있는 게 아니라,

(1) createUser 함수 안에,
(2) 그리고 user 객체 바깥에 _email이라는 변수가 있죠?

대신에 user 객체 안에는 _email 변수의 값을 읽고 쓸 수 있는 email이라는 getter/setter 메소드가 있습니다.

지금 마지막 부분에서 createUser라는 Factory function으로 user1이라는 객체를 생성하고, user1 객체의 email getter 메소드를 호출했는데요. 이 코드의 실행 결과를 확인해보면,

https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=4462&directory=Untitled.png&name=Untitled.png

이렇게 _email 변수의 값이 잘 출력됩니다. 함수 안의 변수의 값을 이미 리턴된 객체에서 읽은 건데요. 어떻게 이게 가능한 걸까요? 이것은 자바스크립트의 클로저(Closure)라고 하는 것 덕분에 가능합니다.

클로저란 자바스크립트에서 어떤 함수와 그 함수가 참조할 수 있는 값들로 이루어진 환경을 하나로 묶은 것을 의미하는데요. 예를 들어, 지금 createUser 함수가 실행되는 시점에 email이라는 getter/setter 메소드는 _email 이라는 변수의 값에 접근할 수 있는 상태입니다. 그리고 여기서 핵심은 이 email getter/setter 메소드들은 메소드를 갖고 있는 객체가 리턴된 이후더라도 여전히 _email에 접근하는 것이 가능하다는 점입니다. 바로 이렇게 함수가 정의된 당시에 참조할 수 있었던 변수들을 계속 참조할 수 있는 상태의 함수를 클로저라고 합니다. 이 클로저는 다른 프로그래밍 언어에서는 쉽게 찾아보기 힘든 자바스크립트만의 특징인데요.(물론 클로저 개념이 있는 다른 언어들도 있습니다)

보통 다른 프로그래밍 언어였다면 createUser 함수 내부가 실행될 때만 email getter/setter 메소드가 _email 변수에 접근할 수 있었겠지만, 자바스크립트에서는 클로저라는 개념으로 해당 환경을 함수와 함께 그대로 유지시켜주는 것입니다.

만약 클로저가 아닌 경우에는 _email 변수에 접근할 수 없습니다. 만약 이런 식으로

function createUser(email, birthdate) {
  let _email = email;

  const user = {
    birthdate,

    get email() {
      return _email;
    },

    set email(address) {
      if (address.includes('@')) {
        _email = address;
      } else {
        throw new Error('invalid email address');
      }
    },
  };

  return user;
}

const user1 = createUser('chris123@google.com', '19920321');
console.log(user1._email);// _ 추가

user1 객체의 _email 프로퍼티에 접근하려고 하면, user1 객체 자체 내에는 _email이라고 하는 프로퍼티가 없고, 바깥의 _email 변수에 현재 접근할 수도 없기 때문에

https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=4462&directory=Untitled%201.png&name=Untitled+1.png

undefined가 출력됩니다.

이런 식으로 자바스크립트에서는 클로저를 사용해서 완벽한 캡슐화를 할 수 있습니다. 신기하죠? 사실 자바스크립트로 프로그래밍을 할 때 캡슐화가 얼마나 중요한지, 꼭 해야하는지에 관해서는 논란이 많습니다. 하지만 어떤 상황이든 이런 식으로 완벽하게 캡슐화를 할 수 있다 정도는 알아두는 게 좋습니다.

2. 메소드도 캡슐화할 수 있어요

이때까지 우리는 프로퍼티를 보호하기 위해 getter/setter 메소드를 활용하거나, 좀더 완벽한 캡슐화를 위해 클로저를 사용할 수 있다는 것을 배웠습니다. 그런데 사실 프로퍼티 뿐만 아니라 메소드를 캡슐화하는 것도 가능합니다. 잠깐 이 코드를 볼까요?

function createUser(email, birthdate) {
  const _email = email;
  let _point = 0;

  function increasePoint() {
    _point += 1;
  }

  const user = {
    birthdate,

    get email() {
      return _email;
    },

    get point() {
      return _point;
    },

    buy(item) {
      console.log(`${this.email} buys ${item.name}`);
      increasePoint();
    },
  };

  return user;
}

const item = {
  name: '스웨터',
  price: 30000,
};

const user1 = createUser('chris123@google.com', '19920321');
user1.buy(item);
user1.buy(item);
user1.buy(item);
console.log(user1.point);

저는 _point라는 변수를 추가했는데요. 사용자가 물건을 살 때마다 1포인트씩 적립해 줄 목적으로 만든 변수입니다. 그리고 point getter 메소드도 지금 정의해둔 상태입니다. _point 변수를 1씩 늘려주는 함수는 바로 밑에 보이는 increasePoint라는 함수입니다.

increasePoint 라는 함수는 유저 객체의 buy 메소드 안에서 쓰이고 있는데요. buy 메소드를 실행할 때 그 안에서 increasePoint 함수도 호출을 해주는 겁니다. 맨 마지막 부분의 코드들을 보면 user1 객체의 buy 메소드를 호출하고 point getter 메소드를 호출하고 있는데요. 이 코드를 실행해보면

https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=4462&directory=Untitled%202.png&name=Untitled+2.png

이렇게 스웨터를 3번 구매했을 때, 포인트는 총 3점이 쌓이게 됩니다.

자, 여기서 중요한 점은 지금 increasePoint라는 함수가 보호받고 있는 함수라는 점입니다. 지금 user1 객체로 바로 increasePoint 함수를 호출할 수는 없습니다. 호출하려고 하면

function createUser(email, birthdate) {
  const _email = email;
  let _point = 0;

  function increasePoint() {
    _point += 1;
  }

  const user = {
    birthdate,

    get email() {
      return _email;
    },

    get point() {
      return _point;
    },

    buy(item) {
      console.log(`${this.email} buys ${item.name}`);
      increasePoint();
    },
  };

  return user;
}

const item = {
  name: '스웨터',
  price: 30000,
};

const user1 = createUser('chris123@google.com', '19920321');
user1.buy(item);
user1.buy(item);
user1.buy(item);
console.log(user1.point);
user1.increasePoint();// user1 객체로 increasePoint 직접 호출

https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=4462&directory=Untitled%203.png&name=Untitled+3.png

이렇게 그런 함수가 없다는 에러가 출력됩니다. 왜냐하면 user1 객체에는 increasePoint라는 메소드가 없기 때문입니다. 지금 저는 increasePoint가 유저 객체 안에서 적절한 곳에 사용되어야 하고, 아무렇게나 함부로 호출해서는 안 되는 메소드라고 가정하고 이렇게 캡슐화를 한 것입니다. 이런 식으로 메소드(정확하게 말하자면 increasePoint가 메소드는 아니니까 함수라고 할 수 있겠죠?)도 프로퍼티와 마찬가지로 클로저를 통해 캡슐화를 해서 보호할 수 있다는 사실, 잘 기억하세요.

06. 상속

  • 자식이 부모의 프로퍼티와 메소드를 물려받을 때
  • 코드의 재사용 성이 좋아진다.
class User {
    constructor(email, birthdate) {
        this.email = email;
        this.birthdate = birthdate;
    }

    buy(item) {
        console.log(`${this.email} buys ${item.name}`);
    }
}

class PremiumUser extends User {
    constructor(email, birthdate, level) {
        this.level= level;
    }

    streamMusicForFree() {
        console.log(`Free music streaming for ${this.email}`);
    }
}

const item = {
    name: '스웨터',
    price: 30000,
}

const pUser1 = new PremiumUser('chris123@googl.com', '1992-03-21')
console.log(pUser1.email);
console.log(pUser1.birthdate);
console.log(pUser1.level);
pUser1.buy(item);
pUser1.streamMusicForFree();

07. super

  • 자식 클래스의 생성자 함수 안에서 부모클래스의 생성자 함수를 먼저 실행해주어야 함
class User {
    constructor(email, birthdate) {
        this.email = email;
        this.birthdate = birthdate;
    }

    buy(item) {
        console.log(`${this.email} buys ${item.name}`);
    }
}

class PremiumUser extends User {
    constructor(email, birthdate, level) {
        super(email, birthdate);
        this.level= level;
    }

    streamMusicForFree() {
        console.log(`Free music streaming for ${this.email}`);
    }
}

const item = {
    name: '스웨터',
    price: 30000,
}

const pUser1 = new PremiumUser('chris123@googl.com', '1992-03-21')
console.log(pUser1.email);
console.log(pUser1.birthdate);
console.log(pUser1.level);
pUser1.buy(item);
pUser1.streamMusicForFree();

09. 다형성

  • 다형성 : 많은 형태를 갖고 있는 성질

class User {
    constructor(email, birthdate) {
        this.email = email;
        this.birthdate = birthdate;
    }

    buy(item) {
        console.log(`${this.email} buys ${item.name}`);
    }
}

class PremiumUser extends User {
    constructor(email, birthdate, level) {
        super(email, birthdate);
        this.level= level;
    }

    streamMusicForFree() {
        console.log(`Free music streaming for ${this.email}`);
    }

    buy(item) {
        console.log(`${this.email} buys ${item.name} with a 5% discount`);
    }
}

// 오버라이딩 (overriding)
const item = {
    name: '스웨터',
    price: 30000,
}

const user1 = new User('sdf@google.com', '19910511');
const pUser1 = new PremiumUser('asd@google.com', '19911210');

user1.buy(item);
pUser1.buy(item);

10. 부모 클래스의 메소드가 필요하다면?

  • super 키워드를 사용한다.
class User {
    constructor(email, birthdate) {
        this.email = email;
        this.birthdate = birthdate;
    }

    buy(item) {
        console.log(`${this.email} buys ${item.name}`);
    }
}

class PremiumUser extends User {
    constructor(email, birthdate, level, point) {
        super(email, birthdate);
        this.level= level;
        this.point = point;
    }

    streamMusicForFree() {
        console.log(`Free music streaming for ${this.email}`);
    }

    buy(item) {
        super.buy(item);
        this.point += item.price * 0.05;
    }
}

12. instanceof 연산자

  • instanceof 메소드로 어느 클래스로 만든 객체인지를 확인할 수 있음
  • 상속받아서 만든 객체는 부모클래스 객체임을 확인할 수 있다.
const users = [user1, pUser1, user2, pUser2];

users.forEach(user) => {
    console.log(user instanceof User);
});

13. static 프로퍼티와 static 메소드

  • static 프로퍼티
  • static 메소드

클래스에 직접적으로 딸려있는 프로퍼티와 메소드

객체가 아닌 클래스 자체로 접근 !

class Math {
    static PI = 3.14;
    static getCircleArea(radius) {
        return Math.Pi * radius * radius;
    }
}

Math.PI = 3.141592;
Math.getRectangleArea = function (width, height) {
    return width * height;
}

console.log(Math.PI); // 3.14
console.log(Math.getCircleArea(5)); // 78.5

댓글

Designed by JB FACTORY