LoginGustBookCategories
Javascript

Class Syntax

thumbnailCreatehb21·2022년 01월 17일 22:30

https://images.unsplash.com/photo-1584907797015-7554cd315667?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1476&q=80



닿을 듯 말듯, 잡힐 듯 말듯하면서 안잡히는 클래스에 대해서 정리합니다.

자바스크립트를 하면서 클래스를 제대로 다룰 줄 아는 능력을 갖추는 것은 정말이지 중요하다고 생각합니다. 클래스는 객체 지향 프로그래밍, 자료 구조 패턴 등과 같은 곳에서 아주 유용하게 쓰일 뿐 아니라, 필수적으로 클래스 패턴의 도움을 받아야 할 때가 있습니다. 프론트엔드 개발자로서 개발을 해나가는 시간이 늘어갈수록 혹은 개발 실력이 점점 올라갈 수록 클래스를 실제로 사용해야 하는 경우는 많아질 겁니다.

따라서 여기서 클래스에 대해서 이제는 정말 꽉 잡고 한번 가보도록 하겠습니다.

ES 2015에서 등장한 클래스는 객체 지향 프로그래밍을 위한 패턴 혹은 데이터 구조를 정의하는 방식입니다. 자바스크립트 자체는 OOP, 그러니까 객체 지향 프로그래밍을 지원하지 않습니다. 그렇지만 이 ES 2015의 새로운 클래스를 키워드를 사용할 수 있는데, 이를 사용하면 데이터 구조를 정의하는 것을 정말 쉽고 간단하게 할 수 있기 때문에 많은 도움이 됩니다.

앞으로는 이진 탐색 트리, 큐, 단일 연결 리스트, 무방향, 이진 힙, 방향 그래프, 해시 테이블, 이중 연결 리스트 ... 등을 직접 이 각각을 클래스로 코딩해야할 것입니다.

따라서 시작해보겠습니다.


Objectives

  • 클래스가 무엇인지 다룹니다.
  • 자바스크립트가 클래스라는 개념(idea)을 어떻게 구현하는지를 이해합니다.
  • 추상화, 캡슐화, 다형성과 같은 단어들을 정의합니다.
  • ES 2015 클래스를 사용해서 데이터 구조를 코딩합니다.

1. 클래스가 그래서 무엇인가요


일반적으로 클래스는 미리 정의한 프로퍼티와 메소드를 가지고 객체를 만들기 위해 만들어 주는 설계도 혹은 청사진입니다. 예를 들어 나중에 단일 연결 리스트와 이중 연결 리스트를 만드는 법을 다루게 될 텐데요, 그러면 연결 리스트에 대한 패턴을 정의하고 많은 객체들과 개별 연결 리스트들을 설계도, 그러니까 클래스를 사용하여 인스턴스화 해야 합니다.

이미 위에서도 말했지만, 자바스크립트는 원래 클래스를 가지고 있지 않았습니다. MDN, 모질라 개발자 네트워크에 있는 내용을 보면 클래스를 다음과 같이 정의하고 있습니다.

ES 2015에서 도입된 자바스크립트의 클래스는 자바스크립트에 이미 존재하는 프로토타입 기반 상속에 대한 문법적 조미료이다(syntactical sugar). 클래스 문법은 새로운 객체 지향 상속 모형을 자바스크립트에 도입한 것은 아니다.

여기 써있는 말인 즉슨, 기본적으로 자바스크립트가 제대로 객체 지향적이었던 적은 없고 그냥 프로토타입 기반 상속이라고 불리는 것을 사용했다는 말입니다. 그리고 ES 2015를 통해 일어난 일이 클래스 문법이 도입되었다는 것입니다. 그렇지만 이것이 실제로 뒤에서 일어나는 일까지 바꾼 것은 아닙니다. 그냥 데이터 구조와 같은 클래스를 정의하고 그것을 가지고 작업하는 더 쉬운 방법이 도입되었다는 것이죠.

그래서 클래스는 왜 우리에게 필요할까요? 이미 말했지만, 엄청나게 많은 데이터 구조를 클래스로 코딩을 해야할 경우가 생기게 됩니다. 앞으로 트리나 그래프나 단일 연결 리스트를 다룰텐데 데이터 구조를 만들때면 언제나 개별 리스트를 인스턴스화 할 수 있도록 자바스크립트에서 클래스를 정의하고 패턴을 정의해야 합니다.


Declare Class and knowing about Constructor


이제 실제로 ES 2015에서 클래스를 선언하는 법을 다뤄보겠습니다.
다음은 간단한 예시입니다.

class Student { constructor(firstName, lastName) { this.firstName = firstname; this.lastName = lastName; } }

여기 Student라는 이름의 아주 간단한 클래스가 있습니다. 클래스의 이름은 대문자로 시작하는 것이 일반적입니다. 그리고 위의 간단한 예제이지만 이는 클래스의 일반적인 패턴입니다. Student에 대한 패턴이죠. 모든 학생은 성과 이름을 가집니다. 이렇게 패턴, 즉 설계도를 만들면 더 작업하기 쉽고, 이해하기 쉬우면서 더 멋지고 잘 구성된 코드를 가질 수 있습니다.

클래스에는 constructor라고 부르는 것이 필요합니다. 위처럼 constructor 라고 명시해야 됩니다. 이건 새로운 Student 인스턴스를 만드는데 사용되는 특별한 메소드입니다. 이제 새로운 Student를 만들 때에 성과 이름을 입력해야 합니다. 그리고 해야할 일은 constructor 안에서 this.firstName을 해서 앞에 firstName과 같도록 설정해주고 this.lastName을 해서 앞에 있던 lastName과 같도록 해주는 겁니다.

그러니까 위 코드의 의미는 새로운 Student가 형성될 때 입력된 것이 무엇이든지 간에 그 두 프로퍼티를 각각의 객체인 Student에 입력해주라는 겁니다.

그리고 그 다음은 새로운 인스턴스를 만드는 부분입니다.

class Student { constructor(firstName, lastName) { this.firstName = firstname; this.lastName = lastName; } } let firstStudent = new Student("Team", "Jupeter"); let secondStudent = new Student("Jack", "Black");

위의 Student 클래스 부분에 패턴, 즉 설계도가 정의되어 있습니다. 여기서는 Student를 새로 만들지 않습니다. 그렇게 하기 위해서는 이 new 라는 키워드를 사용해야 합니다. 그러니까 이게 우리가 클래스에서 객체를 인스턴스화 하는 방법입니다.

위처럼 new Student를 입력하고 성과 이름을 입력해서 넘겨주면 됩니다. 이제 이 각각(첫번째 학생과 두번째 학생)은 위 클래스 패턴에 따라서 새로운 student 객체를 만들 겁니다.

여기에 학년도 추가해 주도록 해보겠습니다.

class Student { // 패턴 및 설계도를 정의합니다. constructor(firstName, lastName, year) { this.firstName = firstname; this.lastName = lastName; this.grade = year; } } // 기본적으로 위 코드들은 여기 설계도가 있다는 것을 나타냅니다. let firstStudent = new Student("Team", "Jupeter", 3); let secondStudent = new Student("Jack", "Black");
constructor 내부에서 this. 뒤에 오는 이름은 뭐가 되든지 상관은 없습니다. 만약에 grade라고 부르고 싶으면 그렇게 하면 됩니다. 그러면 학년은 새로운 학생이 인스턴스로 만들어질 때 입력되는 그대로 설정될 겁니다.

첫번째 학생을 출력해보면 다음과 같습니다.

let firstStudent = new Student("Team", "Jupeter", 3); console.log(firstStudent); // Student {firstName: "Team", lastname: "Jupeter", grade: 3} console.log(firstStudent.lastName); // "Jupeter" let secondStudent = new Student("Jack", "Black"); console.log(secondStudent); // Student {firstName: "Jack", lastname: "Black", grade: undefined} secondStudent.grade = 4; console.log(secondStudent); // Student {firstName: "Jack", lastname: "Black", grade: 4}

year은 실제로 grade라고 표기되어 있습니다. 또한 firstStudent에 접근도 할 수 있네요. 여기까지 기억해볼만한 것은 클래스를 정의하더라도 그 자체는 아무일도 하지 않는다는 것과 사실은 new라는 키워드를 사용해서 그 클래스를 인스턴스로 만들어야 한다는 것입니다.

여기서 this라는 키워드는 무엇을 의미하는지 궁금할 수 있습니다. 보통 this라는 키워드는 거의 항상 자바스크립트를 공부하면서 혼동의 원인이 되기도 하는데 이는 this가 문맥에 따라서 그 의미가 바뀌기 때문입니다. 하지만 위처럼 constructor나 다른 인스턴스 메소드 안에 있는 경우에는 this는 클래스에 있는 개별 인스턴스를 의미합니다. 즉, 개별 학생을 의미하는 겁니다.

그래서 여기서의 this는 우리가 지칭하는 상황에 따라서 이 학생을 가리킬 수도 있고 저 학생을 의미할 수도 있는 겁니다.

좋아요, 여기까지 클래스에 대한 기본 사항이었고 컨스트럭터에 대한 개념이었습니다. 하나만 더 알아두면 좋은 것은 Student.constructor 같은 호출은 하지 않습니다. 실제로 constructor를 호출하는 일은 하지 않고, 그냥 new 라는 키워드를 사용하면 뒤에서 마법 같은 문법의 조미료가 작동합니다. 이렇게 하게 되면 constructor 메소드는 알아서 실행이 됩니다. 즉, constructor는 우리가 새로운 학생이나 클래스를 인스턴스화 하게 되면 작동하게 됩니다.



Instacne Methods


인스턴스 메소드와 정적 메소드, 그리고 클래스 메소드를 알아보겠습니다. 먼저 인스턴스 메소드부터 시작하겠습니다.

class Student { constructor(firstName, lastName, year) { this.firstName = firstName; this.lastname = lastName; this.grade = year; } fullName() { return `Your full name is ${this.firstName} ${this.lastName}`; } } let firstStudent = new Student("Team", "Jupeter"); let secondStudent = new Student("Jack", "Black"); firstStudent.fullName(); // "Your full name is Team Jupeter" secondStudent.fullName(); // "Your full name is Jack Black"

인스턴스 메소드는 기본적으로 특정한 인스턴스에 대해 작용하는 메소드입니다. 다시 좀 더 풀어서 말하자면 개별 인스턴스에 관한 함수성을 제공해줍니다. 우리의 경우에는 학생이 되겠죠. 만약 단일 연결 리스트를 예로 들면, 특정 인스턴스에 대해 많은 인스턴스 메소드를 정의하게 될 겁니다.

위의 fullName()을 한번 보자면, 전체 이름을 담은 스트링을 돌려줍니다. 그리고 그 스트링은 성과 이름을 합쳐서 만들어야 합니다. 그러면 이것은 개별 인스턴스를 참고하게 됩니다.

그러면 이걸 호출하는 것에 따라서, 예를 들면 첫 번째 학생인지 두 번째 학생인지에 따라서 결과가 다를 겁니다. 즉 클래스 전체에 대해 작동하는 것이 아니라 각각의 인스턴스에 관련이 있는 메소드인 것이죠.

지금 하고 있는 위의 예시들은 조금 어이없을 정도의 쉬운 예시들이긴 하지만, 앞으로 연결 리스트를 다루게 되면 reverse 인스턴스 메소드도 정의할 거고 push 메소드도 정의할 겁니다. 이는 실제로 각각의 인스턴스에 대해 실행되는 것들입니다. 이를 개별 인스턴스 메소드라고 부릅니다.

위 클래스를 조금 더 늘려보겠습니다. 프로퍼티 중에서 하나의 값을 바꾸어주는 새로운 메소드를 추가해보겠습니다. 우리 학교에 학생들이 세 번까지 지각을 할 수 있다고 해보겠습니다. 만약 지각이 세번 이상이면 퇴학 처리를 하게 됩니다.

class Student { constructor(firstName, lastName, year) { this.firstName = firstName; this.lastname = lastName; this.grade = year; this.tardies = 0; } fullName() { return `Your full name is ${this.firstName} ${this.lastName}`; } }

tardies를 새롭게 설정해주어서 새로운 학생 인스턴스를 만들 때마다 0이 되도록 설정해 줍니다. 기본값으로는 아직 한 번도 지각한 적이 없으니까요, 그렇지만 이제 markLate라는 메소드를 만들어 줄 겁니다. 다음 markLate 메소드를 Student 클래스에 추가해주겠습니다.

markLate() { this.tardies += 1; if(this.tardies >= 3) { return "You are Expelled!!!!"; } return `${this.firstName} ${this.lastName} has been late ${this.tardies}`; } // tardies 값에 1을 더해줍니다.

그리고 이제 새로운 학생을 인스턴스로 만들어 보고 실행해 보겠습니다.

class Student { constructor(firstName, lastName, year) { this.firstName = firstName; this.lastname = lastName; this.grade = year; this.tardies = 0; } fullName() { return `Your full name is ${this.firstName} ${this.lastName}`; } markLate() { this.tardies += 1; if(this.tardies >= 3) { return "You are Expelled!!!!"; } return `${this.firstName} ${this.lastName} has been late ${this.tardies}`; } } let firstStudent = new Student("Team", "Jupeter", 1); let secondStudent = new Student("Jack", "Black", 2); console.log(firstStudent); // Student {firstName: "Team", lastname: "Jupeter", grade: 1, tardies: 0}; firstStudent.markLate(); // 이제 markLate에 접근을 할 수 있습니다. // "Team Jupeter has been late 1 times" firstStudent.markLate(); // "Team Jupeter has been late 2 times" firstStudent.markLate(); // "You are Expelled!!!!"

인스턴스 메소드에 대해 마지막으로 다룰 것은 일종의 인수를 취하는 것을 찾는 방법입니다.
위의 클래스에 scroes 라는 이름의 배열도 추가해 보겠습니다.

this.scores = [];

처음에 학생을 만들 때는 시험을 본 적이 없을테니까 빈 배열로 두겠습니다. 그리고 이제 메소드를 만드는 겁니다.

addScore(score) { this.scores.push(score); return this.scores; }

이제 출력을 하고 실행을 해보겠습니다.

class Student { constructor(firstName, lastName, year) { this.firstName = firstName; this.lastname = lastName; this.grade = year; this.tardies = 0; this.scores = []; } fullName() { return `Your full name is ${this.firstName} ${this.lastName}`; } markLate() { this.tardies += 1; if(this.tardies >= 3) { return "You are Expelled!!!!"; } return `${this.firstName} ${this.lastName} has been late ${this.tardies}`; } addScore(score) { this.scores.push(score); return this.scores; } } let firstStudent = new Student("Team", "Jupeter", 1); let secondStudent = new Student("Jack", "Black", 2); secondStudent.scores // [] secondStudent.addScore(92); secondStudent.addScore(87); secondStudent.scores // [92, 87]

secondStudent.scores.push 와 같이 데이터를 직접 수정할 수도 있지만, 메소드를 작성하는 것이 더 일반적인 클래스 작성법입니다. 직접 데이터를 수정하는 대신에, 일종의 메소드를 사용하는 거죠.

마지막으로 calculateAverage 라는 메소드를 만들어서 평균을 계산해 볼까요?

calculateAverage() { let sum = this.scores.reduce((a, b) => a + b); return sum / this.scores.length; } // 바로 테스트해보겠습니다 firstStudent.scores // [] firstStudent.addScore(98); firstStudent.addScore(76); firstStudent.calculateAverage() // 87 firstStudent.addScore(100); firstStudent.calculateAverage() // 91.33333 ...

여기까지 다루고 보여주고 싶었던 것은 인스턴스 메소드를 가지고 클래스를 정의하는 기본적인 방법이었습니다. 핵심은 인스턴스 메소드가 개별 인스턴스에 대해 작동한다는 것이었습니다. 다음으로는 (자주 사용하지는 않는) 클래스 메소드를 만드는 법을 다뤄보겠습니다.


Class Methods


지금까지 클래스를 만들거나 패턴을 정의하는 법과 constructor 메소드를 작성하는 법에 대해 배웠고 new 키워드와 인스턴스 메소드에 대해서도 배웠습니다. 이제는 클래스의 두 번째 종류에 대해 다룰 겁니다.

ES 2015를 사용하여 클래스 메소드라는 클래스를 추가할 수 있는데, 여기서는 메소드 정의 앞에 static이라는 키워드를 사용합니다.

class Student { constructor(firstName, lastName, year) { this.firstName = firstName; this.lastname = lastName; this.grade = year; this.tardies = 0; this.scores = []; } fullName() { return `Your full name is ${this.firstName} ${this.lastName}`; } static EnrollStudents(...students) { // maybe send an email here } } let firstStudent = new Student("Team", "Jupeter", 1); let secondStudent = new Student("Jack", "Black", 2); Student.EnrollStudents([firstStudent, secondStudent]);

static 키워드는 반드시 클래스의 개별 인스턴스에 대한 것일 필요는 없습니다만, 클래스에 관련된 메소드나 함수 기능을 찾을 수 있게 해줍니다. 실제로는 그렇게 흔히 사용되는 것은 아니고, 오히려 가장 많이 사용되는 것은 인스턴스 메소드이긴 합니다.

MDN의 문서를 다시 한번 보겠습니다.

static 키워드는 클래스의 정적 메소드를 정의합니다. 정적 메소드는 호출하여도 그 클래스를 인스턴스화 하지 않으며, 클래스 인스턴스 하나에 대해 호출할 수 없습니다. 보통은 어플에 사용되는 유틸리티 함수를 만드는데 사용됩니다.

위에 enrollStudents는 배열에 있는 여러 학생들을 입력할 수 있는데, 학생들의 이메일 주소로 이메일을 보낸다던가 식의 행위를 합니다. 핵심은 이러한 것들이 하는 일이 유틸리티 함수에 더 가깝다는 것인데요, 이건 개별 학생들과 관련이 있는 것은 아닙니다. 인스턴스 메소드는 해당 인스턴스로부터 가져온 데이터를 사용하죠. 하지만 enrollStudents와 같은 것은 특정 인스턴스에 관련된 일을 하지는 않습니다. 그냥 전반적인 함수 기능이나 원하는 유틸리티 함수처럼 클래스 전체의 일부에 대한 겁니다.

그리고 이를 호출할 때는 대문자 S 로 시작하는 Student를 사용합니다.

class Student { constructor(firstName, lastName, year) { this.firstName = firstName; this.lastname = lastName; this.grade = year; this.tardies = 0; this.scores = []; } fullName() { return `Your full name is ${this.firstName} ${this.lastName}`; } markLate() { this.tardies += 1; if(this.tardies >= 3) { return "You are Expelled!!!!"; } return `${this.firstName} ${this.lastName} has been late ${this.tardies}`; } addScore(score) { this.scores.push(score); return this.scores; } calculateAverage() { let sum = this.scores.reduce((a, b) => a + b); return sum / this.scores.length; } static EnrollStudents() { return "Enrolling Students"; } } let firstStudent = new Student("Team", "Jupeter", 1); let secondStudent = new Student("Jack", "Black", 2); firstStudent.EnrollStudents() // Error,개별 인스턴스에 대해 호출할 수 없습니다. Student.EnrollStudents([firstStudent, secondStudent]); // "Enrolling Students" 이제야 이 메소드를 실행할 수 있습니다.

그러니까 이 기능은 개별 인스턴스에 관한 것이 아닙니다. 그렇지만 Student라는 클래스의 일부이고, 학생들에 대한 어떤 역할을 하거나 헬퍼 메소드 또는 유틸리티 메소드에 관한 일을 하게 됩니다. 두번째 예시를 보겠습니다. MDN의 예시에서 가져왔습니다.

class Point { constructor(x, y) { this.x = x; this.y = y; } static distance(a, b) { const dx = a.x - b.x; const dy = a.y - b.y; return Math.hypot(dx, dy); } } const p1 = new Point(5, 5); const p2 = new Point(10, 10);

여기는 Point라는 이름의 클래스가 있습니다. 데카르트 사분면이라고 불리는 것 위에 한 점이 있다고 생각해 보겠습니다. X와 Y로 이루어진 좌표입니다. 만약에 이 좌표 체계를 가지고 작업을 한다면, 각 지점을 X값과 Y값을 가지고 있을 겁니다. 그리고 p1과 p2로 새로운 인스턴스를 생성합니다. Point 클래스 내부에는 distance와 같이 우리가 원하는 유틸리티 메소드를 만들 수 있습니다. 각 좌표 인스턴의들의 거리를 계산해주는 메소드이죠.

예를 들어서 p1과 p2 사이의 거리를 측정하기 원한다면 어떻게 해야 할까요?

Point.distance(p1, p2); // 7.071067....

이러면 이 둘의 거리가 나옵니다.
이를 왜 이런식으로 구성해야 하는 이유가 조금씩 이해가 가지 않나요?

당연히 모든 객체 지향 프로그래밍에서는 모든 작업을 이 클래스 문법을 가지고 합니다. 모든 것이 더 깔끔하고 쉽게 이해할 수 있고 더 논리적인 방식으로 구성되기 때문이죠. 또한 클래스 메소드를 사용하는 이유도 여기에 포함이 됩니다.

우리가 하는 일은 그냥 static 이라는 키워드를 그 앞에 붙여주기만 하면 됩니다. 그러면 클래스 메소드가 되는데, 이는 개별 메소드나 개별 인스턴스에 대해서 더 이상 호출하지 않는다는 말입니다. 그 대신 위의 Point 클래스나 앞서 봤던 예시의 Student처럼 클래스 자체에 대해 호출이 됩니다.


Recap


class DataStructure() { constructor() { // what default properties should it have? } someInstanceMethod() { // what should each object created from this class be able to do? } }

이제 위의 내용들을 간략하게 정리해보겠습니다.

클래스는 데이터 구조를 정의하는 데 쓰입니다. class BinarySearchTree, class DoublyLinkedList 등과 같이 말이죠. 좀 더 풀어서 말하자면, 인스턴스라는 것들을 만드는 설계도 입니다. 그리고 클래스는 new 키워드를 통해서 인스턴스화 됩니다.

그리고 그 안에는 constructor 가 있습니다. 우리가 단일 연결 리스트든 이진 탐색 트리든 필요한 모든 초기 설정되야 할 프로퍼티들이 들어있죠. constructor 함수는 클래스가 인스턴스화될 때 작동하는 특별한 함수입니다. new Student를 실행하면 constructor가 작동됩니다.

그리고 많은 양의 인스턴스 메소드가 있을 겁니다. 앞으로 보게 될 데이터 구조 중 일부는 정의해야 하는 메소드의 종류가 10개가 넘을 수도 있습니다. 인스턴스 메소드는 메소드와 객체처럼 클래스에 추가될 수 있습니다.

정적 메소드를 사용하는 경우는 아마 드물겁니다. 그렇지만 이를 모르고 지나치기에는 부족하기 때문에 위에서 간단하게 언급했습니다. 이러한 클래스 메소드는 static 키워드와 함께 추가가 됩니다.

그리고 마지막으로 이 단어를 기억해야 합니다. 'this 키워드'는 일반적인 것들과는 다르게 행동합니다.
ES 2015 클래스에서 this가 작동하는 방식은 인스턴스 메소드나 컨스트럭터 대신에, this는 개별 클래스에서 만들어진 객체, 즉 실제 인스턴스를 가리킵니다.


다음 포스팅에서는 class 추상화, 캡슐화, 다형성과 같은 단어들을 정의하고 실제로 어떻게 코딩을 해나가야 하는지에 대해서 정리하겠습니다.
# OOP# class# javascript
© 2021 Createhb21.