hmk run dev

자바스크립트가 프로토타입을 선택한 이유 본문

javascript

자바스크립트가 프로토타입을 선택한 이유

hmk run dev 2024. 9. 21. 11:45

자바스크립트를 처음 접했을 때, 사실 아직 까지도 가깝지만 먼 이웃 같은 존재인 프로토타입

 

여러 글들에선 "프로토타입은 자바스크립트에서 상속을 지원하기 위한 방법"라고 이야기하곤 한다.

 

여기엔 꼬리에 꼬리를 무는 질문이 이어질 수 있는데, "왜 다른 언어처럼 클래스가 아니라 프로토타입을 사용하는가?"

"프로토타입과 클래스의 차이는 뭘까?"

 

그 외에도 자바스크립트엔 면접 단골 질문으로도 유명한 this, 호이스팅, 클로저, 스코프체인 등등.. 이 있다.

이러한 자바스크립트의 특성들을 단순하게 암기만 하고 있었고, 대부분의 관련 자료들은 해당 현상에 대해서

설명하는 내용이 주를 이뤘고 단순히 외워야 하는 고통스러운 시간이었다.

 

"호이스팅은 자바스크립트에서 코드 실행 시 전체 코드를 선언부 상단으로 올려 실행가능한 상태인지 확인한다"

"this는 기본적으로 global을 가리키고 실행 콘텍스트에 따라 달라질 수 있으며 call, bind, apply 등을 통해 제어할 수 있다."

 

 

FE 개발자로서 위의 질문과 답변은 정형화되어 있다고 생각했다.

 

사실 필자도, 이직을 준비할 때 위와 처럼 정형화된 답변을 다시 상기시키고 외웠던 사람 중에 하나이다.

그러나 피해왔던 근본적인 "왜?"에 대한 질문을 타개하고자 자바스크립트의 프토로토타입 선택이유에 대해 정리하고자 합니다.

 

Classses vs Prototypes Some Pliosophical and Historical Observations

위 논문을 토대로 이야기하자면 prototype과 class의 OOP는 완전히 상반되는 방식이라고 볼 수 있다.

 

객체를 바라보는 개념 자체가 완전히 다르다.

특히 문맥(context)을 매우 강조하는 철학적 근거에서 태어난 녀석이라 렉시컬 스코프에 의한 호이스팅과 실행 문맨에 의해 결정되는 this가 필연적으로 발생할 수밖에 없다.

 

자바스크립트의 프로토타입 선택 이유에 대해 다뤄보면서 실행 context, this, 호이스팅, 클로저 등이 발생하는 

이유에 대해 정형화된 암기를 벗어나 근본적인 이유에 대해서 이해할 수 있기를 바랍니다.

 


플라톤과 이데아, 그리고 클래스 기반 객체지향프로그래밍

"서양 철학은 플라톤의 각주에 불과하다 - 화이트 헤드"

 

프로토타입을 이해하려면 그 대척점에 있는 클래스의 기원을 알아햐 한다.

클래스 기반 객체지향 언어(Java, C#)를 다뤄봤다면 이미 알고 있는 내용일 수도 있습니다.

 

서양철학은 이분법적 세계관을 갖고 있는데요,

 

- 영혼 / 육체

- 추상적 / 구체적

- 이데아 / 프랙터스

 

눈앞에 실제로, 구체적으로 존재하는 사물이 있다면 반드시 그것의 본질이 존재한다는 것이 플라톤의 주장입니다.

 

의자를 예시로 보면 우리가 앉아있는 의자는 여러 가지 형태가 존재합니다.

사무용 의자, 공원 벤치, 원목 의자 등등 이러한 수많은 의자가 실제로 존재한다면 반드시 

그 본질적이고 추상적인 "의자"라는 것이 존재한다는 것이지요

이러한 본질 세계를 이데아(Idea)라고 합니다. 현실의 의자는 모두 이데아의 "의자"를 모방한 의자라는 것입니다.

 

영어권의 사고방식에는 이러한 이분법적 세계관이 기본입니다.

영어의 "관사"를 보면 이러한 방식이 두드러지는데요,

 

영어에서 이데아의 세계에 있는 의자를 이야기할 때 그냥 "chair"라고 얘기하는 것이 그 방법입니다.

그럼 현실에 존재하는 의자를 가리킬 땐 어떻게 이야기할까요?

현실에 존재하는 것을 얘기할 땐 단어 그대로 이야기하지 않고 "때"를 묻혀서 이야기합니다.

그래서 "the chair"라고 얘기해야 합니다.

 

- chair : 이데아에 존재하는 본질적인, 추상적인 의자. 현실세계에 존재하지 않음

- this chair, a chair : 현실 세계에 존재하는 의자

 

 

이러한 사고방식이 프로그래밍 언어에도 자연스럽게 녹아들어 생긴 언어가 있습니다.

바로 "클래스 기반 객체지향 프로그래밍 언어"입니다.

 

 

Java를 예시로 볼까요?

class Chair {  // class 선언 자체는 이데아에 존재하는 의자
... 
}

Chair myChair = new Chair(); // new 키워드로 추상적으로만 존재하던 의자를 메모리라는 현실세계로

 

위 코드에서 레퍼런스 타입이라 불리는 Chair 클래스는 이데아에 존재하는 추상적인 개념

즉, 코드상으로만 존재하지 실제 메모리상에는 존재하지 않습니다. (사실, PermGen 영역에 있겠지만 Heap 메모리를 실제 하는 공간으로 보겠습니다.)

 

그럼 현실세계에 존재하게 하려면 어떻게 해야 할까요?

바로 new 키워드 사용하면 됩니다. new Chair();를 하는 순간 추상적으로만 존재하던 의자가 메모리라는 현실시계에

구체적으로 존재(인스턴스화)하게 됩니다.


분류(Classification)

이러한 플라톤의 이데아 이론은 그의 제자 아리스토텔레스에 의해서 "분류(Classification)"란 개념으로 정리됩니다.

class라는 키워드는 "학급"을 의미하는 게 아니라 Classification에서 나온 키워드입니다.

 

 

- 개체의 속성이 동일한 경우 개체 그룹은 같은 범주에 속한다. 범주란 정의와 구별의 합니다.

 

이는 전통적인 클래스 기반의 객체 지향 프로그래밍의 아이디어-일반화와 정확하게 일치합니다.

여기서 속성은 프로퍼티가 될 수 있겠죠, 프로퍼티가 유사한 객체가 있다면 일반화 과정을 통해 추상 클래스로 추상화됩니다.

 

실제로 아리스토텔레스는 이러한 기준으로 현실 세계의 많은 것들은 분류했지요,

최초로 동물을 분류했고, 속성에 따라 분류는 어류가 아닌 포유류가 되었죠

 

 

돌고래의 속성에 따른 분류

// 포유류(Mammal) 추상 클래스: 모든 포유류는 허파로 숨을 쉬고 새끼를 낳음
abstract class Mammal {
    abstract void breathe();    // 숨쉬기 메서드 (허파로 호흡)
    abstract void giveBirth();  // 새끼를 낳는 메서드
}

// 돌고래(Dolphin) 클래스: 포유류의 특징을 상속받아 구현
class Dolphin extends Mammal {

    @Override
    void breathe() {
        System.out.println("돌고래는 아가미가 아닌 허파로 숨을 쉽니다.");
    }

    @Override
    void giveBirth() {
        System.out.println("돌고래는 새끼를 낳고 젖을 먹입니다.");
    }
}

// 어류(Fish) 추상 클래스: 어류는 아가미로 호흡하고 알을 낳음
abstract class Fish {
    abstract void breathe();
    abstract void layEggs();
}

// 상어(Shark) 클래스: 어류의 특징을 상속받아 구현
class Shark extends Fish {

    @Override
    void breathe() {
        System.out.println("상어는 아가미로 숨을 쉽니다.");
    }

    @Override
    void layEggs() {
        System.out.println("상어는 알을 낳습니다.");
    }
}

 


프로토타입(Prototype)

"서양 철학은 플라톤의 각주에 불과하다 - 화이트 헤드" 하지만 이 말에는 "비트겐슈타인 이전까지"라는 단서를 붙여야 한다.

- 와스피 히잡

 

 

이제야 프로토타입 이야기가 나오는데요, 프로토타입을 이야기하기 위해서 분류(Classification)에 대해 반드시 알아야 할 필요가 있기

때문에 짚고 넘어갔습니다.

 

프로토 타입은 분류의 개념을 정면으로 반박하여 나온 이론이기 때문입니다.

 

19세기 철학자 비트겐슈타인은 아리스토텔레스의 분류 개념을 정면으로 반박합니다.

 

"공유 속성의 관점에서 정의하기 어려운 개념이 있다(사실) 올바른 분류란 없다 - 비트겐슈타인"

이것을 뒷받침하기 위해 들고 온 것은 바로 게임입니다.

게임은 일반적으로 "승리"와 "패배"가 명확합니다. 

하지만 비트겐슈타인은 이에 대한 반론으로 "승리"와 "패배"가 없는 ring around a rosy라는 게임을 가져옵니다.

 

그냥 빙글빙글 돌다 다 같이 주저 않는 게임으로 누구도 승리, 패배하지 않죠(재미없을 것 같긴 하네요)

 

여하튼 이 게임엔 공유 속성(property)이 없네요

 

- 승리 / 패배? 없음

- 숙련도 여부? 행운(주사위) 위주 게임은 없잖아

- 플레이어 존재여부? 플레이어가 전혀 필요하지 않은 게임도 있잖아

 

특히 예술작품의 경우 공통 속성을 정의하기 더 어렵습니다.

즉, 좀 더 철학적으로 본다면 "게임", "예술" 등의 단어는 결코 속성으로 규정할 수 없습니다.

 


"세계에 미리 내재되어서 대상과 언어를 완전히 규정하는 어떤 언어란 존재하지 않는다. - 비트겐슈타인"

 

 

Java와 같은 객체 지향 프로그래밍 언어로 개발하다 보면 한계에 부딪힐 때가 있는데요,

최적의 클래스를 설계를 만드는 것이 어려울 때가 있습니다.

속성(property)으로 분류하는 것은 확정성을 고려하면 좋은 방식이 아니었습니다.

정답이라고 생각했던 설계도 개발이 진행되면서 뒤엎는 경우가 많았습니다.

 

멋들어진 상속관계는 기술부채가 되어버리는 경우도 많았죠,

물론 도메인 기반 설계, SOLID, 디자인 패턴을 더 습득하며 그럴싸한 클래스를 설계할 수 있도록 갈고닦아야겠지요

한 번에 완벽한 디자인이 나오는 것은 너무나 어렵습니다.

 

이러한 분류(Classification)에 대해 강하게 비판한 비트겐슈타인, 그렇다면 그의 대안은 무엇이었을까요?

 

 

"표현은 삶의 흐름 속에서만 의미를 갖는다 - 비트겐슈타인"

 


의미사용이론(the use theory of meaing)

비트겐슈타인 일생의 후기에 내놓은 이론입니다.

사용(use)에 의해 의미(meaning)가 결정된다는 이론입니다.

단어의 쓰임새가 곧 의미가 됩니다. 즉, 단어의 진정한 본래의 의미란 존재하지 않고 상황과 맥락에 의해서 결정된다라고

주장하고 있습니다. 그러니 단어의 의미를 백날 분석해 봤자 소용이 없다는 것입니다.

 

비트겐슈타인은 "벽돌"을 예로 들었습니다. 누군가 벽돌!이라 외쳤을 때 상황마다 그 의미는 달라집니다.

 

- 벽돌이 필요할 때 : 벽돌을 달라

- 벽돌로 보수해야 할 때 : 벽돌을 채우라

- 벽돌이 떨어질 때: 벽돌을 피해라

 

위의 내용이 와닿지 않는다면 맥락(context)이 중요하다는 것만 기억하시면 됩니다.

콘텍스트로 프로토타입 기반 언어의 실행 콘텍스트를 설명할 수 있습니다.

 


가족 유사성(Family Resemblance)

비트겐슈타인의 위에서 설명한 의미사용론과 더불어 가족 유사성이라는 또 하나의 이론을 주장합니다.

 

출처 :  https://www.slideshare.net/vcmlab/cog5-lecppt-chapter08

 

인간이 현실에서 실제로 대상을 분류할 때 속성이 아닌 가족 유사성을 통해 분류하게 된다고 얘기합니다.

 

위 그림처럼 한 가족이 있을 때 이 가족 모두 공유하는 공통속성은 없습니다.

갈색 머리, 안경, 수염, 큰 코가 가족의 전형적인 특징이라고 하더라도 모든 가족 구성원에게 적용되는

공통된 특성(속성)은 없을 수 있습니다.

그런데도 우리는 이 그림을 보고 전형적인 특징을 통해 "가족"으로 분류합니다.

이러한 분류 방식을 "가족 유사성"에 의한 분류라고 합니다.

 

이 이론은 프로토타입 이론의 근거가 됩니다.

 


Rosch의 프로토타입 이론

이론의 마지막 장입니다. 드디어 "프로토타입"이라는 단어를 직접 다루는 이론입니다.

 

비트겐슈타인의 의미사용이론, 가족유사성은 1970년경 철학자 Eleanor Rosch에 의해

프로토타입 이론(Prototype theory)으로 정리됩니다.

 

- 실험 참가자들에게 여러 범주 구성원(사과, 코코넛, 오렌지)의 속성을 적어보라고 함

- 각 범주 구성원에 대해 범주의 다른 구성원과 공유하는 속성의 개수를 도출

- 사과, 오렌지 : 2점(둥글다, 즙이 있다.)

- 코코넛: 1점(둥글다)

 

점수가 높을수록 "가족 유사성"이 높다고 볼 수 있습니다.

전통적인 분류에선 모두 과일로 볼 수 있지만, 프로토타입 이론에서는 사과 오렌지가 가장 전형적인 무언가라고 볼 수 있습니다.

반면에 코코넛은 저 중에서 가장 비전형적인 것으로 볼 수 있겠네요

 

이 실험을 통해 로쉬는 "인간은 등급이 매겨진 구조를 가진다"라고 주장합니다.

인간이 사물을 분류할 때 자연스럽게 가장 유사성이 높은 것들을 순서대로 등급을 매긴다는 의미로 볼 수 있습니다.

이렇게 분류를 하다 보면 가장 높은 등급을 가진 녀석이 나올 텐데요, 이것이 바로 Prototype이다.

라는 주장이 프로토타입 이론입니다.

 

"새"를 예로 들어보면

 

"참새"는 새의 범주는 대표할 만한 가장 전형적인 새입니다.

이것을 "원형(prototype)"으로 간주해 봅시다.

 

출처 :  https://laurabecker.gitlab.io/classes/as/08-semantics.pdf

 

같은 새지만 "타조"는 분류에선 원형과 가장 멀리 떨어진 "비전형적인" 새가 됩니다.

 

즉, 객체는 "정의"로부터 분류되는 것이 아니라 가장 보기 좋은(prototype exemplar)로부터 범주화됩니다.

 

이러한 분류 체계는 매우 경제적입니다.

만약 우리가 새로운 대상을 접해서 분류해야 한다면, 우리는 새로운 대상의 몇 가지 특징만 원형(prototype)과

비교해서 확인만 하면 됩니다.

 

이 이론에 또 한 가지 중요한 것이 있는데요

 

바로 같은 단어라 할지라도 누가 어떤 상황(context)에서 접했느냐에 따라 의미가 달라질 수 있습니다 - 의미사용론

 

예를 들면 아이가 생각하는 새의 범주엔 "참새"는 속하지만 "펭귄"은 해당범주에 속할 수 있습니다.

아이가 생각할 때 펭귄은 매우 비전형적이기 때문이죠

하지만 조류학자가 생각할 때 "참새"와 "펭귄"은 명확하게 유사한 새의 범주에 속할 수 있습니다.

 

즉, 같은 단어라도 어떤 상황에서 접했느냐에 따라 범주는 크게 달라집니다.

 

- 현실에서 존재는 것 중 가장 좋은 본보기를 원형(prototype)으로 선택

- 문맥(context)에 따라 의미가 달라진다.


프로토타입 기반 객체지향 프로그래밍

이러한 프로토타입 이론은 그대로 프로토타입 기반 객체지향 프로그래밍 언어를 통해 구현되었습니다.

 

프로토타입 기반 OOP 언어의 특징은 다음과 같습니다.

 

- 개별 객체(instance) 수준에서 메서드와 변수를 추가

- 객체 생성은 일반적으로 복사를 통해 이루어짐

- 확장(extends)은 클래스가 아니라 위임(delegation)을 통해 이뤄짐

- 개별 객체 수준에서 객체를 수정하고 발전시키는 능력은 선험적 분류의 필요성을 줄이고 반복적인 프로그래밍 및 디자인스타일 추구

- 분류하지 않고 유사성을 활용하도록 선택

- 설계는 맥락(context)에 의해 평가

 

javascript 언어를 사용하는 사람의 관점으로 중요하다 생각되는 부분을 되짚어보면

 

- 프로토타입 언어에서는 "분류"를 우선하지 않는다. 생성된 객체 위주로 유사성을 정의한다.

- 쓰임새는 contenxt에 의해 평가된다

   > 실행 콘텍스트, 스코프 체인이 파생됨

   > 클로저, this, 호이스팅 등등 이 모든 것들이 프로토타입의 "맥락"을 표현하기 위함

 


자바스크립트의 프로토타입

function 참새(){
    this.날개갯수 = 2;
    this.날수있나 = true;
}
const 참새1 = new 참새();

console.log("참새의 날개 갯수 : ", 참새1.날개갯수); // 2

function 닭(){
  this.벼슬 = true;
}
닭.prototype = 참새1; // reference(오른쪽이 인스턴스인 점 주목)
const 닭1 = new 닭();
console.log("닭1 날개 : ", 닭1.날개갯수, ", 날수있나? ", 닭1.날수있나); // 2, true
닭1.날수있나 = false;
console.log("다시 물어본다. 닭1은 날 수 있나? :", 닭1.날수있나); // false
// 아래는 고전적인 방식의 프로토타입 연결
function 펭귄(){
    참새.call(this);    // copy properties
}
펭귄.prototype = Object.create(참새.prototype); // 프로토타입 연결
const 펭귄1 = new 펭귄();
console.log("펭귄1 날개 : ", 펭귄1.날개갯수, ", 날수있나? ", 펭귄1.날수있나); // 2, true
펭귄1.날수있나 = false;
console.log("다시 물어본다. 펭귄1은 날 수 있나? :", 펭귄1.날수있나); // false

 

  • 5L : 날개가 2개, 날 수 있는 참새 1 이 있습니다.
  • 13L : 참새 1을 프로토타입으로 갖는 닭 1 이 생겼습니다. 여기서 주목할 점은 오른쪽이 참새(함수)가 아니라 참새 1(인스턴스)인 점입니다. 프로토타입 이론은 이미 존재하는 사물을 통해 범주화한다는 점에서 일치합니다
  • 14L : 닭의 정의(10L)에는 날개개수가 없지만 2가 출력됩니다. 프로토타입 체인에 의해 참새 1의 속성에 접근했기 때문입니다.
  • 15L : 닭 1 은 날 수 없다고 합니다. 닭 1은 날 수 없어도 프로토타입에 해당하는 참새 1 은 날 수 있습니다. (닭 1은 참새 1 프로토타입에서 좀 더 멀어졌습니다) 같은 속성을 변경해도 프로토타입 객체의 속성은 변경되지 않은 점에 주의
  • 17L~25L : 고전적인 방식으로 프로토타입을 사용해 봤습니다. 프로토타입에선 객체 생성을 통해 확장한다는 부분이 좀 더 직관적으로 다가옵니다.

위 코드는 아래처럼 도식화할 수 있습니다.

 

 

  • 닭 1의 원형(프로토타입)은 참새 1이다
  • 닭 1에 없는 속성(날개개수)은 프로토타입 체인을 통해 참조된다
  • 닭 1에 동일한 속성명(날 수 있나)을 추가해도 원형은 변하지 않는다(위임)
    > 원리적으로는 닭 1을 통해 원형(prototype)을 변경하는 건 불가능해야 한다. 하지만 JS 에선 문법적으로 가능. (권장하지 않음)

 


자바스크립트의 어휘적 범위(lexical scope)

의미사용론에 따르면 단어의 의미는 그 어휘적인, 근처 환경에서의 의미가 됩니다.

이는 javascritp에서 다음처럼 적용됩니다.

 

변수의 의미는 그 어휘적인(Lexical), 실행 문맥(Excution Context)에서의 의미가 된다.

 

그렇기 때문에 실행문맥(context)의 모든 선언을 참고(호이스팅)해 의미를 정의합니다.

 

호이스팅은 자바스크립트의 대표적인 특징으로 면접단골 질문이기도 하죠

대부분 "코드가 로드될 때 선언부가 상단으로 끌어올려지는" 정도로 대답을 합니다.

 

틀린 대답은 아니지만 아쉽습니다. 좀 더 보태면  아래처럼 대답할 수 있겠네요

 

‘실행 콘텍스트 생성 시 렉시컬 스코프 내의 선언이 끌어올려지는 게 호이스팅이다'

 

"프로토타입 기반 언어인 자바스크립트에서는 ‘단어의 의미가 사용되는 근처 환경’에서의 ‘근처'를 어휘적인 범위(Lexical Scope)로 정의했습니다. 자바스크립트 엔진은 코드가 로드될 때 실행 콘텍스트를 생성하고 그 안에 선언된 변수, 함수를 실행 콘텍스트 최상단으로 호이스팅 합니다. 이러한 범위를 렉시컬 스코프라 합니다."

 

// 전역 실행문맥 생성. 전체 정의(name, init) 호이스팅
var name = 'Kai';
init(); // init 실행문맥 생성. 내부 정의(name, displayName) 호이스팅
function init() {
    var name = "Steve";
    function displayName() {  
      console.log(name); // 현재 실행문맥 내에 정의된게 없으니 outer 로 chain
      // var name = 'troll?'; // 주석 해제되면 호이스팅
    }
    displayName(); // displayName 실행문맥 생성. 내부 정의 호이스팅.
}

 

 

- 2L: 코드가 로드될 때 전역 실행문맥(Execution Scope)이 생성됩니다. 전역의 선언부를 모두 호이스팅 하게 되는데 여기선 2L의 name과 4L의 init이 렉시컬 스코프에 들어갑니다.

 

- 3L: 렉시컬 스코프 상에 4L에 init 함수가 존재하니 에러 없이 실행할 수 있습니다. 코드 로딩 시점에 init 함수를 타고 들어가 실행 문맥을 생성

 

- 4L: init 함수에 대한 렉시컬 스코프를 생성 name과 displayName이 들어옵니다.

 

- 6L-9L: displayName 실행 문맥 내에 name이라 선언된 것이 없음. 이렇 때 Scope Change을 통해 상위 콘텍스트로 위임

코드를 로드하게 되면 아래와 같은 구조가 생성됩니다.

- Global Execution // 1
   - Lexical : name, init 
- Execution : init // 2
   - Lexical : name, displayName
   - Outer : global 
- Execution : displayName // 3
   - Lexical : null
   - Outer : init

 

 

중요한 것은 자바스크립트의 동작방식보다, 프로토타입 언어인 자바스크립트에 

도대체 왜 "실행 문맥", "렉시컬 스코프", "호이스팅"이 존재하는가입니다.

"왜?"를 이해한다면 이러한 것들은 더 이상 암기과목이 아닙니다.

 

앞서 말했듯이 프로토타입 철학의 근원인 비트겐슈타인류에서 가장 중요하게 생각하는 것이 있다면

"어휘"이고 이것은 "문맥" 내에서만 의미를 가진다는 것이 핵심입니다.

 

이 핵심을 자바스크립트에서 구현하기 위해 자연스럽게 발생할 특징임을 이해한다면 더 이상 외울 필요가 없게 됩니다.

 


자바스크립트의 this

자바스크립트의 또 다른 특징은 this이죠,

클래스 기반객체지향 언어에서의 this와는 완전히 다른 동작을 합니다.

 

대부분의 문서에선 Case by case로 this가 가리키는 객체를 설명합니다.

이런 식의 접근보단 프로토타입 관점에서 이해하고 왜 javascript에서 this가 요 모양인지 이해한다면

매력적인 요소로 다시 느끼게 될 수 있을 겁니다.

 

this에 대한 대표적인 오해들

 

- this는 기본적으로 window다

- 이벤트 리스너에 등록한 콜백의 this는 내부 bind 등을 통해 바뀌기 때문에 무엇인지 알 수 없다

- this는 외워야 한다.

 

이러한 오해들을 바로잡기 위해선 먼저 프로토타입 철학에서 이런 상황을 어떻게 해석하는지에 대해 알 필요가 있습니다.

 

비트겐슈타인은 쓰임새가 곧 의미라는 점을 강조했습니다.(의미사용론)

"대상"에 따라서 같은 단어도 의미가 달라진다는 얘기입니다.

 

이것이 바로 프로토타입과 클래스의 대표적인 차이라고 볼 수 있습니다.

단어를 바라보는 방식이며 중요한 세계관의 차이입니다.

 

미리 분류하고 정의한 클래스를 중요하게 여기는 방식 vs 주체와 문맥을 중요하게 여기는 방식

프로그래밍으로 보자면 실행(invoke)하는 "객체"가 중요하다는 의미입니다.

 

이것이 프로토타입 기반의 언어인 자바스크립트에서 this가 클래스 기반 언어들과 다르게 동작하는 이유입니다.

 

javascript에선 this가 정의된 함수가 어떻게 발화(invoke) 되었는지에 따라 가리키는 값이 달라집니다.

정확히는 받아들이는 대상의 컨텐스트를 가리킵니다.

 

이를 이해하려면 먼저 메서드와 메시지를 명확하게 알아야 합니다.

  • 메소드 : 객체의 함수
  • 메시지 : 메서드를 실행하라는 메시지 전달

 

자바에선 클래스의 메서드를 호출하는 것을 메시지라고 합니다.

자바스크립트를 예로보면 foo라는 객체가 있고 그 내부에 bar()이라는 함수가 있을 때 다음처럼

invoke 할 객체를 지정할 수 있습니다.

 

  • foo.bar()
  • bar.call(foo)
  • var boundBar = bar.bind(foo)

위처럼 foo 객체를 통해 invoke 된 함수는 내부 this가 무조건 foo를 가리키게 됩니다.

만약 아무것도 지정된 게 없으면 global(브라우저는 window)을 가리키겠죠?

 

var someValue = 'hello';
function outerFunc() {
    console.log(this.someValue); // 첫번째 : ?, 두번째 : ?
    this.innerFunc();
}
const obj = {
    someValue : 'world',
    outerFunc,
    innerFunc : function() {
        console.log("innerFunc's this : ", this); // 첫번째 : ?, 두번째 : ?
    }
}
obj.outerFunc(); // 첫번째
outerFunc(); // 두번째

 

  • 3L : 13L 에서 호출한 첫 번째는 world가, 14L 에서 호출한 두 번째는 hello 가 찍힙니다. outerFunc 가 누구를 통해 발화되었는지를 알면 this 가 무엇이 될지 알 수 있습니다
  • 4L : obj를 통해 발화되면 innerFunc 가 존재하기 때문에 호출되지만, 글로벌에서 발화되면 innerFunc 가 없기 때문에 에러가 납니다
  • 10L : this 가 이중으로 들어가 있어 헷갈릴 수 있는데 복잡하지 않습니다. this(obj)를 통해 발화했기 때문에 첫 번째는 obj 가 됩니다.

 

13L의 obj.outerFunc 호출 상황을 그림으로 표현하면 아래와 같습니다.

 

  • 시작 : 자바스크립트 엔진이 코드를 실행합니다. (브라우저에서 use strict 모드가 아닌 경우 this는 window를 가리킵니다)
  • 1번 : 코드의 13L을 만나면 엔진은 obj에 outerFunc를 실행하라는 메시지를 보냅니다.
  • 2번 : obj에서 outerFunc를 발화합니다. 코드 로드 시 만들어져 있는 실행문맥을 참고해 실행합니다. 이때 실행 문맥상의 this는 발화한 obj를 가리킵니다.
  • 3번 : 실행 중에 this.innerFunc를 만납니다. 엔진은 this 가 가리키는 obj에 innerFunc 를 실행하라는 메시지를 보냅니다.
  • 4번 : obj 에 innerFunc 이 선언되어 있으니 잘 실행합니다

 

14L의 outerFunc 호출 상황을 그림으로 그려보면 아래와 같습니다.

  • 1번 : 코드의 14L을 만나면 엔진은 자신(global)의 실행문맥상에 존재하는 outerFunc을 호출합니다.
  • 2번 : 발화한 지점이 엔진(global)이니 this는 엔진을 가리킵니다. 엔진에 innerFunc을 실행하라는 메시지를 보냅니다
  • 3번 : 글로벌 실행문맥에는 innerFunc 이 없기 때문에 에러가 납니다

 

흔히 사용하는 이벤트핸들러를 기준으로 예시를 하나 더 들어 봅시다.

 

function handle() {   
    console.log(this);  // 첫번째 ?, 두번째 ? 
}  
document
    .getElementsByTagName('body')[0]
    .addEventListener('click', handle);  // 첫번째. 호출되었다고 가정.
handle();  // 두번째. 첫번째 이후에 호출되었다고 가정.

 

다시 한번 강조하지만 this를 발화 지점 기준으로 생각하면 헷갈리지 않습니다.

 

 

addEventListener 함수는 해당 엘리먼트에 콜백을 등록하는 함수입니다.
간단하게 얘기하면 ‘div’ 객체에 ‘handle’ 메서드를 등록했다고 볼 수 있죠.

 

  • 1번 : 브라우저에서 div를 클릭하면 엔진이 반응합니다
  • 2번 : 해당 엘리먼트(div)에 등록된 event listener 들을 실행하라는 메시지를 보냅니다
  • 3번 : div 엘리먼트에서 handle을 발화합니다.
  • 4번 : 이때 handle의 실행문맥의 this는 발화한 div 를 가리킵니다.

 

보너스로 넣은 문제도 있습니다.

var value = 1;

var obj = {
  value: 100,
  foo: function() {
    setTimeout(function() {
      console.log("callback's this: ",  this);  // ?
      console.log("callback's this.value: ",  this.value);  // ?
      function bar() {
        console.log("bar's this: ", this);  // ?
      }
      bar();
    }, 1000);
  }
};

obj.foo();

 

this 는 무엇이 될까요?

7L : 브라우저 라면 window를 가리킵니다. 하지만 node.js 라면? 재밌게도 Timer 란 객체를 가리킵니다. setTimeout에 대한 구현이 다른 거죠. node.js 에서는 setTimeout 시 Timer 객체에 등록하고 해당 tick 이 되면 Timer에 실행하라는 메시지를 보내나 봅니다. 중요한 점은, 같은 코드여도 돌아가는 엔진마다 다를 수 있다는 의미인 것 같습니다.

8L : 브라우저라면 1, node.js 라면 undefined 가 나옵니다. 기본 this 는 global 라고 외우면 안되는 이유입니다

9L : 당연하게도 브라우져라면 window, node.js 면 global을 가리킵니다. 엔진이 어떤 방식으로 실행했는지를 기억하시면 됩니다.

 


마무리

프로토타입은 "클래스"의 다른 구현이 아닌 완전히 새로운 철학하에 만들어진 이론입니다.

이러한 차이점을 이해한다면 더 이상 호이스팅, this, 렉시컬 스코프 등은 암기 과목이 아니게 됩니다.

 

자바스크립트는 class, arrow function, let, const 등 여타 일반적인 언어와 보편성을 맞추고 있고,

이것들을 정말 편하게 사용 중이기도 합니다.

 

하지만 언어의 근본(프로토타입)은 변하지 않습니다.

이것들은 언어적 지원이 아닌 syntactic sugar인 부분도 언어의 근본적 구조가 다르기 때문을 이해할 수 있게 되면 좋겠습니다.


 

출처

https://medium.com/@limsungmook/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%8A%94-%EC%99%9C-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85%EC%9D%84-%EC%84%A0%ED%83%9D%ED%96%88%EC%9D%84%EA%B9%8C-997f985adb42

 

'javascript' 카테고리의 다른 글

자바스크립트와 V8  (0) 2024.04.11
자바스크립트의 call by value와 call by reference  (0) 2024.03.13
자바스크립트 가비지 콜렉션  (0) 2024.03.10
CommonJS와 ESM(esModule)  (0) 2024.02.24
자바스크립트의 stack & heap  (0) 2024.01.18
Comments