※ 책 [혼자 공부하는 자바스크립트]를 공부하며 기록, 정리해 놓은 노트이다.
목차
클래스의 기본 기능
객체 지향 패러다임, 추상화, 클래스, 인스턴스, 생성자
요약:
학생을 객체로 표현하고 학생 이름과 성적을 속성으로, 평균과 총점을 구하는 기능을 함수로 표현하려고 할 때 코드의 발전사:
- 학생(객체) 하나하나의 성적(속성)을 타이핑하여 객체들을 생성 + 학생들의 총점과 평균을 구하려고 for문을 돌림
- ⇒ 학생(객체) 하나하나의 성적(속성)을 타이핑하여 객체들을 생성 + 학생 객체를 받아 사용하는 함수로 총점과 평균을 구함
- ⇒ 학생(객체) 하나하나의 성적(속성)을 타이핑하여 객체들을 생성 + 만든 함수를 학생(객체) 각각에 장착시켜줌
- ⇒ 성적(속성)과 총점 평균 함수(기능)가 들어간 학생(객체)을 만드는 것도 하나의 함수로 구현함
- ⇒ 마침내 이 자체(속성과 기능을 가진 객체 만들기)를 함수 대신 클래스라는 붕어빵 틀로 표현하기로 함. 더 안전하고 효율적으로 바뀜.
객체 지향 패러다임
: 객체를 우선적으로 생각해서 프로그램을 만든다는 방법론.
추상화 abstraction
: 현실의 객체가 가지는 모든 정보를 데이터로 넣지 않고 프로그램에서 필요한 요소만을 사용해서 객체를 표현하는 것. 복잡한 자료나 모듈, 시스템 등으로부터 핵심적인 개념과 기능을 간추려내는 것.
ex. 병원 시스템 중 환자를 관리하는 프로그램을 만든다 ⇒ 환자의 머리카락 색깔, 손가락 개수 같은 정보는 무시하고 이름, 생년월일, 성별, 연락처 등만 속성으로 포함시킨다.
ex. 학생 성적 관리 프로그램을 만든다 ⇒ 학생이라는 객체로부터 성적 관리에 필요한 공통사항을 추출한다
다음은 일일이 타이핑해서 만들어줘야 하던 객체가 클래스라는 붕어빵 틀로 바뀌게 되는 과정이다:
1. 같은 형태의 객체 손수(노가다로) 만들기
// 객체와 배열을 조합한 학생 데이터 생김새 예시:
const students = []
students.push({ 이름: '구름', 국어: 87, 영어: 98, 수학: 88, 과학: 90 })
students.push({ 이름: '바다', 국어: 87, 영어: 98, 수학: 88, 과학: 90 })
students.push({ 이름: '산', 국어: 87, 영어: 98, 수학: 88, 과학: 90 })
// 객체{ }를 JSON 문자열로 변환
console.log(JSON.stringify(students, null, 4))
=> 결과:
[
{
"이름": "구름",
"국어": 87,
"영어": 98,
"수학": 88,
"과학": 90
},
{
"이름": "바다",
"국어": 87,
"영어": 98,
"수학": 88,
"과학": 90
},
{
"이름": "산",
"국어": 87,
"영어": 98,
"수학": 88,
"과학": 90
}
]
위의 데이터로 학생들의 성적 총합과 평균 구하기:
// 이름 총점 평균:
let output = `이름\t총점\t평균\n`
for (const student of students) {
student["총점"] = student.국어 + student.영어 + student.수학 + student.과학
student["평균"] = student.총점 / 4
output += `${student.이름}\t${student.총점}점\t${student.평균}점\n`
}
console.log(output)
=> 결과:
이름 총점 평균
구름 363점 90.75점
바다 363점 90.75점
산 363점 90.75점
2. 객체를 활용하는 함수
위의 총점과 평균을 구하는 과정을 함수로 만들어 두면 나중에 사용하기가 더 좋다. (확장성 고려)
⇒ 전체 코드가 객체를 만드는 부분과 객체를 활용하는 부분으로 나뉘게 됨
// 객체들을 선언
const students = []
students.push({ 이름: '구름', 국어: 87, 영어: 98, 수학: 88, 과학: 90 })
students.push({ 이름: '바다', 국어: 87, 영어: 98, 수학: 88, 과학: 90 })
students.push({ 이름: '산', 국어: 87, 영어: 98, 수학: 88, 과학: 90 })
// 학생 객체를 받아 총점을 구하는 함수
function getSumOf(student) {
return student.국어 + student.영어 + student.수학 + student.과학
}
// 학생 객체를 받아 평균을 구하는 함수
function getAverageOf(student) {
return getSumOf(student) / 4
}
// 출력
let output = '이름\t총점\t평균\n'
for (const student of students) {
output += `${student.이름}\t${getSumOf(student)점\t${getAverageOf(student)점\n`
}
console.log(output)
=> 결과:
이름 총점 평균
구름 363점 90.75점
바다 363점 90.75점
산 363점 90.75점
3. 객체의 기능을 메소드로 추가하기
getAverageOf(학생)같의 중의적인 이름으로 함수를 사용하면 어떤 종류의 객체를 넣어야 하는지 모를 수 있고 이름 충돌도 생길 수 있다. 그렇다고 getAverageOfStudent 같이 의미를 다 집어넣어 길게 작성하면 가독성이 떨어진다. 오오오 바로 내가 겪던 문제가 아닌가…
그래서 개발자들은 함수를 메소드로써 객체 내부에 넣어서 활용하기로 한다. 학생 객체 하나하나마다 getsum()과 getAverage() 메소드를 추가해 넣는 것이다. 그러면 ‘어떤’ 종류의 객체가 얘를 사용한다는 건지 (이름에)설명할 필요도 없고, 이름 충돌이 생길 일도 없다(?)
// 학생 객체마다 총점 함수와 평균 함수를 넣어주기 - 이름 충돌 방지, 긴_이름_방지:
for (const student of students) {
student.getSum = function () {
return this.국어 + this.영어 + this.수학 + this.과학;
}
student.getAverage = function () {
return this.getSum() / 4;
}
}
// 출력
let output3 = '이름\t총점\t평균\n'
for (const s of students) {
output3 += `${s.이름}\t${s.getSum()}점\t${s.getAverage()}점\n`
}
console.log(output3)
- ⇒ 근데 this.getAverage는, this에 getSum()이 있는지 없는지도 모르는데 막 갖다 쓰네?
4. 객체자체도 함수로 생성하기
// 학생 객체 자체도 함수로 생성하도록 만들기:
function createStudent(이름, 국어, 영어, 수학, 과학) {
return {
// 속성 선언
이름: 이름,
국어: 국어,
영어: 영어,
수학: 수학,
과학: 과학,
// 메소드를 선언
getSum() {
return this.국어 + this.영어 + this.수학 + this.과학;
},
getAverage() {
return this.getSum() / 4;
},
toString() {
return `${this.이름}\t${this.getSum()}점\t${this.getAverage()}점\n`
}
}
}
// 객체 선언하기
const students2 = []
students2.push(createStudent('구름', 87, 98, 88, 90))
students2.push(createStudent('바람', 92, 98, 96, 88))
students2.push(createStudent('바다', 76, 96, 94, 86))
students2.push(createStudent('산', 98, 52, 98, 92))
// 출력
let output4 = '이름\t총점\t평균\n'
for (const student of students2) {
output4 += student.toString()
}
console.log(output4)
=> 결과:
이름 총점 평균
구름 363점 90.75점
바람 374점 93.5점
바다 352점 88점
산 340점 85점
⇒ 문제: 객체별로 메소드를 생성하므로 함수라는 기본 자료형 (createStudent() 자체)보다 무거운 자료형이 여러 번 생성되는 문제가 있다고 한다. 즉, 저 createStudent()가 한 번 불려서 학생 하나를 만들 때마다 getSum(), getAverage(), toString()이라는 함수들이 선언되고 담긴 객체가 생성된다.
⇒ 클래스를 이용하면… 그래도 인스턴스를 생성할 때마다 메소드가 읽히는 거 아닌가? ⇒ 아닌가? 생성자가 불리는 거니까 일단 속성만 다 초기화가 되는 걸까? 메소드는 인스턴스마다 만들지는 않는 건가? 그래야 이같은 문제를 제기하고 ‘그래서 클래스를 사용한다~’와 같이 넘어가는 문맥이 말이 되긴 하다.
5. (마침내) 클래스(로) 선언하기
객체 지향 프로그래밍 Object Oriented Programming
: 객체들을 정의하고 그러한 객체들의 상호작용을 중심으로 개발하는 방법론.
프로그래밍 언어 개발자들은 크게 클래스와 프로토타입이라는 2가지 문법으로 객체를 효율적으로 만들 수 있게 했다.
클래스 class
: 객체를 안전하고 효율적으로 만들 수 있게 해주는 문법. 객체를 만들 때 수많은 지원을 하는 대신 많은 제한을 거는 문법
class 클래스 이름 { … }
클래스 이름 첫글자는 대문자로 짓는 것이 모든 개발 언어의(?) 약속!
프로토타입 prototype
: 제한을 많이 하지 않지만 대신 지원도 별로 하지 않는 문법
자바, C++, C#, 루비, 코틀린, 스위프트, PHP 등 현재 대부분의 언어는 클래스 기반의 객체 지향 프로그래밍 언어이다.
자바스크립트도 초기에는 (유연함의 대명사답게) 프로토타입 문법을 사용했지만 시대의 흐름에 따라 클래스 문법을 제공하기 시작했다.
인스턴스(객체) instance
: 클래스를 기반으로 생성한 객체
new 클래스 이름()
// 클래스 선언:
class Student {
...
}
// 인스턴스 하나 생성:
const student = new Student()
// 인스턴스 리스트 생성:
const students = [
new Student(),
new Student(),
...
]
생성자 constructor
: 클래스를 기반으로 인스턴스를 생성할 때 처음 호출되는 메소드
class 클래스 이름 { constructor ( ) { /* 생성자 코드 */ } }
클래스 내부에 정의할 때는 constructor()
라는 이름으로 나타내지만, 실제 호출될 땐 new Student()
처럼 클래스의 이름으로 호출된다.
생성자에서 객체의 속성을 초기화 한다.
// 클래스 내에 생성자 정의하기 예시
class Student {
constructor (이름, 국어, 영어, 수학, 과학) {
this.이름: 이름,
this.국어: 국어,
this.영어: 영어,
this.수학: 수학,
this.과학: 과학
}
}
const student = new Student() // constructor()가 실행됨.
메소드 method
객체의 메소드는 (생성자 바깥에) 따로 추가한다.
// 속성은 생성자에서, 그 밖의 메소드는 생성자 바깥에 줄줄이.
class Student {
constructor (이름, 국어, 영어, 수학, 과학) {
this.이름: 이름,
this.국어: 국어,
this.영어: 영어,
this.수학: 수학,
this.과학: 과학
}
// 메소드
getSum() {
return this.국어 + this.영어 + this.수학 + this.과학;
}
getAverage() {
return this.getSum() / 4;
}
toString() {
return `${this.이름}\t${this.getSum()}점\t${this.getAverage()}점\n`
}
// (+추가) 이런 형식도 된다:
findAllPost = async () => { ... } // = (비동기)'화살표' '메소드'
findAllPost2 = async function() { ... } // =(비동기)'익명' '메소드'
async findAllPost3() { ... } // =(비동기)'간단선언' '메소드'
// => "async function findAllPost3()"는 맞는 구문이 아니다..!
}
- 잠깐만… 왜 메소드끼리 콤마(,)로도 구분하고 아무것도 없이로도 구분하고 세미콜론(;)으로도 구분 가능한거지
⇒ 헐 newline(=’아무것도 없이’)이나 세미콜론(;)으로 메소드 구분이 가능한 거고 콤마(,)는 (아마) 구문오류가 나는 거 ㅅ같다… 아니 교과서…가 잘못됐거나, 아니면 내가 그냥 잘못 타이핑한 걸 수도.
- => 객체의 '메소드 간단 선언 구문'에서 다뤘었다.
const product = { 고객문의: function () { console.log(`this.제품명`) } }
⇒
const product = { 고객문의 () { console.log(`this.제품명`) } }
이런 식으로 객체 내부에서 익명함수의 선언을 더 간단히 하는 방법이 있었다. 클래스에서는 별 언급이 없었었는데 자동으로 ‘간단 버전’ 선언 구문으로 메소드를 작성하고 있었구나.
⇒ 객체와 클래스가 다른 점은, 속성과 속성을 콤마(,)로 구분하고 그렇지 않고의 차이 같다! (작성시 가장 크게 와닿는 부분)
⇒ 아 그리고 메소드 선언시 객체는 콜론(:)으로, 클래스는 등호(=)로 해준다는 큰 차이가 있다.
아무튼 그래서 .. 저렇게 3가지 메소드 작성 방식이 나올 수 있는 것이다.
- 객체에서 메소드를 만들 땐 메소드 안에서 사용되는 this 키워드가 가리키는 대상이 달라지는 이슈 때문에 화살표 함수는 사용하지 않고 ‘정식 익명 함수’나 ‘간단 선언 구문’으로 메소드를 선언하라고 배웠다. 클래스에서는 이런 제약이 없는가?
참고:
[책] 혼자 공부하는 자바스크립트 - 윤인성
Uploaded by N2T