JS 넌 누구냐

이영민·2026년 1월 6일
post-thumbnail

자바스크립트를 공부하다 보면 프로토타입(Prototype)이라는 독특한 개념이 등장한다. JS가 기존의 객체지향 언어와 다른 목표와 철학을 가지고 개발되지 않았을까? 라는 생각이 들어 관련 내용을 찾아보았다.

전통적인 객체지향 언어인 자바(Java)나 C++은 플라톤의 이데아론을 충실히 따른다. 현실의 사물이 존재하기 위해서는 완벽한 설계도(Class)가 선행되어야 하며, 실제 객체(Instance)는 그 설계도를 본떠 만들어진다는 관점이다. 붕어빵 틀이 있어야 붕어빵을 찍어낼 수 있듯이, 클래스 기반 언어에서는 추상화된 설계도가 실재보다 우선한다. 이를 '추상화 우선주의'라 부를 수 있다.

반면 자바스크립트의 철학은 설계도를 버리고 존재부터 시작한다. "설계도는 존재하지 않는다. 오직 구체적인 객체들만 존재할 뿐이다"라는 실존주의적 태도를 취하는 것이다. 만약 새로운 객체가 필요하다면 복잡한 설계도를 새로 그리는 대신, 이미 존재하는 객체를 원형(Prototype)으로 삼아 이를 복제하거나 연결한다. 맛있는 '원조 붕어빵'이 있다면 그 붕어빵을 기준으로 삼아 앙금을 바꾸거나 반죽을 덧붙여 새로운 맛을 만들어내는 식이다.

이러한 유연함은 1995년 브렌던 아이크가 자바스크립트를 설계할 당시의 시대적 요구와 맞닿아 있다. 당시의 웹은 지금보다 훨씬 가벼워야 했고, 브라우저 환경은 변화의 속도가 매우 빨랐다. 엄격한 클래스 계층 구조를 미리 설계하는 것은 역동적인 웹 환경에 너무나 무거운 짐이었다. 그래서 그는 "일단 객체를 만들고, 나중에 필요하면 기능을 덧붙이거나 다른 객체와 연결하자"는 전략을 세웠다. 이것이 바로 자바스크립트 상속의 핵심인 '위임(Delegation)'의 미학이다.

자바의 상속이 부모의 유산을 자식의 통장에 직접 '복사'해주는 방식이라면, 자바스크립트의 상속은 부모의 '신용카드'를 들고 다니는 것과 같다. 나에게 당장 돈(메서드)이 없더라도 당황하지 않고 연결된 부모의 카드를 긁어 결제하듯, 객체는 자신에게 없는 기능을 부모격인 프로토타입 객체에 물어보고 빌려 쓴다. 이 메커니즘 덕분에 수만 개의 객체가 생성되더라도 공통 기능은 프로토타입이라는 단 하나의 '보물 창고'에만 존재하게 되어 메모리를 혁신적으로 아낄 수 있다.

이 위임의 원리가 실제로 구현되는 과정이 바로 프로토타입 체이닝(Prototype Chaining)이다. 우리가 객체의 특정 속성을 호출하면 엔진은 객체 자신에게 해당 기능이 있는지 먼저 확인한다. 만약 없다면 __proto__라는 이름의 연결 선을 타고 부모 객체로 올라가 탐색을 이어간다. 이 여정은 조상을 거슬러 올라가 모든 객체의 시작점인 Object.prototype에 도달할 때까지 반복된다. 우리가 정의한 적 없는 toString() 같은 메서드를 모든 객체에서 쓸 수 있는 이유도 결국 이 체인의 끝에 모든 객체의 공통 조상이 그 기능을 보관하고 있기 때문이다. 자바스크립트에서 Object.prototype 는 아담과 이브라고 할 수 있겠다.

아래의 코드를 보고 조금 더 자세히 이해해보자.

자바스크립트의 프로토타입은 단순히 객체를 복사하는 수준을 넘어, 객체 간의 관계가 거대한 네트워크와 같다. 이 철학이 실제 코드에서 어떻게 구현되고 동작하는지, 핵심적인 세 가지 갈래로 나누어 살펴본다.

1. 설계도 없는 실존: Object.create로 보는 원형의 연결

자바스크립트에서 상속은 새로운 설계도를 그리는 작업이 아니라, 이미 존재하는 객체를 나의 '원형(Prototype)'으로 지목하는 것에서 시작한다. 이는 "나는 저 객체의 특징을 기본으로 삼겠다"는 선언과 같다. Object.create 메서드는 이러한 철학을 가장 직관적으로 보여주는 도구이다.

// '원조 붕어빵'이라는 실제 객체가 먼저 존재한다.
const originalBread = {
  material: "팥",
  status: "따끈함",
  describe: function() {
    console.log(`이 붕어빵은 ${this.material}이 들어있고 ${this.status} 상태이다.`);
  }
};

// 새로운 붕어빵을 만들 때, originalBread를 원형(Prototype)으로 지정한다.
const creamBread = Object.create(originalBread);

// creamBread 자체에는 material이 없지만, 원형에서 빌려와 출력한다.
creamBread.describe(); // 이 붕어빵은 팥이 들어있고 따끈함 상태이다.

// 필요한 부분만 수정(적응)하여 자신만의 개성을 갖는다.
creamBread.material = "슈크림";
creamBread.describe(); // 이 붕어빵은 슈크림이 들어있고 따끈함 상태이다.

위 코드에서 creamBreadoriginalBread와 연결되어 있다. 자바의 클래스 상속처럼 부모의 속성을 통째로 복사해 가져오는 것이 아니라, 자신에게 없는 정보가 필요할 때마다 원형 객체에 접근하여 해당 기능을 위임(Delegation)받아 수행하는 방식이다.


2. 유전자 보관소: prototype 속성을 통한 메모리 최적화

자바스크립트에서 함수는 객체를 생성하는 생성자 역할을 할 수 있다. 이때 생성자 함수가 가진 prototype이라는 속성은 앞으로 태어날 자식들이 공유할 보물창고와 같은 역할을 한다. 모든 인스턴스가 각자 메서드를 가지는 대신, 이 창고에 메서드를 하나만 보관함으로써 메모리를 획기적으로 절약할 수 있다.

function Person(name) {
  this.name = name; // 이름은 각자 다르므로 인스턴스에 직접 저장한다.
}

// 모든 사람이 공유할 '인사하기' 기능은 보물 창고(prototype)에 딱 하나만 둔다.
Person.prototype.sayHello = function() {
  console.log(`안녕하세요, 저는 ${this.name}입니다.`);
};

const kim = new Person("김철수");
const lee = new Person("이영희");

kim.sayHello(); // 안녕하세요, 저는 김철수입니다.
lee.sayHello(); // 안녕하세요, 저는 이영희입니다.

// 두 객체는 서로 다른 실체지만, sayHello 메서드는 같은 곳을 바라보고 있다.
console.log(kim.sayHello === lee.sayHello); // true

만약 sayHellothis.sayHello로 생성자 내부에서 정의했다면, 인스턴스가 100만 개 생성될 때 메서드도 100만 개가 복사되어 메모리를 점유했을 것이다. 하지만 프로토타입을 이용하면 메서드는 단 하나만 존재하며, 모든 자식은 그곳으로 연결된 통로를 통해 기능을 공유한다.


3. 프로토타입 체이닝의 실체

객체의 속성에 접근할 때 발생하는 자동 탐색 과정을 프로토타입 체이닝이라 한다. 이는 __proto__라는 숨겨진 링크를 타고 조상을 거슬러 올라가는 여정이다. 이 과정에서 자식이 부모와 같은 이름의 속성을 가지게 되면 부모의 것이 가려지는 섀도잉(Shadowing) 현상이 발생하며, 이는 자바스크립트 다형성의 근간이 된다.

const grandParent = { fortune: 1000, talent: "음악" };

// 조부모를 원형으로 하는 부모 객체
const parent = Object.create(grandParent);
parent.fortune = 500; // 부모 단계에서 재산이 수정됨

// 부모를 원형으로 하는 자녀 객체
const child = Object.create(parent);
child.talent = "코딩"; // 자녀 단계에서 재능이 수정됨

// 1. 자신의 속성 탐색
console.log(child.talent); // "코딩" (조부모의 "음악"은 섀도잉되어 가려짐)

// 2. 부모 단계에서 탐색
console.log(child.fortune); // 500 (부모 객체에서 발견하여 멈춤)

// 3. 최상위 조상까지 탐색
console.log(child.hasOwnProperty('fortune')); // false (내 것은 아니지만 체인을 통해 사용 가능)

// 4. 인류의 시조(Object.prototype) 탐색
console.log(child.toString()); // [object Object] (정의한 적 없지만 최상위 조상에게 빌려옴)

자바스크립트 엔진은 속성을 찾을 때까지 체인을 타고 올라가며, 가장 먼저 발견된 값을 반환하고 탐색을 종료한다. 이러한 구조 덕분에 자바스크립트는 미리 모든 것을 결정해두지 않아도 실행 중에 유연하게 기능을 확장하고 수정할 수 있는 역동성을 얻게 된다.

객체지향 언어와 비교하여 설명해보자면 …

자바스크립트의 프로토타입 시스템은 단순히 객체를 생성하는 기술을 넘어, 자바(Java)와 같은 전통적인 클래스 기반 언어들과는 완전히 궤를 달리하는 철학적 선택이다. 많은 개발자가 Java의 상속 모델에 익숙해져 있다 보니 자바스크립트의 동작 방식을 오해하곤 한다. 이 두 언어의 결정적인 차이를 존재론, 메커니즘, 그리고 유연성이라는 세 가지 관점에서 비교하며 자바스크립트 프로토타입의 본질을 정리한다.

1. 존재론적 기원: 완벽한 설계도(Class)인가, 실존하는 원형(Prototype)인가?

Java와 자바스크립트의 가장 근본적인 차이는 "무엇이 먼저 존재하는가"에 있다. Java는 플라톤의 이데아론처럼 완벽한 설계도(Class)가 먼저 정의되어야만 객체를 탄생시킬 수 있는 '추상화 우선'의 언어다. 반면 자바스크립트는 설계도 없이 실존하는 객체로부터 기능을 확장하는 '실체 우선'의 언어다.

  • Java (Class-based): 클래스라는 엄격한 틀을 통해 인스턴스를 찍어낸다. 상속은 설계도와 설계도 간의 관계이며, 한 번 생성된 객체의 구조는 실행 중에 바꿀 수 없다. 붕어빵 틀이 있어야 붕어빵이 나오는 구조다.
  • JavaScript (Prototype-based): 설계도라는 환상을 버리고 '실존'에서 시작한다. 이미 존재하는 객체를 나의 원형(Prototype)으로 지목하여 관계를 맺는다. '원조 붕어빵'이 맛있으니 그 붕어빵을 기준으로 삼아 조금씩 바꿔나가는 식이다.
// [JS] 설계도 없이 객체 간의 연결만으로 기능을 확장하는 모습
const originalBread = {
  material: "팥",
  describe: function() { console.log(`${this.material} 붕어빵이다.`); }
};

// originalBread라는 '실체'를 원형으로 삼아 새로운 객체 생성
const creamBread = Object.create(originalBread);
creamBread.material = "슈크림";

creamBread.describe(); // "슈크림 붕어빵이다." (원형의 기능을 빌려씀)

2. 상속의 방식: 복제(Copy)인가, 위임(Delegation)인가?

Java의 상속이 부모의 유산을 자식의 계좌에 직접 넣어주는 '복합과 복제'의 개념이라면, 자바스크립트의 상속은 부모의 '신용카드'를 들고 다니는 것과 같다. 이는 메모리 관리와 공유 방식에서 커다란 차이를 만든다.

  • Java: 상속 시 부모 클래스의 속성과 메서드가 자식 인스턴스에 논리적으로 복제되어 포함된다. 각 인스턴스는 자신만의 메서드 정보를 (참조 형태로든 무엇으로든) 유지해야 한다.
  • JavaScript: 자식 객체는 부모의 기능을 직접 소유하지 않는다. 대신 부모 객체로 연결된 (__proto__)을 가질 뿐이다. 나에게 없는 기능이 호출되면 이 통로를 통해 부모에게 처리를 맡기는 위임(Delegation)이 일어난다.
function Person(name) {
  this.name = name; // 이름은 각자 가짐 (Own Property)
}

// 가문의 보물창고(prototype)에 메서드를 딱 하나만 보관한다.
Person.prototype.sayHello = function() {
  console.log(`안녕하세요, ${this.name}입니다.`);
};

const kim = new Person("김철수");
const lee = new Person("이영희");

// kim과 lee는 sayHello를 직접 들고 있지 않다.
// 호출 시에만 보물창고로 '위임'하여 실행한다.
console.log(kim.hasOwnProperty('sayHello')); // false
console.log(kim.sayHello === lee.sayHello);   // true (메모리상 단 하나만 존재)

이 구조 덕분에 자바스크립트는 수만 개의 객체를 만들어도 공통 메서드를 단 한 곳(prototype)에서 관리하여 메모리를 획기적으로 절약한다.


3. 탐색: 정적 위계(Hierarchy)인가, 동적 체이닝(Chaining)인가?

Java의 상속 구조는 컴파일 시점에 결정되는 정적인 위계다. 반면 자바스크립트는 실행 중에 부모를 바꾸거나 기능을 수정할 수 있는 동적인 체인 구조를 가진다. 이를 탐색의 여정인 프로토타입 체이닝이라 부른다.

  • Java: 메서드 오버라이딩은 설계 단계에서 결정된 위계에 따라 부모의 메서드를 '교체'하는 것이다.
  • JavaScript: 엔진이 __proto__ 링크를 타고 조상을 거슬러 올라가다가 가장 먼저 발견한 속성을 반환한다. 자식이 부모와 같은 이름의 속성을 가졌다면, 조상까지 갈 필요 없이 거기서 탐색을 멈춘다. 이를 섀도잉(Shadowing)이라 하며, 이는 '교체'가 아닌 '가려짐'의 원리다.
const grandParent = { fortune: 1000, talent: "음악" };
const parent = Object.create(grandParent);
parent.fortune = 500; // 부모 단계에서 재산 수정

const child = Object.create(parent);
child.talent = "코딩"; // 자식 단계에서 조상의 talent를 가리는 '섀도잉'

// 1. 자신의 단계에서 "코딩"을 발견하고 즉시 탐색 종료
console.log(child.talent); // "코딩"

// 2. 자신에게 없는 재산은 체인을 타고 부모에게서 찾음
console.log(child.fortune); // 500

// 3. 인류의 시조(Object.prototype)까지 올라가서 공통 기능을 빌려옴
console.log(child.toString()); // "[object Object]"

결국 Java가 "완벽한 계획 하에 건설되어 구조 변경이 어려운 아파트"라면, 자바스크립트는 "필요에 따라 방을 덧붙이고 옆집과 통로를 내며 확장해가는 유기적인 마을"과 같다.

그렇다면 자바스크립트는 객체지향 언어가 아닌가? 싶겠지만 자바스크립트는 객체지향 언어이기도 하다. 다만 클래스라는 분류보다 객체 그 자체가 우선된다. 어떤 객체가 다른 객체와 비슷하다면, 그 객체를 자신의 '원형(Prototype)'으로 삼아 기능을 빌려 쓰는 방식으로 상속을 구현한다. 설계도에 얽매이지 않고 객체와 객체가 직접 연결되는 유연한 구조다. 하지만 현대의 자바스크립트를 단순히 객체지향 언어라고만 부르기엔 그 스펙트럼이 매우 넓다. 그래서 전문가들은 자바스크립트를 '멀티 패러다임 언어'라고 정의하기도 한다.

  • 함수형 프로그래밍(Functional): 자바스크립트에서 함수는 '일급 객체'다. 함수를 변수에 담고, 인자로 넘기고, 결과값으로 반환할 수 있다. 이는 객체지향과는 또 다른 강력한 패러다임이다.
  • 명령형 및 프로시저(Procedural): 순차적인 로직 흐름을 중시하는 고전적인 프로그래밍 방식도 완벽히 수용한다.

또한 자바스크립트의 성격을 규정하는 또 다른 중요한 키워드는 '동적(Dynamic)'이다.자바는 컴파일 시점에 모든 타입과 구조가 결정되는 '정적(Static)'인 성격이 강하다. 반면 자바스크립트는 실행 중(Runtime)에 객체에 새로운 팔다리를 붙이거나, 심지어 부모 객체를 통째로 바꿔버릴 수도 있다.

그렇다면,, JS는 뭘까?

자바스크립트는 프로토타입을 기반으로 하며, 함수형 프로그래밍의 특징을 결합한 유연한 '멀티 패러다임 객체지향 언어'이다.

0개의 댓글