typescript를 익히기 전에 javascript의 class를 먼저 익히고 가보자
기본적으로 class는 패턴을 생성하는 역할을 하는데 객체를 나타낼 때 프로퍼티뿐 아니라 기능도 나타내준다.
클래스는 TS의 인터페이스나 타입 별칭과는 다르다.
class Player{
taunt(){
console.log("FUS RO DAH")
}
}
const player1 = new Player();
player1.taunt(); // FUS RO DAH
const player2 = new Player();
player2.taunt(); // FUS RO DAH
어떠한 프로퍼티도 없지만 이게 class
를 사용하는 방법이다 그냥 매번 어딜가나 듣는 붕어빵 기계 틀 그거와 같다.
class Player
라는 붕어빵 틀이 있고 new Playser()
가 붕어빵을 만드는 방법이다
그래서 plyaer1
과 player2
라는 붕어빵이 나왔다.
둘 다 taunt()
라는 속재료를 가지고 있는 것이다.
class 배우면 항상 배우는 생성자인데 사용자가 정의할 수 있는 메서드이다.
정의가 필수는 아니며 정의하지 않아도 문제는 없지만 대부분의 클래스에서는 정의를 한다.
한번만 할 수 있고 이름은 반드시 constructor
라고 해야 자동으로 호출된다.
class Player{
constructor(){
console.log("IN CONSTRUCTOR1")
}
taunt(){
console.log("FUS RO DAH")
}
}
const player1 = new Player(); //IN CONSTRUCTOR1
player1.taunt(); // FUS RO DAH
const player2 = new Player(); //IN CONSTRUCTOR1
player2.taunt(); // FUS RO DAH
위와 같이 적을 경우 클래스를 인스턴스화(instantiate)할 때마다 JS가 자동으로 생성자 함수를 호출해준다.
new Player()
로 만들때마다 자동으로 호출되는 것!
그럼 처음 만들때 이름과 성이 들어가야한다면
class Player{
constructor(first, last){
this.first = first;
this.last = last;
}
taunt(){
console.log("FUS RO DAH")
}
}
const player1 = new Player("blue","steele");
player1.taunt(); // FUS RO DAH
console.log(player1.first) //blue
console.log(player1.last)//steele
const player2 = new Player("charlie","brown");
player2.taunt(); // FUS RO DAH
console.log(player2.first) //charlie
console.log(player2.last)//brown
생성자 함수에 first
와 last
란 이름의 파라미터(이름은 어떻게 되도 상관없다! 파라미터일 뿐이니까)로 this.first=first
와 같은 구문으로 입력하면
이제 모든 Player 인스턴스에는 first
와 last
라는 프로퍼티가 있고 엑세스 한다.
따라서 새 플레이어를 만들 땐 그 둘의 값을 꼭 인자로 넘겨줘야 한다.
여기서 this는 해당하는 인스턴스를 가리킨다!
필드와 프로퍼티를 빠르게 정의하게 해주는 구문으로 클래스 안에서 자유롭게 바꿔가며 사용할 수 있다.
클래스에 플레이어가 생성될 때마다 점수와 목숨등을 넣어보려 하는데
class Player{
constructor(first, last){
this.first = first;
this.last = last;
this.score = 0;
this.numLives = 10;
}
taunt(){
console.log("FUS RO DAH")
}
}
위와 같이 직접 생성자에 집어넣어 만들수도 있고
class Player{
score = 0;
numLives = 10;
constructor(first, last){
this.first = first;
this.last = last;
}
taunt(){
console.log("FUS RO DAH")
}
}
이렇게 밖에 설정할 수도 있다. this
를 통해 생성자 안에서 인스턴스를 참조할 필요없이 JS는 스스로 파악할 수 있다.
위와 같이 만들경우 모든 Player
는 score
란 프로퍼티는 0이고 numLives
프로퍼티는 10이 된다.
이처럼 클래스 필드의 기능을 통해 프로퍼티를 정의할 때 생성자를 거치지도 this
를 참조하지 않고도 하드 코딩된 값을 추가할 수 있다.
하지만 각자 다르고 받아야 한 값이 있다면 사용할 수 없다.
이는 모든 플레이어가 처음 만들어질 땐 0점과 10개의 목숨으로 고정되어 있기 때문에 가능한 것!
아래와 같이 목숨을 잃는 방법도 추가할 수 있다.
class Player{
score = 0;
numLives = 10;
constructor(first, last){
this.first = first;
this.last = last;
}
taunt(){
console.log("FUS RO DAH")
}
loseLife(){
this.numLives -= 1;
}
}
class Player{
score = 0;
numLives = 10;
constructor(first, last){
this.first = first;
this.last = last;
}
taunt(){
console.log("FUS RO DAH")
}
loseLife(){
this.numLives -= 1;
}
}
const player1 = new Player("blue","steele");
player1.score = -234234//가능
모든 인스턴스의 score 값은 0에서 시작하는데 점수가 계속 커진다 가정하자.
하지만 0보다 작은 점수는 없고 최소가 0점이다.
그런데 만약 누군가 player1.score = -음수
식으로 실수를 한다고 했을 때 JS는 막아줄 수 있을까?
그대로 실행될 것이다.
모든게 퍼블릭 클래스로 간주돼서 클래스 안이나 밖에서 엑세스 할 수가 있다.
그래서 나온 기능이 프라이빗으로
class Player{
#score = 0;
numLives = 10;
constructor(first, last){
this.first = first;
this.last = last;
}
taunt(){
console.log("FUS RO DAH")
}
loseLife(){
this.numLives -= 1;
}
}
const player1 = new Player("blue","steele");
player1.#score = -234234 // Private field error 접근 자체가 거부된다.
#
으로 시작하는 프로퍼티가 있다면(#score = 0;
) JS는 이 프로퍼티가 Player 클래스 안에서만 사용할 수 있다고 인식한다!
그렇기에 밖에서 프로퍼티에 엑세스 할 수 없다.
Private field
라는 에러가 발생한다! 하지만 사용자가 score 프로퍼티에 엑세스 하려는 방법은 있는데 래퍼 메서드를 사용한다.
class Player{
#score = 0;
numLives = 10;
constructor(first, last){
this.first = first;
this.last = last;
}
getScore(){
return this.#scroe;
}
taunt(){
console.log("FUS RO DAH")
}
loseLife(){
this.numLives -= 1;
}
}
const player1 = new Player("blue","steele");
console.log(player1.getScore()) // 0
이처럼 접근할 수 있고 만약 값을 변경하고 싶다면 setScore
나 update..
등의 메서드를 추가하면 된다.
class Player{
#score = 0;
numLives = 10;
constructor(first, last){
this.first = first;
this.last = last;
}
getScore(){
return this.#scroe;
}
updateScore(newScore){
this.#score = newScore;
}
taunt(){
console.log("FUS RO DAH")
}
loseLife(){
this.numLives -= 1;
}
}
const player1 = new Player("blue","steele");
player1.updateScore(2) // #score의 값이 변경됨
물론 생성자 안에서 this.#score
이런식으로 적어도 똑같이 동작한다!
또한 같은 구문을 이용해 메서드 또한 프라이빗한 메서드를 만들 수 있다.
class Player{
#score = 0;
numLives = 10;
constructor(first, last){
this.first = first;
this.last = last;
}
getScore(){
return this.#scroe;
}
updateScore(newScore){
this.#score = newScore;
}
taunt(){
console.log("FUS RO DAH")
}
loseLife(){
this.numLives -= 1;
}
#privateMethod(){
console.log("secret")
}
}
const player1 = new Player("blue","steele");
player1.#privateMethod() //Private field Error
객체 접근자라고도 불리는 기능인데 부가적인 로직을 간단히 추가하거나 프로퍼티처럼 보이는 합성 프로퍼티를 쉽게 만들 수 있다!
class Player{
#score = 0;
numLives = 10;
constructor(first, last){
this.first = first;
this.last = last;
}
get fullName() { //get을 이용해 프로퍼티처럼 만들었다
return `${this.first} ${this.last}`
}
getScore(){
return this.#scroe;
}
updateScore(newScore){
this.#score = newScore;
}
loseLife(){
this.numLives -= 1;
}
}
const player1 = new Player("blue","steele");
console.log(playser1.fullName) // 호출시 ()를 적지 않는다. 메서드 취급이 아니기때문에!
이 방법이 getter이다.
위에서 fullName
을 통해 이름 전체를 호출하고 싶은데 해당 메서드나 프로퍼티가 없다. 메서드를 만들어도 되지만 앞에 get
을 쓰고 뒤에 메서드를 쓸 경우 정상 동작한다.
단, 메서드와 다른 것은 호출시 ()
를 통해 실행하지 않고 프로퍼티처럼 작성하여도 뒤에서 동작한다는 것
class Player{
#score = 0;
numLives = 10;
constructor(first, last){
this.first = first;
this.last = last;
}
get score() { // 아래의의 getScore()메서드와 똑같이 동작한다. 하지만 이 get 구문이 더욱 깔끔하다
return this.#score;
}
getScore(){
return this.#scroe;
}
updateScore(newScore){
this.#score = newScore;
}
loseLife(){
this.numLives -= 1;
}
}
const player1 = new Player("blue","steele");
console.log(player1.score) // 메서드가 아닌 프로퍼티처럼 보여주기 때문
#score
를 통해 프라이빗으로 만들었지만 getter
를 통해 score
점수를 부르는 메서드를 만들고 이를 호출할 때 프로퍼티처럼 호출하니player1.getScore()
가 아닌 player1.score
이와 같이 프로퍼티에 접근하는 듯한 코드가 되었다
만약 이경우 player1.score = -2394293
이렇게 한다 해도 변경되지 않는다 getter
만 정의했기 때문이다.
프로퍼티를 설정하는 객체 접근자로 정의할 때 비슷한 구문을 사용하면 된다.
메서드를 작성한 것 처럼 동작한다. 하지만 getter
와 마찬가지로 프로퍼티처럼 취급한다!
class Player{
#score = 0;
numLives = 10;
constructor(first, last){
this.first = first;
this.last = last;
}
get score() {
return this.#score;
}
set score(newScore) { // 이름은 동일하게 해도 된다
if(newScore < 0 ) {
throw new Error("Score must be positive")
}
this.#score = newScore;
}
updateScore(newScore){
this.#score = newScore;
}
loseLife(){
this.numLives -= 1;
}
}
const player1 = new Player("blue","steele");
console.log(player1.score) // 0
player1.score = 30
console.log(player1.score) // 30
이 모습은 일반적인 정규 프로퍼티(원시형)와 다를바 없어 보인다. player1.score
통해 값을 불러올 수 있고 player.score = 2042
로 값을 변경할 수도 있다.
하지만 값을 도와주는 부수적인 로직이 class
안을 살펴보면 존재한다.
코드를 래핑해서 score
가 변경될 때 올바른 값이 들어왔는지 로직에서 확인한다.
정적은 클래스 필드 같은 프로퍼티의 앞부분에 static
을 넣거나 메서드 앞에 넣음으로 JS에 해당 프로퍼티나 메서드가 클래스 자체에 존재하며 개별 인스턴스에는 없다고 알린다.
class Player{
static description = "Player In Our Game";
#score = 0;
numLives = 10;
constructor(first, last){
this.first = first;
this.last = last;
}
get score() {
return this.#score;
}
set score(newScore) { // 이름은 동일하게 해도 된다
if(newScore < 0 ) {
throw new Error("Score must be positive")
}
this.#score = newScore;
}
updateScore(newScore){
this.#score = newScore;
}
loseLife(){
this.numLives -= 1;
}
}
const player1 = new Player("blue","steele");
static description = "Player In Our Game";
이 부분에서 만약 description
앞의 static
이 없다면 모듬 Player
에 존재할 것이다.
각 인스턴스화 된 Player
마다 누구는 그대로 문자열을 쓰고 누구는 내용을 변경하여 완전히 다른 것을 쓸 것이다
하지만 정적으로(static
을 추가하여)만들면 프로퍼티는 개별 인스턴스가 아닌 Player
클래스에만 존재한다.
따라서 인스턴스에는 관계없는 기능에 사용할 수 있는 방식이라고 할 수 있다.
그대로 예시를 본다면
class Player{
#score = 0;
numLives = 10;
constructor(first, last){
this.first = first;
this.last = last;
}
static randomPlayer() { // 예시로 하드코딩된 값이고 실제로 랜덤 플레이어를 배출하면 된다
new Player("Andy", "Samberg")
}
get score() {
return this.#score;
}
set score(newScore) { // 이름은 동일하게 해도 된다
if(newScore < 0 ) {
throw new Error("Score must be positive")
}
this.#score = newScore;
}
updateScore(newScore){
this.#score = newScore;
}
loseLife(){
this.numLives -= 1;
}
}
const player1 = new Player("blue","steele");
player1.randomPlayer() // Error player1 안에 randomPlayer라는 메서드 자체가 존재하지 않는다
const Player2 = Player.randomPlayer() // 새로운 랜덤 플레이어를 생성함!
이처럼 특정 인스턴스와 관련 없으면서 클래스 자체와 연관된 기능에 static
을 사용할 수 있다.
JS에서는 다른 부모 클래스로부터 상송되는 클래스를 가질 수 있고 또 다른 클래스와 함께 기능을 공유할 수 있다.
위의 Player
클래스가 있다는 가정하에 추가적인 class를 만들어 보자
class AdminPlayer extends Player { //extends를 통해 Player의 프로퍼티들을 받았다
isAdmin = true
}
const adminPlayer = new AdminPlayer() // Player프로퍼티 + isAdmin 프로퍼티를 추가적으로 가지고 있는 Player가 만들어졌다
이처럼 Player
의 프로퍼티들을 extends
를 통해 확장하여 가져올 수 있다.
super
는 클래스 상속이나 클래스 확장 시 사용되는데 구체적으론 자식 클래스에 생성자를 추가할 때 사용된다.
현재
class AdminPlayer extends Player {
isAdmin = true
}
const adminPlayer = new AdminPlayer()
아까 작성한 코드를 보면 AdminPlayer
에는 생성자가 없는 상태이다
다만 JS는 부모 클래스에 생성자 함수가 있으면 자동으로 그 함수를 호출하긴 한다. 따라서 이름의 first
와 last
를 인자로 넘겨줄 경우 해당 프로퍼티들에 값이 생긴다.(적지 않을경우 프로퍼티는 있지만 값이 undefined
로 되어있다.)
하지만 무엇인가를 추가해야 할 경우가 많다고 생각하면 생성자를 추가하거나 자식클래스에 기능을 추가하는 것이다.
class AdminPlayer extends Player {
constructor(powers){
this.powers = powers;
}
isAdmin = true
}
const adminPlayer = new AdminPlayer(["delete","restore world"])
앞서 Player
에서 작성했던 것과 똑같이 작성해 보았다. JS는 어떻게 반응할까?
둘 중 하나만 실행되고 JS가 처음 마주치는 AdminPlayer
의 constructor(powers)
이 부분만 실행될 것까?
일단 실행시켜본다면
에러가 발생한다! 엑세스 하거나 파생 생성자에서 반환하기 전에 반드시 파생 클래스의 슈퍼 생성자를 호출하라는 에러가 발생한다.
(Uncaught ReferenceError: Must call super constructor in derived class befor...)
이는 자식 클래스에 생성자가 있지만 부모 클래스 생성자를 먼저 호출하지 않았기 때문이다.
이때 super()
를 사용한다
super
는 슈퍼 클래스에 있는 constructor()
함수를 참조한다. 부모 클래스 , 파생클래스 , 기본 클래스도 모두 같은 말이다.
class AdminPlayer extends Player {
constructor(first,last,powers){
super(first, last) // 여기서 슈퍼 클래스는 Player이다
this.powers = powers;
}
isAdmin = true
}
const adminPlayer = new AdminPlayer("adimn","mCadmin",["delete","restore world"])
이 경우 정상적으로 Player
의 생성자에서 만들어지는 first
와 last
프로퍼티를 가지고 추가적으로 AdminPlayer
의 클래스의 생성자에서 만들어지는 powers
프로퍼티를 모두 가질 수 있다.