[JavaScript] 객체 지향 프로그래밍

2023-09-18

객체 지향 프로그래밍


객체 지향 프로그래밍 정의

캡슐화

상속

추상화

다형성

프로토타입 기초


자바스크립트에는 클래스가 존재하지 않는다

많은 사람들이 프로토타입을 설명할 때 클래스의 상속과 연결지어 설명하는데 전혀 다른 개념입니다. 자바스크립트에는 클래스라는 개념이 없기 때문입니다. ‘복사’를 통한 상속도 존재하기 않습니다. prototype은 클래스, 객체의 내용 복사 없이도 상속을 구현할 수 있게 해주는 방법입니다.

  • Prototype은 ‘연결’입니다.

클래스가 없다면 객체를 어떻게 설계대로 찍어내는 것일까?

  • 클래스가 아니라면 return이 없는데 객체가 어떻게 생성되는 것일까?
function Person(name) {
	this.name = name;
	this.sayHello = function() {
		console.log(`${this.name}: hello!`);
	}
}
 
const chris = new Person('chris');

위 코드를 예시로 설명하겠습니다.

  1. new 연산자가 새로운 빈 객체를 메모리 상에 생성합니다.
  2. 생성된 빈 객체가 this에 바인딩 됩니다.
  3. this 객체의 속성을 채우는 동작이 수행됩니다.
  4. return하는 것이 없다면 그렇게 만들어진 this가 return됩니다.

일반적인 ‘일부 내용 복사’로서의 상속

class Person {
	constructor(name) {
		this.name = name;
	}
	
	sayHello() {
		console.log(`${this.name}: hello!`);
	}
}
 
class Crew extends Person {
	constructor(name) {
		super(name);
	}
 
	doCoding() {
		console.log(`${this.name}: coding...!`);
	}
}

자바스크립트에서는 다른 언어와는 상속하는 과정이 아예 다릅니다. 자바스크립트는 깊은 복사를 수행하지 않습니다. 복사할 수 있는 것은 오로지 원시 값과 참조 값 뿐입니다. 그래서 자바스크립트는 상속을 흉내내기 위해서 ‘연결’이라는 기능을 사용합니다. 이 연결은 __proto__(프로토)라는 속성을 이용합니다.

객체간의 연결 관계 이해하기


자바스크립트의 모든 객체는 __proto__(프로토) 속성을 가지고 있습니다. 이 속성은 객체와 객체간의 연결을 해주는 하나의 Link라고 생각을 해주시면 됩니다.

1. 다른 객체를 바탕으로 만들어진 객체라면

  • 객체는 자신의 원형(유전자)이라고 할 수 있는 객체가 있다면 그 객체를 가리키는 __proto__ 링크를 자동으로 가집니다.
    • 예시
    const newObj = Object.create(oldObj);
    newObj.__proto__ === oldObj

2. 그냥 객체가 아니라 함수라면

  • 이 경우에는 __proto__(프로토) 외에도 만들어지는 것이 있습니다. 그것이 바로 함수의 프로토타입 객체입니다.

  • 예를 들어 Person 함수가 있다면 Person과 자동으로 연결된 프로토타입 객체가 함께 만들어집니다. Person함수의 프로토타입 객체는 그 프로토타입 객체 안에 constructor라는 속성을 통해 Person함수를 가리키는 순환 체계를 갖고 있습니다.

    230922-150638

3. new + 함수로 만들어진 객체라면

  • 만들어진 새로운 객체에 __proto__ 링크가 Person 객체의 prototype을 가리킵니다.

    230922-150705

프로토타입


프로토타입 기반 언어란

JavaScript에는 클래스라는 개념이 없습니다. 결국 ES6에 추가된 클래스도 syntatic sugar입니다. 클래스인 척을 하는 특수한 함수이고 객체인 것이죠.

프로토타입 기반 언어는 객체 원형인 프로토타입을 이용하여 새로운 객체를 만들어냅니다. 이렇게 생성된 객체 역시 또 다른 객체의 원형이 될 수 있습니다. 원형을 이해하시기 여러우면 유전자라고 생각하셔도 됩니다.

프로토타입은 객체를 확장하고 객체 지향적인 프로그래밍을 할 수 있게 해줍니다.

프로토타입이란

프로토타입은 객체의 원형(유전자)입니다.

아래 코드에서 sort 메서드를 사용할 수 있는 이유는 JavaScript 내부적으로 배열 자료형에는 sort 메서드를 쓸 수 있게 처리해 놓았기 때문일 것입니다. 바로 상속이라는 개념을 통해서요.

const arr = [3, 2, 1];
arr.sort(); // [1, 2, 3]

위 코드에서 arr이라는 변수는 배열 리터럴로 생성했지만 내부적으로는 new Array()같이 생성자 함수로 선언한 것입니다. new라는 키워드는 객체를 생성하는 방법 중, 생성자 함수(Constructor)를 사용하여 객체를 만들 때 함께 쓰는 키워드입니다. new 생성자함수명()의 형식을 통해 자바스크립트에서 동일한 구성을 가진 객체를 여러 개 만들어 낼 수 있습니다.

arr 배열 객체의 원형은 Array입니다. 그러면 객체 == 프로퍼티일까요?

정확히는 아닙니다. 함수 객체가 가지는 프로토타입이라는 특수한 형태의 객체 프로퍼티를 통해 상속받습니다.

230922-150724

Array.prototype을 통해 새롭게 데이터를 만들 수도 있습니다.

Array.prototype.name = '어레이'; //
const arr = [1, 2, 3];
arr.name; // '어레이'

당연히 Array.prototype을 통해 기존 메서드를 오염시킬 수도 있습니다.

const arr = [3, 2, 1];
Array.prototype.sort = function() {console.log('파괴')};
arr.sort(); // 파괴

상속이 .prototype 프로퍼티를 통해서 가능하다면 인스턴가 자신의 프로토타입은 .__proto__를 통해서 가능합니다.

230922-150742

// 부모 클래스
function 기계() {
	this.q = 'strike';
	this.w = 'snowball';
}
 
기계.prototype.name = 'kim'; // 부모 유전자에 추가
 
var nunu = new 기계(); // 자식 클래스
 
console.log(nunu.name);

위 코드는 prototype 객체를 이용해 key 값이 name이고 value 값이 ‘kim’인 객체를 기계라는 부모 유전자에 추가해주는 코드입니다.

여기서 nunu.name이 분명 존재하지 않는데 출력되는 이유는 JavaScript가 코드가 실행될 때 nunu.name이 존재하지 않으면 부모 유전자까지 찾아서 존재하면 출력합니다. 부모 유전자에도 없으면 부모의 부모 유전자까지 부모가 존재하지 않을 때까지 계속 찾습니다. 이것을 프로토타입 체인이라고 합니다.

프로토타입 체인


인스턴스 객체의 key에 접근할 때, 해당 객체에게 key가 없다면 그 다음으로 상위 프로토타입(원형, 유전자) 속성에서 key가 있는지 확인합니다. 없다면 그것을 찾기 위해 더 상위의 프로토타입(부모)에서 찾는다. 이것을 프로토타입 체인이라고 한다.

Array의 프로토타입을 알아봅시다. Array의 프로토타입은 바로 Object입니다.

우선 Array는 함수입니다. 함수는 객체입니다. 함수 객체만 프로토타입 프로퍼티를 가질 수 있고 인스턴스를 생성할 수 있습니다.

230922-150803

  • 그렇다면 프로토타입 체인의 끝나는 지점은 어디일까요? null

프로토타입 체이닝 이해하기

  • 다음 코드가 있다고 가정해봅시다. 당연히 에러가 납니다. Person에는 sayHello라는 메서드가 없기 때문입니다.
function sayHello() {
	console.log(`${this.name}: hello!`);
}
 
function Person(name) {
	this.name = name;
}
 
const chris = new Person('chris');
chris.sayHello();
  • 만약 코드를 이렇게 만들어봅시다.
function sayHello() {
	console.log(`${this.name}: hello!`);
}
 
function Person(name) {
	this.name = name;
}
 
Person.prototype.sayHello = sayHello;
 
const chris = new Person('chris');
chris.sayHello();

이렇게 되면 코드가 잘 실행이 됩니다. 이유가 무엇일까요? 바로 프로토타입 체이닝 때문입니다.

프로토타입 체이닝을 한마디로 정리하자면 __proto__를 따라 탐색하기입니다.

클로저 모듈 패턴


클로저(closure)란

모듈 패턴(module-pattern)이란

클로저 모듈 패턴이란

클로저 모듈 패턴 구현

만약 똑같은 기능을 하는 함수가 여러 개가 필요하다면 어떻게 해야 할까요? 같은 코드를 그대로 복사/붙여넣기 하는 것은 재사용성이 떨어집니다.

똑같은 기능을 하는 함수를 여러 개 만드는 방법 중 하나는, 아래 코드와 같이 클로저 모듈 패턴을 이용할 수 있습니다.

function makeCounter() {
  let value = 0;
  return {
    increase: function() {
      value++;
    },
    decrease: function() {
      value--;
    },
    getValue: function() {
      return value;
    }
  }
}
 
let counter1 = makeCounter()
counter1.increase()
counter1.getValue() // 1
 
let counter2 = makeCounter()
counter2.decrease()
counter2.decrease()
counter2.getValue() // -2

클로저 모듈 패턴의 구현 방법은 다양합니다. 그러나 일반적으로는 즉시 실행 함수 표현식(IIFE)을 사용하여 모듈을 정의하고, 모듈의 공개 API를 반환하는 방식으로 구현합니다. 이를 통해 비공개 함수와 변수를 정의하고, 공개 함수와 변수만 외부에 노출시킵니다.

var module = (function() {
  // 비공개 함수와 변수
  var privateFunction = function() {
    console.log('This is a private function.');
  };
 
  var privateVariable = 'This is a private variable.';
 
  // 공개 함수와 변수
  return {
    publicFunction: function() {
      console.log('This is a public function.');
    },
 
    publicVariable: 'This is a public variable.',
 
    // 비공개 함수와 변수에 대한 접근 권한을 가진 공개 함수
    accessPrivate: function() {
      privateFunction();
      console.log(privateVariable);
    }
  };
})();
 
// 모듈의 공개 함수 및 변수 사용
module.publicFunction(); // 'This is a public function.'
console.log(module.publicVariable); // 'This is a public variable.'
 
// 모듈의 비공개 함수와 변수에 대한 접근 권한을 가진 공개 함수 호출
module.accessPrivate(); // 'This is a private function.' 'This is a private variable.'

위 코드에서는 IIFE(즉시 실행 함수 표현식)를 사용하여 모듈을 생성하고, 비공개 함수와 변수를 정의하고, 공개 함수와 변수를 반환합니다. 반환된 객체는 모듈의 공개 API를 나타내며, 비공개 함수와 변수는 해당 함수 내에서만 액세스할 수 있습니다. accessPrivate 함수를 통해 공개 함수에서 비공개 함수와 변수에 접근할 수 있습니다.

클래스와 인스턴스


클래스 정의

인스턴스 정의

클래스와 인스턴스의 예시

class Car {
	constructor(brand, name, color) {
		// 인스턴스가 만들어질 때 실행되는 코드
	}
}

constructor 함수는 객체 지향 프로그래밍에서 생성자 함수라고 부릅니다. 인스턴스가 만들어질 때 실행되는 코드입니다.

let avante = new Car('hyundai', 'avante', 'black');
let mini = new Car('bmw', 'mini', 'white');
let beetless = new Car('volkswagen', 'beetles', 'red');

위 코드는 Car 클래스에 대한 인스턴스를 만드는 예시입니다. 인스턴트는 new 키워드를 통해 만들어집니다. 인스턴스를 만드는 즉시 생성자 함수가 실행됩니다. 각각의 인스턴스는 클래스의 고유한 속성과 메서드를 갖게 됩니다.

// ES5
function Car(brand, name, color) {
	this.brand = brand;
	this.name = name;
	this.color = color;
}
 
// ES6
class Car {
	constructor(brand, name, color) {
		this.brand = brand;
		this.name = name;
		this.color = color;
	}
}

위 코드에서 this라는 새로운 키워드가 등장합니다. this는 한 마디로 인스턴스 객체를 의미합니다. 매개변수로 넘어온 브랜드, 이름, 색상 등은 인스턴스 생성 시 지정하는 값입니다.

// ES5
function Car(brand, name, color) { /* 생략*/ }
Car.prototype.refuel = function() { // 메서드 정의
	// 연료 공급을 구현하는 코드
}
Car.prototype.drive = function() { // 메서드 정의
	// 운전을 구현하는 코드
}
 
// ES6
class Car {
	constructor(brand, name, color) { /* 생략 */ }
		refuel() {
	}
		drive() {
	}
}

위 코드는 메서드 정의를 하는 코드입니다. ES5 문법에서는 prototype이라는 키워드를 사용해야 메서드를 정의할 수 있었습니다. 그러나 ES6 문법 이후에는 생성자 함수와 함께 class 키워드 안쪽에 묶어서 정의합니다. refuel() {}, drive() {}와 같이 작성되어 있는 부분입니다.

클래스와 인스턴스의 관계

javascriptclassprototype

프로필 사진
TaeWoo Kim
Junior Frontend Engineer