-
ES6 - class로 작성한 OOP개발 2020. 5. 8. 22:33
ES6부터 추가된 클래스 구문은 이 전까지 사용되어 왔던 객체지향 프로그래밍의 생성자 정의문의 문법 설탕 입니다.
클래스 구문을 사용하면 다양한 종류의 생성자 정의문과 생성자 상속 방법을 통일된 문법으로 간결하게 표현할 수 있습니다.
클래스 구문 종류에는 클래스 선언문과 클래스 표현식이 있습니다.
먼저 클래스를 적용하지 않은 다음과 같은 생성자 함수를 살펴보겠습니다.
function Person(name, first, second){ this.name = name; this.first = first; this.second = second; } Person.prototype.sum = function(){ return this.first + this.second; }
앞의 코드를 클래스 선언문과 클래스 표현식으로 작성하는 방법에 대해 알아보겠습니다.
클래스 선언문으로 정의하기
class Person{ // 생성자를 사용한 초기화 constructor(name, first, second){ this.name = name; this.first = first; this. second = second; } // prototype 메서드 sum(){ return this.first + this.second; } } let kim = new Person( "kim", 10, 20 ); console.log(kim); // Person {name: "kim", first: 10, second: 20} console.log(kim.sum()); // 30
이 코드로 생성자 함수 Person을 정의하게 됩니다.
실제로 인스턴스를 생성한 결과를 콘솔로 확인하면 의도한대로 잘 출력되는 것을 볼 수 있습니다.
클래스 선언문을 작성하는 방법
- class 키워드 뒤에 생성자 함수의 이름을 표기합니다. 이 함수 이름은 클래스 이름입니다.
- {...} 안은 클래스 몸통(class body)이라고 합니다. 클래스 몸통에는 클래스 멤버를 정의합니다.
클래스 멤버는 함수 선언문에서 function 키워드를 생략한 표현식 입니다. - 클래스의 멤버인 constructor () {...}에는 특별한 의미가 있습니다. constructor는 생성자로 객체를 생성할 때 초기화 처리를 담당하는 메서드 입니다. 지금까지 생성자 함수에 작성했던 작업들을 이곳에 작성해서 객체에 프로퍼티를 추가할 수 있습니다.
- constructor 다음에 작성된 클래스 멤버는 생성자 함수의 prototype에 메서드로 추가됩니다.
클래스 선언문으로 정의한 생성자는 함수 선언문으로 정의한 생성자와 같습니다. 그러나 클래스 선언문과 함수 선언문은 차이점이 있습니다.
!! 클래스 선언문과 함수 선언문의 차이점
- 클래스 선언문은 자바스크립트 엔진이 호이스팅하지 않습니다. 따라서 생성자를 사용하기 전에 클래스 선언문을 작성해야 합니다.
- 클래스 선언문은 한 번만 작성할 수 있습니다. 같은 이름을 가진 클래스 선언문을 두 번 이상 작성하면 타입 오류가 발생합니다.
- 클래스 선언문에 정의한 생성자만 따로 호출할 수 없습니다.
클래스 표현식으로 정의하기
let Person = class { // 생성자를 사용한 초기화 constructor(name, first, second){ this.name = name; this.first = first; this.second = second; } // prototype 메서드 sum(){ return this.first + this.second; } } let kim = new Person( "kim", 10, 20 ); console.log(kim); // Person {name: "kim", first: 10, second: 20} console.log(kim.sum()); // 30
클래스 표현식으로 작성하여도 콘솔에 결과 값을 잘 출력해내는 것을 확인할 수 있습니다.
constructor
그렇다면 constructor가 무엇인지 좀 더 자세하게 살펴보겠습니다.
어떠한 객체 즉, 인스턴트가 생성될 때 그 객체에 초기상태를 지정하기 위한, 객체가 만들어지기 직전에 실행되도록 약속되어있는 함수입니다.
자바스크립트는 인스턴스를 생성할 때 constructor 함수를 자동으로 호출해주는 것입니다.
위의 사진처럼 콘솔에 kim을 출력하기 전에 constructor라는 텍스트가 먼저 출력이 되었다는 것은 이 constructor라는 함수가 객체가 생성되는 과정에서 실행되었다는 것입니다.
위의 코드에서 보시다시피 constructor()라고 호출해주지 않아도 출력이 되었다는 것을 보면 자동으로 호출해주었다는 것을 확인할 수 있습니다.
그렇다면 이 constructor함수는 인스턴스객체를 초기화 할 때 사용된다고 했는데 class내에서 constructor를 어떻게 사용해야 할까요?
new Person()의 전달인자로 초기화하고자 하는 입력값을 넘겨주는 것을 통해서 kim이라는 객체를 초기화하고 싶다면 다음과 같이 작성할 수 있겠습니다.
class Person{ constructor(name, first, second){ this.name = name; this.first = first; this.second = second; } } let kim = new Person( "kim" , 10, 20 ); console.log(kim); // Person {name: "kim", first: 10, second: 20}
new Person( "kim", 10, 20); 으로 입력 값을 넘겨주었고 이 전달인자들을 constructor의 매개변수로 받아 this.name, this.first, this.second에 각각 할당하여 초기화 해주었습니다.
콘솔의 결과 값을 보면 각 프로퍼티에 입력 값들이 잘 들어가있는 것을 볼 수 있습니다.
Class에서 객체의 method 구현하는 방법
기존에 사용되었던 생성자 함수에서 메서드를 구현하는 방법은 prototype과 prootype Chain 에서 배워서 이미 알고 있습니다.
그렇다면 class에서는 어떤 방법으로 구현할 수 있을까요?
★ 첫 번째 방법
class Person{ constructor(name, first, second){ this.name = name; this.first = first; this.second = second; } } Person.prototype.sum = function (){ return this.first + this.second; } let kim = new Person( "kim" , 10, 20 ); console.log(kim.sum()); // 30
생성자 함수에서 구현했던 방법 그대로 작성하는 방법입니다. 생성자 함수에서 메서드를 구현하는 것과 다를 것이 없습니다.
왜냐하면 class는 새로운 기능이 아니라 이미 있는 것에서 문법만 추가해놓은 양념이기 때문에 기존과 같이 작성해주어도 콘솔에 잘 출력해 내는 것입니다.
하지만 이렇게 사용한다면 기존에 사용되고 있던 방법과 크게 달라질 것이 없으므로 class를 사용하는 의미가 부족합니다.
★ 두 번째 방법
class Person{ constructor(name, first, second){ this.name = name; this.first = first; this.second = second; } sum(){ return this.first + this.second; } } let kim = new Person( "kim" , 10, 20 ); let soo = new Person( "soo", 30, 40); console.log(kim.sum()); // 30 console.log(soo.sum()); // 70
class안에 function 키워드를 생략하고 위와 같이 작성해주는 문법이 제공되고 있습니다.
즉, function 키워드를 생략할 수 있다는 것입니다. 훨씬 더 간결한 코드가 되었습니다.
단, 이 메서드는 constructor 안에 작성하는 것이 아닌 밑에 작성해주어야 합니다!
이 sum메서드는 soo라는 인스턴스에서도 동일하게 사용할 수 있는 것으로 보아 같은 class에 속해있는 모든 객체가 공유하는 함수인 것을 알 수있습니다.
그리고 kim이라는 인스턴스만큼은 다르게 동작하는 함수를 정의하고 싶다면 다음과 같이 작성할 수 있습니다.
class Person{ constructor(name, first, second){ this.name = name; this.first = first; this.second = second; } sum(){ return this.first + this.second; } } let kim = new Person( "kim" , 10, 20 ); kim.sum = function(){ return this.first * this.second; } let soo = new Person( "soo", 30, 40); console.log(kim.sum()); // 200 console.log(soo.sum()); // 70
kim이라는 객체에 sum이라는 함수를 재정의 해주면 자바스크립트는 우선 kim이라는 객체가 sum이라는 함수를 가지고 있는지를 먼저 확인해보고 있다면 이 함수를 우선순위로 출력하고, 없다면 kim이라는 객체의 Person이라고 하는 class안에 sum이라고 하는 메서드가 정의되어 있는지를 확인합니다. 콘솔로 출력한 결과를 보면 알 수 있습니다.
class의 상속
위에 작성한 코드에 sum이라는 메서드가 Person이라는 class 안에 정의되어 있었습니다.
하지만 여기서 조금 아쉬운 점이 있습니다. sum이라는 메서드는 합을 구하는 메서드인데 평균을 구하고 싶은 메서드를 추가하고 싶다면 sum 이라는 메서드 밑에 추가로 평균을 구하는 메서드를 만들면 될 것입니다.
하지만 이렇게 메서드가 필요할 때마다 추가하는 것은 언제나 가능한 것은 아닙니다.
만약 이 Person이라는 class가 다른 개발자가 만든 것일 경우, 아니면 다른 라이브러리를 가져다 쓰는 경우라면 이 class를 우리가 임의대로 수정하게 된다면 해당 class가 업데이트 되었을 때 내가 작업한 것들을 덮어씌우게 되거나 내가 작업한 것들 때문에 업데이트가 불가능할 수도 있게 됩니다.
다른 개발자가 짠 코드가 아니라고 해도 내가 짠 코드 안에서 Person이라는 class안의 기능들은 최소한으로 유지하는 것이 좋습니다.
왜냐하면 내가 추가하려는 메서드가 다른 여러개의 인스터들 모두에게 추가적으로 필요하지는 않을수도 있기 때문입니다.
즉, 모든 인스터들에게 적용되지 않는 기능을 Person에 정의한다는 것은 불필요한 일이고 부담스러운 일이 될 것입니다.
이런경우에 필요한 것이 바로 상속입니다.
class Person{ constructor(name, first, second){ this.name = name; this.first = first; this.second = second; } sum(){ return this.first + this.second; } } class Person2 extends Person{ avg(){ return (this.first + this.second) / 2; } }
Person2라는 class를 만들었습니다. 이 Person2 뒤에 나오는 extends는 확장하다 라는 뜻을 가지고 있습니다.
이 extends 뒤에 상속받을 class의 이름을 써주면 Person2를 Person으로부터 확장하겠습니다 라는 뜻입니다.
즉, Person에 있는 모든 프로퍼티와 메서드들이 Person2에 상속되는 것입니다.
하지만 Person2에서는 avg라는 메서드만 존재하고 나머지 공유되는 기능들은 Person에 있기 때문에 Person안에 있는 sum 메서드에 추가적인 작업을 더해서 Person2에 정의하고 싶다면 다음과 같이 써주어야 할 것입니다.
class Person{ constructor(name, first, second){ this.name = name; this.first = first; this.second = second; } sum(){ return this.first + this.second; } } class Person2 extends Person{ constructor(name, first, second, third){ this.name = name; this.first = first; this.second = second; this.third = third; } sum(){ return this.first + this.second + this.third; } avg(){ return (this.first + this.second + this.third) / 3; } } let kim = new Person2( "kim", 10, 20, 30);
Person2의 sum이라는 메서드는 Person과 동일하지만 third라는 입력 값을 하나 더 받아서 위와 같이 사용하려고 합니다.
그렇다면 Person에 있는 모든 것들을 다 복사해서 Person2에 넣어주어야 하는데 이렇게 된다면 중복이 발생하고 extends를 사용하여 상속이라는 것을 사용한 의의가 없어지는 것입니다.
( !! 위 코드는 지금으로써는 동작하지 않는 코드 입니다! 아래에서 super키워드를 알아보면서 저 코드를 동작할 수 있는 방법에 대해 알아볼 것입니다!! )
이 중복을 줄이기 위해서는 super키워드를 사용할 수 있습니다..
super 키워드
부모클래스를 불러서 부모클래스한테 일을 시키고 부모가 하지 못하는 일은 나만 할 수 있도록 만들어주는 것이 super입니다.
즉, super키워드를 사용하면 중복없이 부모가 가지고 있는 것들을 상속받을 수 있으면서 나만 할 수 있는일도 정의할 수 있다는 말입니다.
class Person{ constructor(name, first, second){ this.name = name; this.first = first; this.second = second; } sum(){ return this.first + this.second; } } class Person2 extends Person{ constructor(name, first, second, third){ super(name, first, second); this.third = third; } sum(){ return super.sum() + this.third; } avg(){ return (super.sum() + this.third) / 3; } } let kim = new Person2( "kim", 10, 20, 30); console.log(kim.sum()); // 60 console.log(kim.avg()); // 20
super라는 키워드는 두 가지 용법이 있습니다.
super뒤에 실행문 ()이 붙으면 이것은 부모클래스의 생성자 입니다. 그래서 이 실행문안에 전달인자로 name, first, second를 전달해주게 되면 Person2의 부모 클래스에 생성자가 호출이 되고 생성자 안에 있는 프로퍼티들을 상속받아옵니다.
따라서 자식 클래스(Person2)는 this.third만 설정해주면 되는 것입니다.
super뒤에 .이 오면 부모가 갖고 있는 sum이 호출되면서 그 결과가 return됩니다.
Person2에 sum 메서드를 보면 super.sum() + this.third; 라고 적어주었고 super.sum()이 부모 클래스에서 호출되어 값을 리턴해서 가져오고 그 값에 this.third를 더해서 총 계산된 값을 리턴해 주는 것입니다.
콘솔에 출력된 결과 값을 보면 의도한대로 잘 출력되는 것을 볼 수 있습니다.
!! super키워드 정리하기
- super()와 super. 의 차이점
- super() 는 부모 클래스의 constructor 즉, 생성자로서의 역할을 수행합니다. 부모로부터 상속 받아올 값(부모 클래스의 생성자를 호출)을 super()의 괄호 안에 넣어주는 방식으로 활용됩니다.
- super. 은 부모 클래스 자체를 의미하며 super.<어떤 것>에서 <어떤 것>은 부모가 가지고 있는 속성 중 <어떤 것>을 사용할지 선택하기 위해 사용하는 용법입니다. - super가 없다면 어떤점이 불편하고 있다면 어떤점이 편리한가?
super가 없다면 class를 사용하는 의미가 없어지며, 부모 클래스와 중복되는 코드를 모두 다시 한번 작성해야하기 때문에 효율성이 떨어집니다. 반면에 super 키워드를 사용하게 되면 부모가 가지고 있는 기능들을 모두 가져다 쓸 수 있어 중복을 제거할 수 있고, 유지보수에 좋습니다. - 상속의 기능으로 인해 생기는 단점을 어떻게 보완했는가?
상속받은 기능은 변경할 수 없었는데 super키워드를 통해 중복되는 부분은 상속받아 사용하고, 추가적인 부분은 상속받은 값에 추가하여 보완할 수 있게 되었습니다.
'개발' 카테고리의 다른 글
HTTP 통신 (0) 2020.05.19 Object.create()와 constructor (2) 2020.05.11 화살표 함수( Arrow Function )에서의 this 바인딩 (0) 2020.04.29 ES6 문법 - Arrow Function (0) 2020.04.28 Node.js 시작하기 (0) 2020.04.27 - class 키워드 뒤에 생성자 함수의 이름을 표기합니다. 이 함수 이름은 클래스 이름입니다.