본문 바로가기

개발공부/Javascript

[JS]프로토타입

프로토타입 기반의 객체 지향 언어이다.

상속과 재사용성을 가능하게 하는 중요한 메커니즘이다. 프로토타입 기반의 접근법에서는 객체가 다른 객체의 구조, 상태, 행동을 직접적으로 상속 받을 수 있고, 클래스를 사용하지 않고도 객체간의 관계를 정의할 수 있다.

이러한 접근 방식은 문맥이라고 하는 컨텍스트와 실행환경의 중요성을 강조하는 철학적 기반에서 출발한다. JS의 프로토타입 시스템은 실행문맥과 렉시컬 스코프를 중심으로 구성되어 있고, 이는 this 키워드의 동적 바인딩과 같은 복잡한 특성을 수반한다. 이런 특성은 함수의 호출 방식과 실행 문맥에 따라 this의 값이 결정되는 방식에서 잘 나타나고, 프로토 타입 기반 언어의 유연성을 증가시키는 것과 동시에 다소 복잡성도 증가시킨다. 결론적으로 프로토타입 기반의 객체 지향은 객체 간의 관계를 다루는 독특한 방식을 제공하고, 이는 문맥과 실행 환경의 중요성을 강조하는 비트겐슈타인의 철학 기반에 뿌리를 두고 있다.

 

 

비트겐슈타인 분류 이론 반박, 추상적인 개념을 특정 속성으로 규정하고 분류하는 것에는 한계가 있다. 의미의 사용과 맥락에 기반을 두어야 한다. 언어와 개념의 의미가 고정된 본질적인 속성에 의해 결정되는 것이 아니라, 맥락 속에서 사용되는 방식에 의해 형성된다고 본다. 언어의 삶의 흐름 속에서 의미와 개념을 가지게 된다. 가족 유사성은 같은 범주에 속한다고 여겨지는 개념들 사이에 명확한 공통 속성이 존재하지 않을 수 있다. 대신 유사성이 존재할 것이고 이런 유사성이 개념들을 연결하고, 특정 원형 Prototype을 기반으로 하여 분류하는 것이 더 타당하다. 언어 사용이 이루어지는 상황이 언어의 맥락을 결정한다.

 

JS와 같은 프로그래밍 언어에서 함수의 실행 컨텍스트와 this 키워드의 결정 , 변수의 호스팅과 같은 특성은 맥락의 중요성을 반영한다. 프로그래밍 언어에서 함수가 호출되는 시점과 위치, 그 주변의 코드 구조는 함수 내에서 this가 어떤 객체를 가리키게 될지, 변수와 함수 선언이 어떻게 처리되는지를 결정짓는 요소이다. 프로그래밍 언어의 구문과 의미가 코드가 작성되고 실행되는 구체적인 컨텍스트 즉 맥락에 의해 영향을 받는다. 이것이 JS는 왜 함수의 실행 컨텍스트가 형성될 때 this가 결정이 되는 이유이다.

 

실행컨텍스트와 렉시컬 스코프의 개념은 프로그래밍 언어의 어휘적 구조와 실행 환경의 상호작용을 기반으로 한다. 변수의 의미는 변수가 선언된 렉시컬 환경과 실행 컨텍스트에 따라 결정 된다. 프로토 타입 기반 언어인 JS는 변수의 의미를 결정하는 근처 환경을 어휘적 범위인 렉시컬 scope로 정의를 한다. 이는 코드의 문법적 구조에 의해 결정이 된다. JS 엔진은 코드가 실행될 때 실행 컨텍스트를 생성하고, 해당 컨텍스트 내에서 변수와 함수 선언을 컨텍스트의 최상단으로 이동시키는 것 같은 호이스팅을 거친다. JS에서 this 키워드의 의미는 사용되는 시점과 맥락에 따라 변화가 된다.

비트겐슈타인의 관점은 프로토타입과 클래스 기반 프로그래밍의 근본적 차이점을 설명하는 데 유용하다. 클래스 기반 프로그래밍에서는 미리 정의된 클래스의 구조가 중심이 된다. 프로토타입 기반의 프로그래밍에서는 객체의 상호 작용과 맥락 (Context)가 더 중요하다. 프로토타입 기반 언어에서 this는 함수 호출의 맥락, 즉 해당 함수를 호출하는 객체의 컨텍스트를 반영한다. 이는 받아들이는 주체와 그 주체의 맥락이 언어의 사용과 의미를 결정하는 비트겐슈타인의 관점과 일맥상통하다. 프로토타입 기반 프로그래밍 언어에서는 실행되는 객체의 맥락이 함수의 의미와 동작을 결정하는 핵심 요소가 된다. 객체는 다른 객체를 바로 상속할 수 있다. 클래스 기반 프로그래밍보다 가볍게 프로토타입 체이닝을 사용하여 상속한다. 클래스 없이도 객체지향프로그래밍을 가능하게 하도록 했다. 이러한 접근 방식은 객체지향 프로그래밍에 있어 보다 동적이고 유연하도록 만들었다.

 

프로토타입 기반 언어의 상속

Java와 같은 클래스 기반 언어는 복사를 통해 상속을 구현한다. 이 과정은 컴파일시에 이루어진다. 반면, JS와 같은 프로토타입 기반 언어는 프로토타입 링크를 통해 상속을 구현한다. JS에서 모든 객체는

다른 객체를 참조하는 내부 프로토타입 속성 혹인 프로토타입 링크를 가지고 있고, 참조된 객체가 바로 그 객체의 프로토타입이다. 객체에서 특정 속성이나 메소드를 찾을 때, 그것이 현재 객체에 없으면 JS는 프로토타입 링크를 따라 속성이나 메소드를 찾는다. 프로토타입의 프로토타입으로 계속 이어지고 최상위에 도달할 때까지 계속된다. 프로토타입 기반의 상속에서는 속성이나 메소드가 복사되지 않는다. 대신, 객체는 프로토타입 체인을 통해 필요한 속성이나 메소드에 접근할 수 있다. 이러한 메커니즘은 런타임에 동적으로 수행이 되고, 객체의 프로토타입을 변경하므로써 상속 구조를 동적으로 수정할 수 있다. 이는 프로토타입 기반 언어에서 상속이 클래스 기반 언어와 비교했을 때, 유연하고 동적인 특성을 가지는 이유이다.

console.dir()

웹브라우저의 JS 콘솔에서 사용할 수 있는 유용한 디버깅함수이다. 인자로 받은 JS 객체의 속성을 탐색가능한 트리구조로 출력하여 객체 내부의 속성과 메소드를 상세하게 살펴볼 수 있다. 또한, 객체의 프로토타입 체인 정보도 함께 볼 수 있다. 객체 간 상속을 확인할 수 있다.

 

 

객체의 프로토타입

const myObj = {};
console.dir(myObj);

 

myObj는 JS의 기본 객체 중의 하나인 Object의 인스턴스로 표시가 된다. 이는 JS에서 모든 객체가 기본적으로 Object를 상속받는 다는 것을 확인할 수 있다. 이 객체의 속성 중에는 [[Prototype]] 링크도 포함이 되어있다. 이는 JS 프로토타입 기반 상속 메커니즘을 보여준다. [[Prototype]] 링크는 해당 객체가 생성될 때 객체의 원형을 가리키고, 프로토타입은 객체가 상속받은 속성과 메소드를 보유한다.

Object
	> [[Prototype]]:Object
		...
		**> __proto__**: Object
			...
			**> __proto__**: null

객체의 프로토타입 링크를 따라 가다보면, 최상위에는 null이 위치하게 된다. 이는, 프로토 타입의 은 null이라는 것을 의미한다. 더이상 상속받을 원형이 없다는 것을 의미한다.

 

배열의 프로토타입

const arr = [];
console.dir(arr);

result console

Array(0)
	> [[Prototype]]: Array(0)
			...
		> indexOf: ƒ indexOf()
			...
		> push: ƒ push()
			...
		> [[Prototype]]: Object

자주 사용하는 배열의 메서드 들이 있는 것을 확인할 수 있다. 마지막에 프로토타입 링크를 확인해보면, Object가 표시되는 것을 확인할 수 있다. JS에서 배열 또한 객체이다. 궁극적으로 모든 프로토타입은 Object에서 파생된다는 중요한 개념을 확인할 수 있다. 이러한 계층 구조 덕분에 이 배열은 Object 프로토타입에 정의가 된 객체의 메서드들을 사용할 수 있다. 예를 들어 hasOwnProperty 같은 메서드를 프로토타입 링크를 통해 사용할 수 있다.

 

const fruits = ['apple', 'banana'];

console.log(fruits.hasOwnProperty(1));
console.log(fruits.hasOwnProperty(2));

result console

true
false

이 예제는 객체를 프로토타입으로 상속 받는 배열에서도 hasOwnProperty 메서드를 사용할 수 있음을 확인할 수 있다.

const fruits = ['apple', 'banana'];
console.dir(fruits);

result console

Array(2)
	0: "apple"
	1: "banana"
	length: 2
	> [[Prototype]]: Array(0)
	...

fruits를 브라우저의 console.dir() 로 확인해보면, 배열의 메서드들을 확인할 수 있다. 이러한 메서드들은 배열 인스턴스의 프로토타입인 [[Prototype]]:Array(0) 에 정의가 되어있고, 프로토타입 체인을 통해 배열 인스턴스에서 접근이 가능하다. 따라서, 이러한 메서드들은 프로토타입 링크를 비교연산자로 표현해보면, true를 반환한다.

console.log(fruits.forEach === fruits.__proto__.forEach)

result console

true

배열에서 객체의 메서드를 사용하는 것을 프로토타입 링크로 표현해보면,

fruits.__proto__.__proto__ === Object.prototype

result console

true

 

JS에서 모든 객체가 궁극적으로는 Object의 모든 메소드와 속성을 상속받는 다는 사실을 확인할 수 있다.이를 통해 배열은 자신의 배열 메서드 뿐 아니라 Object로 부터 상속받는 메소드도 프로토타입 체이닝을 통해 사용할 수 있다.

JS의 프로토타입 상속 메커니즘은 언어의 가장 근본적인 특징 중 하나이다. 객체 간의 속성과 메서드를 공유하는 방법이다. 이 메커니즘의 핵심은 개발자가 직접 명시적으로 프로토타입 링크 속성을 쓰지 않아도, JS엔진이 내부적으로 프로토타입 체인을 관리하는 것과 필요한 속성과 메소드를 찾아주는 데에 있다. 프로토 타입 상속의 작동 원리는 JS에서 모든 객체는 다른 객체로 부터 속성과 메서드를 상속받을 수 있고, 이러한 상속 관계는 프로토타입 체인을 통해 구성이 된다. 객체의 프로토타입 체인은 그 객체가 생성될 때 정의가 되고, 객체의 타입 생성자 함수에 따라 달라진다. 객체의 메서드나 속성에 접근하려고 할 때, JS엔진은 다음과 같은 과정을 거친다. 먼저, 해당 객체에 요청된 속성이나 메서드가 직접적으로 존재하는지 확인한다. 해당 속성을 찾을 수 없는 경우, JS엔진은 객체의 프로토타입 즉 부모 객체에서 해당 속성을 찾는다. 이 과정을 프로토타입 체인을 따라 계속해서 상위 프로토 타입으로 이동하고, 요청한 속성이나 메서드를 찾을 때까지 반복한다. 요청된 속성이나 메서드를 찾으면 그것을 반환하고, 만약 최상위 프로토타입에 이르러서도 찾지 못하면 undefined를 반환한다. 이러한 메커니즘으로 개발자가 직접 프로토타입 링크를 쓰지 않아도 자동으로 프로토타입을 통해 객체의 메서드를 상속받아 사용할 수 있다.

 

 

프로토타입 상속의 장점

프로토타입 상속 메커니즘은 코드의 재사용성을 향상시키고, 메모리를 효율적으로 사용할 수 있게 한다. 생성자 함수로부터 생성된 모든 인스턴스는 메서드를 공유할 수 있기 때문에, 각 인스턴스가 메서드의 복사본을 가질 필요가 없다. 따라서 이러한 특성은 많은 수의 객체를 생성할 때 유용하다.

Q. Object.keys 같은 객체 메서드가 프로토타입에 없는 이유는?

JS에서는 프로토타입 메서드와 전역 객체에 정의된 정적 메서드를 구분한다. 서로 다른 용도와 사용 방법을 가진다. 프로토타입에 정의된 메서드는 모든 객체 인스턴스에서 사용할 수 있다. 이 메서드들은 객체 인스턴스를 통해 호출이 되고, 객체 내부 상태에 접근하고, 조작할 수 있다. 이러한 프로토타입 메서드는 객체 인스턴스의 프로토타입 체인에 존재하기 때문에, 모든 객체는 이 메서드를 상속 받아 사용할 수 있다. 예를 들면, hasOwnProperty같은 경우 객체나 객체를 상속받는 배열에서도 사용이 가능하다. 반면 Object.keys 같은 전역 객체의 정적 메서드는 특정 객체 인스턴스에 바인딩 되지 않고, Object 타입의 메서드로 직접 호출이 된다. 이 메서드들은 주로 객체를 조작하거나, 객체에 대한 정보를 얻는데 사용이 된다. 이 정적 메서드는 특정 객체 인스턴스의 메서드가 아니라, Object함수 자체의 메서드이기 때문에 이러한 메서드들을 사용할 때에는 객체 인스턴스 대신 Object 함수를 직접 사용한다.

const obj = { a: 1 };
console.log(Object.keys(obj));

result console

[ 'a' ]

객체에서만 사용할 메서드를 Object 프로토타입에 정의하지 않고, 따로 Object의 전역 객체의 정적 메서드에 정의하는 이유는 다음과 같다. 메서드의 적용 범위, 상속 구조, 명시성, 안전성과 관련이 있다.

 

  1. 적용 범위와 상속: Object 프로토타입에 메서드를 추가하면, JS 모든 객체가 해당 메서드를 상속받는다. 그런데, Object.keys와 같은 객체 내부 속성의 열거를 위한 메서드는 객체에만 필요하다. 배열이나 함수같은 객체는 이 메서드의 상속이 불필요 하다. 또한, 정적 메서드를 사용하면, 메서드가 적용되는 객체의 타입이 명확해진다. Object.keys에서 인자로 받는 Object가 일반 객체임을 명시적으로 나타낸다. 반면에, 프로토타입으로 상속받은 메서드는 어떤 타입의 객체인지 명확하지 않을 수가 있다.
  2. 프로토타입 오염 방지: 프로토타입 체인을 수정하는 것은 프로토타입 오염이라는 보안 문제를 일으킬 수 있다. 악의적으로 Object 프로토타입을 추가하거나 변경하면, 이를 상속받는 모든 객체에서 해당 코드가 실행되도록 할 수 있다. 이러한 위험성 때문에 ECMAScript 표준에서는 새로운 메서드를 추가할 때, 프로토타입에 추가하기 보다는 Object 전역 객체에 정적 메서드로 추가하는 것을 선언한다.
  3. 상속 구조와 명확성: 배열이나 함수 등의 내장 객체는 자체 프로토타입 체인을 가지고 Object 프로토타입이 상속 체인의 맨 위에 위치한다. Object 프로토타입에서 메서드를 추가하면 이 배열과 함수 같은 이러한 객체 타입이 해당 메서드를 상속받게 되고, 특정 메서드가 모든 객체 타입에 적합하지 않은 경우, 예를 들어 Object.keys와 같은 메서드를 추가하면 혼란을 초래할 수 있다.
  4. 명시성과 사용 편의성: Object 전역 객체에 정적 메서드로 정의를 하면 개발자는 해당 메서드가 특정 객체에만 적용될 것임을 인식할 수 있다. 코드 가독성과 명시성이 증가된다.

결론적으로는, 객체에서만 사용할 메서드는 프로토타입 링크가 아닌 Object 전역 객체에 정적 메서드로 정의하는 것은 명시성, 안전성, 상속 구조와 관련된 이유 때문이다. 이러한 접근 방식은 JS의 유연성과 동적 특성을 유지하면서도 코드의 안전성과 명확성을 보장한다.

JS에서 ES6에 도입된 클래스 문법은 내부적으로 프로토타입 기반 메커니즘을 사용하여 구현되어있다. 클래스 문법은 기존에 프로토타입 기반 상속을 보다 명확하고 이해하기 쉬운 구문으로 작성할 수 있게 해준다. JS의 클래스는 함수로 정의되어 있고, 이 함수의 프로토타입 객체가 인스턴스들 간에 공유되는 메서드를 보유한다. 클래스의 생성자 함수는 new 키워드와 함께 호출될 때 인스턴스를 생성하고 초기화 한다. ES6의 클래스 문법을 사용하는 것이 직관적이고 명확하다. 클래스 문법은 내부적으로 프로토타입 상속 모델을 사용하여 인스턴스 간에 메서드를 공유한다. 따라서, 이 클래스 문법은 JS의 기본적인 프로토타입 메커니즘 위에 구축된 일종의 추상화 레이어로 이해할 수 있다.

 

정리

JS 의 프로토타입 기반 상속은 전통적인 클래스 기반 언어와는 다른 개념적 접근을 제공한다. 클래스 기반 언어는 상속을 할 때, 클래스 간의 계층적 관계를 통해 구현이 된다. 부모 클래스의 상속과 메서드는 자식 클래스에 복사가 된다. 이러한 접근 방식은 계층적이고 정적이다.

프로토타입 기반 언어에서는 클래스라는 전통적 개념이 존재하지 않고, 객체는 다른 객체의 프로토타입을 바탕으로 생성될 수 있고, 이를 프로토타입 상속이라고 한다. 이 과정에서 중요한 역할을 하는 것이 프로토타입 링크이다. 객체가 다른 객체의 속성과 메서드에 접근할 수 있게 하는 내부 링크이다. 이 링크를 통해 객체는 상위 프토토타입에 이르기 까지 체인을 따라가며 필요한 속성이나 메서드를 찾을 수 있다.

프로토타입 객체의 원형이며, JS에서 모든 객체는 프로토타입이라는 내부 속성을 가지고 있다. 객체 리터럴을 통해 생성된 객체의 경우에는 Object 프로토타입을 가리킨다. 이 프로토 타입 상속은 프로토타입 링크를 통해 이루어지고, 객체에서 속성이나 메서드를 찾을 때, 해당 객체 내부에서 찾지 못하면 JS엔진은 프로토타입 링크를 통해 프로토타입 체인을 계속 탐색하여, 끝까지 해당 속성이나 메서드를 찾는다. 끝내 찾지 못하면 undefined를 리턴한다.

정적메서드는 객체의 인스턴스가 아닌 객체 생성자 자체에 직접 추가되는 메서드이다. 정적 메서드는 인스턴스화 없이 클래스 레벨에서 호출이 되고, 주로 유틸리티 함수나 인스턴스에 의존하지 않는 작업에 사용이 된다.

 

[참고]

패스트캠퍼스 강의 (내 생에 마지막 JavaScript : 기초 문법부터 실무 웹 개발까지 한 번에 끝내기 초격차 패키지 Online) 를 기반으로 작성하였고, 추가 서치 내용을 정리하였습니다.

'개발공부 > Javascript' 카테고리의 다른 글

[JS] this  (0) 2024.05.12
[JS] ESM, CJS 모듈 시스템  (0) 2024.05.05
[JS] 이벤트 루프  (1) 2024.04.18
[JS] 비동기  (0) 2024.04.10
[JS] 이벤트 버블링과 캡처링  (0) 2024.04.04