2020년 8월 16일

참조 타입

심화 학습

이번 절에선 특정 에지 케이스(edge case)를 설명하기 위한 심화 내용을 다룹니다.

숙련된 상당수의 개발자가 이 절에서 다룰 내용을 모른 채로 일하고 있지만 문제가 없고, 중요한 내용은 아니기 때문에 자바스크립트 내부에서 어떤 일이 일어나는지 알고 싶지 않다면 이번 글은 넘어가거나 미뤄도 괜찮습니다.

복잡한 상황에서 메서드를 호출하면 this 값을 잃어버리는 경우가 생깁니다.

예시를 살펴봅시다.

let user = {
  name: "John",
  hi() { alert(this.name); },
  bye() { alert("Bye"); }
};

user.hi(); // John (간단한 호출은 의도한 대로 잘 동작합니다.)

// name에 따라 user.hi나 user.bye가 호출되게 해봅시다.
(user.name == "John" ? user.hi : user.bye)(); // TypeError: Cannot read property 'name' of undefined

마지막 줄에서 조건부 연산자를 사용해 user.hiuser.bye 중 하나가 호출되도록 했습니다. user의 name이 "John"이므로 user.hi가 호출될 것이라 예상하며 말이죠.

그런데 에러가 발생했습니다. 뒤에 ()가 있어서 메서드 hi가 즉시 호출될 것이라 예상했는데 원하는 대로 되지 않았네요.

에러는 메서드를 호출할 때 "this"undefined가 할당되었기 때문에 발생했습니다.

마지막 줄이 아래와 같았다면 에러 없이 잘 작동했을 겁니다.

user.hi();

그런데 아래 코드에선 에러가 발생하죠.

(user.name == "John" ? user.hi : user.bye)();

원인이 뭘까요? 원인을 알려면 obj.method()를 호출했을 때, 내부에서 어떤 일이 일어나는지 알아야 합니다.

참조 타입 자세히 알아보기

코드를 유심히 살펴보면 obj.method()엔 연산이 두 개 있다는 걸 눈치챌 수 있습니다.

  1. '.'은 객체 프로퍼티 obj.method에 접근합니다.
  2. 괄호 ()는 접근한 프로퍼티(메서드)를 실행합니다.

그렇다면 첫 번째 연산에서 얻은 this 정보가 어떻게 두 번째 연산으로 전달될까요?

두 연산을 각각 별도의 줄에서 두었다면 this 정보를 잃는 건 확실합니다.

let user = {
  name: "John",
  hi() { alert(this.name); }
}

// 메서드 접근과 호출을 별도의 줄에서 실행함
let hi = user.hi;
hi(); // this가 undefined이기 때문에 에러가 발생합니다.

hi = user.hi에선 함수가 변수에 할당됩니다. 그런데 마지막 줄과는 완전히 독립적으로 동작하므로 this엔 아무런 값도 저장되지 않습니다.

user.hi()를 의도한 대로 동작시키기 위해 자바스크립트는 속임수를 사용합니다. '.'이 함수가 아닌, 참조 타입(Reference Type) 값을 반환하게 하죠.

참조 타입은 '명세서 에서만 사용되는 타입(specification type)'입니다. 개발자가 실제론 사용할 수 없습니다.

참조 타입에 속하는 값은 (base, name, strict)이 조합된 형태를 띱니다.

  • base: 객체
  • name: 프로퍼티의 이름
  • strict: 엄격 모드에서 true

user.hi로 프로퍼티에 접근하면 함수가 아닌, 참조형(참조 타입) 값을 반환합니다. 엄격 모드에선 아래와 같은 값이 반환되죠.

// 참조형 값
(user, "hi", true)

참조형 값에 괄호 ()를 붙여 호출하면 객체, 객체의 메서드와 연관된 모든 정보를 받습니다. 이 정보를 기반으로 this(=user)가 결정됩니다.

이렇게 참조 타입은 내부에서 점 .연산에서 알아낸 정보를 괄호 ()로 전달해주는 ‘중개인’ 역할을 합니다.

그런데 점 연산 이외의 연산(할당 연산 등)은 참조 타입을 통째로 버리고 user.hi 값(함수)만 받아 전달합니다. 이 때문에 점 이외의 연산에선 this 정보가 사라집니다.

obj.method() 같이 점을 사용하거나, obj[method]() 같이 대괄호를 사용해 함수를 호출했을 때만 this 값이 의도한 대로 전달됩니다. 이런 문제는 func.bind() 등을 이용하면 해결 할 수 있는데, 이에 대해선 추후에 알아보도록 하겠습니다.

요약

참조 타입은 자바스크립트 내부에서 사용되는 타입입니다.

.이나 대괄호를 사용해 객체 프로퍼티인 메서드(obj.method())에 접근하려 하면 정확한 프로퍼티 값이 반환되는 것이 아니라 특별한 형태의 값인 ‘참조 타입’ 값이 반한됩니다. 이 참조타입 값엔 프로퍼티 값과 프로퍼티가 정의된 객체 정보가 담겨있습니다.

()를 사용해 메서드를 호출할 때, 메서드 내에서 사용되는 this에 제대로 된 객체 정보를 전달해 줄 수 있는 이유가 바로 ‘참조 타입’ 덕분입니다.

그런데 .이나 대괄호 이외의 연산에선 참조 타입이 그냥 프로퍼티 값으로 변해버립니다. 객체 메서드라면 함숫값으로 변해버리죠.

이런 내부 동작은 보이지 않는 곳에서 일어납니다. 참조 타입이 어떻게 동작하는지 알아야 해결할 수 있는 문제는 표현식을 이용해 동적으로 객체에서 메서드를 가져올 때와 같이 자주 발생하지 않습니다.

과제

중요도: 2

아래 코드의 실행 결과를 예측해보세요.

let user = {
  name: "John",
  go: function() { alert(this.name) }
}

(user.go)()

주의: 함정을 파놓았습니다. (• ◡•)

에러가 발생합니다!

코드를 직접 실행해봅시다.

let user = {
  name: "John",
  go: function() { alert(this.name) }
}

(user.go)() // error!

브라우저에서 출력되는 에러 메시지만 봐서는 무엇이 잘못되었는지 파악하기 어려울 겁니다.

에러는 user = {...}뒤에 세미콜론이 없어서 발생했습니다.

자바스크립트는 괄호((us... ) 앞에 세미콜론을 자동으로 넣어주지 않습니다. 따라서 코드는 아래와 같아집니다.

let user = { go:... }(user.go)()

이렇게 두 표현식이 합쳐지면서 인수가 (user.go)인 객체 형태의 함수를 호출한 것처럼 되었습니다. 여기에 더하여 객체 user가 정의되지 않은 상태에서 같은 줄에 let user를 사용했기 때문에 에러가 발생합니다.

user = {...}뒤에 세미콜론을 붙여서 에러를 해결해봅시다.

let user = {
  name: "John",
  go: function() { alert(this.name) }
};

(user.go)() // John

참고로, (user.go)를 감싸는 괄호는 아무런 역할을 하지 않습니다. 괄호는 대개 연산자 우선순위를 바꾸는 데 사용되는데, (user.go)에선 점 . 연산자가 먼저 동작하기 때문에 의미가 없습니다. 문제 출제 의도는 세미콜론 여부였습니다.

중요도: 3

아래 코드에선 다양한 방법으로 user.go()를 4번 연속 호출합니다.

그런데 첫 번째((1))와 두 번째 호출((2)) 결과는 세 번째((3))와 네 번째((4)) 호출 결과와 다릅니다. 이유가 뭘까요?

let obj, method;

obj = {
  go: function() { alert(this); }
};

obj.go();               // (1) [object Object]

(obj.go)();             // (2) [object Object]

(method = obj.go)();    // (3) undefined

(obj.go || obj.stop)(); // (4) undefined

이유는 다음과 같습니다.

  1. 우리가 알고 있는 일반적인 메서드 호출 방법입니다.

  2. 역시 일반적인 호출 방법에 속합니다. 괄호가 추가되었긴 하지만 연산 우선순위를 바꾸진 않으므로 점 연산자가 먼저 실행됩니다.

  3. 좀 더 복잡한 패턴의 호출((expression).method())이 등장했네요. 세 번째 호출은 아래와 같은 코드로 쪼갤 수 있습니다.

    f = obj.go; // 표현식 계산하기
    f();        // 저장된 것 호출하기

    위 코드에서 f()는 (메서드가 아닌) 함수로써 호출되었습니다. this에 대한 정보가 전혀 없는 상태에서 말이죠.

  4. (3)과 동일한 패턴의 호출입니다. expressionobj.go || obj.stop라는 차이점만 있습니다.

(3)(4)에서 어떤 일이 일어나는지 알려면 참조 타입을 다시 상기해야 합니다. 점이나 대괄호를 통해 프로퍼티에 접근하려는 경우 참조 타입 값((base, name, strict))이 반환됩니다.

메서드 호출을 제외하고, 참조 타입 값에 행해지는 모든 연산은 참조 타입 값을 일반 값으로 변환시킵니다. 이 과정에서 this에 정보가 누락됩니다.

튜토리얼 지도