Javascript Object

1. 들어가며

원래 이 블로그에서는 한국어 포스팅은 안 하려고 했는데 다른 것 보다는 이 블로그는 누가 읽어도 안 읽어도 뭐 크게 상관없다… 라는 스탠스가 기본이었기 때문이다. 하지만 왠지 이런 기술관련 글은 혹시 도움이 될 사람이 있을지도 모른다는 생각에 간만에 한국어 포스팅을 해 보기로 했다. 혹시 시간이 되면 영어나 일본어로 다시 써보게 될지도 모르겠지만.

최근 간만에 프로그래밍 관련 공부를 다시 하고 있는데 이전에 봤던 “You Don’t Know JS” 시리즈를 다시 읽고서 매우 감동을 받고 있다. Javascript를 보통 공부할 때 어느정도 익숙해지고 나면 읽게되는 유명한 책이 Douglas Crockford의 “JavaScript: The Good Parts” 이다. 이 책은 Javascript에 대한 이해를 높여주면서 이를 사용할 때 겪게되는 오류들을 미리 피할 수 있는 바람직한 코딩 가이드를 제공해 준다. 시간이 없는 사람들은 Crokford의 강연 비디오도 다양하게 올라와 있어 꼭 한번 보는 것이 좋다.

하지만 “You Don’t Know JS” 시리즈는 Javascript의 good parts만이 아니라 bad parts에 대해서도 충분한 이해가 필요하다는 생각에서 출발하고 있다. Javascript는 개인적으로 겪어본 프로그래밍 언어중에서 가장 특이하고 ‘현학적’인 언어라고 생각한다. 여전히 Javascript를 매우 ‘쉬운’ 언어라고 생각하는 사람들도 많은데 (사실 쉽게만 쓸 수 있기도 하다.) 이는 태생부터 이상하게 나온 Java + Script 라는 이름이 문제일지도 모르겠다. 혹시 해서 말하지만 Javascript는 Java와는 전혀 관련이 없으며 Script도 일반적으로 인지되는 ‘쉽다’와는 거리가 먼 언어이다. 물론 이는 그만큼 언어가 널리 쓰이게 되면서 보통은 신경쓰지 않아도 될 언어의 내부까지의 이해가 중요해졌기 때문이기도 하다.

보통 프로그래밍 언어를 공부하게 되면 1) 언어의 문법의 이해, 2) 특정 상황에 대한 해결방법 3) 언어 내부 구조의 이해 의 3단계를 거치게 된다고 생각한다. 사실 보통은 1), 2)만으로도 충분한 경우도 많다. 하지만 해당 언어를 정말로 전문가로서 활용하고 싶다면 3)의 과정은 매우 중요한데, Javascript는 이 3)이 매우 어려운편이고 (특히 C/C++ 계열의 언어에 익숙할수록 그러하다. 언어 자체는 마치 한국어와 영어처럼 그 근본이 다른 언어인데 문법적으로는 마치 비슷한 것 같은 모양을 띄고 있어서 더 그럴 수도 있겠다.) 더 잘 알수록 더 다른 사람이 이해하기 힘든 (현학적인) 코드를 만들어 낼 수 있다. 개인적으로는 이런 현학적인 코드를 가능한 만들지 않아야 한다고 생각하지만, Javascript에는 오랜 세월동안 만들어져온 일종의 패턴과 같은 많은 현학적 코드들이 존재한다. 그리고 그 코드들은 단지 자신의 지적 능력의 과시가 아닌, 꽤 현실적인 고민을 해결하기 위해 나온 경우가 많다. 그렇기 때문에 이를 이해하는 것 역시 매우 중요한 일이 된다.

그런 이유로 개인적으로 Javascript에 대한 글을 써보고 싶어졌다. 기본적으로는 내 자신의 공부를 위해서이다. (언제나 공부를 하는데 가장 좋은 방법 중 하나는 다른 사람에게 가르쳐 줄 수 있을만큼 파고들어보는 것이다.) 처음 이 글은 Javascript의 객체(Object) 개념에 대해 쓰고, 그 다음에는 많은 이들을 혼동시키는 ’prototype’에 대해 써 보고자 한다. 그리고 그 다음 글은 역시 많은 사람들을 충격과 공포로 인도하는;; ’this’에 대해 써 보려고 한다. (쓰다보니 너무 길어져 3개의 글로 나누려고 한다.) 책의 많은 부분은 앞에서 언급한 “You Don’t Know JS” 시리즈 중 “You Don’t Know JS: this & Object Prototypes” 편에서 영감을 얻고 있다. 하지만 단순한 번역은 아닌 개인적인 해석과 추가적인 이해에 도움되는 내용을 적고자 한다. 매우 좋은 책이기 때문에 관심있는 분들은 읽어봐도 좋을 것 같다. (참고로 이 글의 코드들은 기본적으로 브라우저 환경이 아닌 Node.js 환경을 가정하고 있다.)

2. Javacript의 모든 것은 객체이다?

흔히 Javascript의 모든 것은 객체이며 Javascript (만이) 진정한 객체지향언어라는 이야기를 한다. 하지만 엄격히 문법적으로 말하자면 사실이 아니다. Javascript에는 다음과 같이 6개의 기본 데이터 타입(primitive data type)이 있다. (You don’t Know JS (YDKJ) 기준으로)

  • string
  • number
  • boolean
  • null
  • undefined
  • object

좀 더 엄격하게 말하면 MDN reference에서는 5개의 primitive type (string, number, boolean, null, undefined)와 object로 구분하여 이야기하기도 한다. ES6부터는 symbol을 primitive type에 추가해 이야기한다. 다른 곳들과 달리 YDKJ에서는 소문자로 모든 기본 type을 쓰는데 (String이 아니라 string) 개인적으로는 이 편을 선호한다. 뒤에 나올 내장객체(Built-in Object)와의 혼동을 피하기 위함이다.

Note: Javascript에서는 모든 것은 객체이다…라고 이야기 되는 몇가지 ‘버그’들이 있는데 대표적인 것이 다음과 같은 것이다. null은 그 자신이 그냥 null인 primitive type이 맞다.

> typeof null
‘object’
// what the h… No, this is a bug.

’null’과 ‘undefined’: 이 두 개념도 꽤 만악의 근원…인데 기본적으로 공식 정의조차 매우 어긋나 있다. 언어 정의는 undefined는 어떤 변수가 선언되었지만 어떤 값을 할당받지 않은 상태를 말하고 null은 할당값이라고 설명하고 있다. 하지만 undefined와 null은 그 자체로 엄연한 primitive type이고 각각 ‘undefined’와 ‘null’이라는 값을 가진다. (null과 달리 typeof undefined는 ‘undefined’라고 잘 나온다.) 실제로 undefined는 다음과 같은 경우에 쓰인다.

  • 변수에 값이 할당되기 전의 기본 값
  • 함수가 호출될 때 caller로 부터 제공되지 않은 인자의 값
  • 접근한 객체의 프로퍼티가 ‘존재하지 않는’ 경우의 반환 값

이에 반해 null은 개인적인 감각으로는 언어 자체에 의해서 보다는 ‘프로그래머들에게서’ 사용되는 값이다. 원래는 선언은 되었지만 값이 없을 때 쓰이는 것으로 기획되었지만 사실 런타임에서는 이 용도로는 undefined가 더 일반적으로 쓰이기 때문이다. 이에 반해 프로그래머들은 위와 같은 용도로 null을 직접 많이 사용한다. 예를 들어 foo라는 변수를 선언하고 일단 ‘값이 없음’ 이라고 해 두려면 null을 넣어두는게 다른 언어를 사용하던 프로그래머들의 본성 아니겠는가? 이런 이유에서인지 언어 런타임 자체가 아니라 3자(?)가 제작한 프레임워크들의 함수에서는 어떤 객체를 요청했을 때 없는 경우 null이 리턴되는 경우가 많다. (브라우저에서 DOM 객체 접근시 getElementById 같은 함수에서 없으면 null이 넘어오는 게 대표적인 예이다.)

> var foo = null;
// yes, it is usual

3. Built-in Functions (내장 함수들)

위의 primitive type(굳이 기본타입이 아니라 primitive type이라고 하는 것은 한국어로 기본타입이라고 써 버리면 너무 일반적인 의미로 이해되기 때문이다. primitive type은 명확히 다른 내장 객체/함수들이나 다른 객체들과 구분해서 이해할 필요가 있다.)들은 이에 대응하는 내장(built-in) 객체를 가지고 있다. 아니, 매우 엄밀히 말하면 내장 객체가 아니라 그냥 ‘함수’이다. 뭐.. JS에서 함수도 객체 아닌가 하면 맞는 말이기도 하지만 함수는 조금 ‘특이한’ 객체이다. 이는 뒤에 다시 이야기하겠다. Primitive type들 중 undefined나 null을 제외하면 각각 대응하는 내장 함수들이 존재한다. 내장 함수들은 대표적으로 다음과 같은 것들이다. (구분하기 위해서 첫글자를 대문자로 쓴다.)

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

보다시피 바로 대응하는 것도 있고 Date나 RegExp처럼 primitive data type에는 없는 것들도 있다. 예를 들어 primitive type인 ‘string’과 내장 함수 String은 엄연히 다른 존재들이다.

var strPrimitive = "I am a string";
typeof strPrimitive; // 'string'
strPrimitive instanceof String; // false

var strObject = new String("I am a string");
typeof strObject; // 'object'
strObject instanceof String; // true

4. JS에서의 객체 생성

JS에서 객체를 생성하는 방법은 크게 3가지가 있다.

  • 1) 객체 literal로 생성하는 법
  • 2) 생성자 호출, 즉 함수에 new를 써서 생성하는 법
  • 3) Object.create() 를 써서 생성하는 법

3)은 많이 쓰이지 않지만 매우 좋은 방법이다. 왜 그런지는 추후 설명하고자 한다. 1)은 가장 선호되는 방법이다. JS는 간단한 선언 방법으로 객체를 선언할 수 있다.

var myObject = {
    a: 2,
    name: "I'm myObject"
};

> myObject
{ a: 2, name: 'I\'m myObject' }
> myObject.a
2
> myObject.name
'I\'m myObject'

Literal을 이용한 객체 선언은 간단한 객체의 선언이 필요할 때, JSON을 활용하여 객체를 선언할 때 등에서 매우 편리한 방법이다. 이에 비해 조금 복잡한 객체를 선언하거나 기존의 class 기반 객체지향 언어에 익숙한 사람들이 많이 사용할 수 있는 방법이 2)의 방법일 것이다.

var MyFunction = function() {
    this.a = 1;
this.name = "I am MyObject!";
}

var mine = new MyFunction();

> mine
MyFunction { a: 1, name: 'I am MyObject!' }

이 때의 MyFunction을 생성자/constructor라고 한다. 사실 이를 ‘생성자’라고 부르는 것도 많은 오해를 낳게 하는 악의 근원이다. 기존의 Java나 C++같은 클래스 기반 객체지향 언어를 사용하던 사람들이 볼 때 위의 코드는 매우 평이하게 보일 것이다. 비록 class라고 쓰지는 않았지만 JS에서 함수는 객체와 같다고도 했다. (정확히는 객체 중에는 함수객체라는것도 있는 것이지만) 그래서 함수를 new하면 mine이라는 인스턴스(instance)가 만들어졌다. 라고 생각한다. NO! 하지만 매우 유감이게도 이 말은 틀렸다.

기존의 class기반 객체지향 언어에서는 class는 일종의 청사진(blueprint)이다. 객체의 설계를 class에서 한다음 이를 실제 생성, 즉 인스턴스(instance)를 만드는 것이다. 기존 class기반 언어에서 위의 MyFunction과 mine의 관계는 다음 그림과 같이 이해된다.

하지만 JS에서는 이 class와 instance의 개념이 없다. 굳이 말하자면 모두 instance이지만 인스턴스라고 말하면 헷갈릴 수도 있어 JS에서는 그냥 이를 모두 object라고 한다. 다시말해 위의 MyFunction 부분은 다른 언어처럼 그냥 ‘선언(declaration)’이 아니다. 이 자체로 객체가 생성된다. 그래서 위의 MyFunction, mine은 각각 별도의 object로 존재한다. 그림으로 표현하면 다음과 같다.

5. new가 하는 일

위의 두번째 그림에서 한가지 재미있는 것은 화살표 방향이 첫번째 그림과는 반대라는 것이다. 이 화살표는 무엇인가?의 답을 알기 위해서는 정확하게 new가 어떤 일을 하는지 알아야 할 필요가 있다.

어떤 함수에 new를 사용하면 새로운 객체를 생성할 수 있다. 이 때 이 함수를 생성자(constructor)라고 한다. 여기서 중요한 개념은 생성자에 new를 사용해서 객체를 만드는게 아니라 “new에 사용되는 함수를 생성자라고 하는 것”이다. 다시 말해 모든 함수는 생성자가 될 수 있고 뭔가 따로 있는 것이 아니라는 것이다.

어떤 함수에 new를 사용하면 다음의 4단계의 동작이 발생한다.

  • 1) 새로운 객체가 생성된다.
  • 2) 새로 생성된 객체의 [[prototype]] 이 링크된다.
  • 3) 생성자함수의 this가 새로 생성된 객체로 (bind)되어 생성자가 호출된다.
  • 4) 새로 생성된 객체가 리턴된다 (new의 리턴값으로). 단, 한가지 예외가 있는데 해당 생성자가 다른 별도의 객체를 리턴하는 경우다. 이 경우는 새로 생성된 객체는 그냥 버려지고 생성자가 리턴하는 객체가 new의 리턴값이 된다.

위의 4단계를 잘 살펴보면 매우 중요한 개념을 알 수 있다. 생성자는 새로 생성된 객체와 강하게 연관된(기존 class/instance관계처럼) 그 무엇이 아니라 그냥 객체의 생성에서 호출되어 이를 도와주는 일종의 헬퍼와 같은 역할임을 알 수 있다. JS에서 함수, 즉 여기서의 생성자는 절대 다른 언어의 class와 같은 개념이 아니다. 다음 글에서는 2) 과정의 [[prototype]] 에 대해서 알아보겠다.