객체는 사용자(user), 주문(order) 등과 같이 실제 존재하는 개체(entity)를 표현하고자 할 때 생성됩니다.

let user = {
  name: "John",
  age: 30
};

사용자는 현실에서 장바구니에서 물건 선택하기, 로그인하기, 로그아웃하기 등의 행동을 합니다. 이와 마찬가지로 사용자를 나타내는 객체 user도 특정한 행동을 할 수 있습니다.

자바스크립트에선 객체의 프로퍼티에 함수를 할당해 객체에게 행동할 수 있는 능력을 부여해줍니다.

메서드 만들기

객체 user에게 인사할 수 있는 능력을 부여해 줍시다.

let user = {
  name: "John",
  age: 30
};

user.sayHi = function() {
  alert("안녕하세요!");
};

user.sayHi(); // 안녕하세요!

함수 표현식으로 함수를 만들고, 객체 프로퍼티 user.sayHi에 함수를 할당해 주었습니다.

이제 객체에 할당된 함수를 호출하면 user가 인사를 해줍니다.

이렇게 객체 프로퍼티에 할당된 함수를 메서드(method) 라고 부릅니다.

위 예시에선 usersayHi가 메서드이죠.

메서드는 아래와 같이 이미 정의된 함수를 이용해서 만들 수도 있습니다.

let user = {
  // ...
};

// 함수 선언
function sayHi() {
  alert("안녕하세요!");
};

// 선언된 함수를 메서드로 등록
user.sayHi = sayHi;

user.sayHi(); // 안녕하세요!
객체 지향 프로그래밍

객체를 사용하여 개체를 표현하는 방식을 객체 지향 프로그래밍(object-oriented programming, OOP) 이라 부릅니다.

OOP는 그 자체만으로도 학문의 분야를 만드는 중요한 주제입니다. 올바른 개체를 선택하는 방법, 개체 사이의 상호작용을 나타내는 방법 등에 관한 의사결정은 (객체 지향) 설계를 기반으로 이뤄집니다. 관련 추천도서로는 에릭 감마의 “GoF의 디자인 패턴”, 그래디 부치의 “UML을 활용한 객체지향 분석 설계” 등이 있습니다.

메서드 단축 구문

객체 리터럴 안에 메서드를 선언할 때 사용할 수 있는 단축 문법을 소개해 드리겠습니다.

// 아래 두 객체는 동일하게 동작합니다.

user = {
  sayHi: function() {
    alert("Hello");
  }
};

// 단축 구문을 사용하니 더 깔끔해 보이네요.
user = {
  sayHi() { // "sayHi: function()"과 동일합니다.
    alert("Hello");
  }
};

위처럼 "function"을 생략해도 메서드를 정의할 수 있습니다.

일반적인 방법과 단축 구문을 사용한 방법이 완전히 동일하진 않습니다. 객체 상속과 관련된 미묘한 차이가 존재하는데 지금으로선 이 차이가 중요하지 않기 때문에 넘어가도록 하겠습니다.

메서드와 “this”

메서드는 객체에 저장된 정보에 접근할 수 있어야 제 역할을 할 수 있습니다. 모든 메서드가 그런 건 아니지만, 대부분의 메서드가 객체 프로퍼티의 값을 활용합니다.

user.sayHi()의 내부 코드에서 객체 user에 저장된 이름(name)을 이용해 인사말을 만드는 경우가 이런 경우에 속합니다.

메서드 내부에서 this 키워드를 사용하면 객체에 접근할 수 있습니다.

"점 앞"의 this는 객체를 나타냅니다. 정확히는 메서드를 호출할 때 사용된 객체를 나타내죠.

예시:

let user = {
  name: "John",
  age: 30,

  sayHi() {
    // "this"는 "현재 객체"를 나타냅니다.
    alert(this.name);
  }

};

user.sayHi(); // John

user.sayHi()가 실행되는 동안에 thisuser를 나타냅니다.

this를 사용하지 않고 외부 변수를 참조해 객체에 접근하는 것도 가능합니다.

let user = {
  name: "John",
  age: 30,

  sayHi() {
    alert(user.name); // "this" 대신 "user"를 이용함
  }

};

그런데 이렇게 외부 변수를 사용해 객체를 참조하면 예상치 못한 에러가 발생할 수 있습니다. user를 복사해 다른 변수에 할당(admin = user)하고, user는 전혀 다른 값으로 덮어썼다고 가정해 봅시다. sayHi()는 원치 않는 값(null)을 참조할 겁니다.

실제 코드를 이용해 에러를 만들어 보겠습니다.

let user = {
  name: "John",
  age: 30,

  sayHi() {
    alert( user.name ); // Error: Cannot read property 'name' of null
  }

};


let admin = user;
user = null; // user를 null로 덮어씁니다.

admin.sayHi(); // sayHi()가 엉뚱한 객체를 참고하면서 에러가 발생했습니다.

alert 함수가 user.name 대신 this.name을 인수로 받았다면 에러가 발생하지 않았을 겁니다.

자유로운 “this”

자바스크립트의 this는 다른 프로그래밍 언어의 this와 동작 방식이 다릅니다. 자바스크립트에선 모든 함수에 this를 사용할 수 있습니다.

아래와 같이 코드를 작성해도 문법 에러가 발생하지 않습니다.

function sayHi() {
  alert( this.name );
}

this 값은 런타임에 결정됩니다. 컨텍스트에 따라 달라지죠.

동일한 함수라도 다른 객체에서 호출했다면 "this"가 참조하는 값이 달라집니다.

let user = { name: "John" };
let admin = { name: "Admin" };

function sayHi() {
  alert( this.name );
}

// 다른 객체에서 동일한 함수를 사용함
user.f = sayHi;
admin.f = sayHi;

// "this"는 "점(.) 앞의" 객체를 참조하기 때문에
// this 값이 달라짐
user.f(); // John  (this == user)
admin.f(); // Admin  (this == admin)

admin['f'](); // Admin (점(.)과 대괄호는 동일하게 동작함)

규칙은 간단합니다. obj.f()를 호출했다면 thisf를 호출하는 동안의 obj입니다. 위 예시에선 objuseradmin을 참조하겠죠.

객체 없이 호출하기: this == undefined

객체가 없어도 함수를 호출할 수 있습니다.

function sayHi() {
  alert(this);
}

sayHi(); // undefined

위와 같은 코드를 엄격 모드에서 실행하면, thisundefined가 할당됩니다. this.name으로 name에 접근하려고 하면 에러가 발생하죠.

그런데 엄격 모드가 아닐 때는 this전역 객체를 참조합니다(브라우저 환경에선 window. 전역 객체는 전역 객체에서 자세히 다룰 예정입니다). 이런 동작 차이는 "use strict"가 도입된 배경이기도 합니다.

이런 식의 코드는 대게 실수로 작성된 경우가 많습니다. 함수 본문에 this가 사용되었다면, 객체 컨텍스트 내에서 함수를 호출할 것이라고 예상하시면 됩니다.

자유로운 this가 만드는 결과

다른 언어를 사용하다 자바스크립트로 넘어온 개발자는 this를 혼동하기 쉽습니다. this는 항상 메서드가 정의된 객체를 참조할 것이라고 착각하죠. 이런 개념을 "bound this"라고 부릅니다.

자바스크립트에서 this는 런타임에 결정됩니다. 메서드가 어디서 정의되었는지에 상관없이 this는 “점 앞의” 객체가 무엇인가에 따라 “자유롭게” 결정됩니다.

이렇게 this가 런타임에 결정되면 좋은 점도 있고 나쁜 점도 있습니다. 함수(메서드)를 하나만 만들어 여러 객체에서 재사용할 수 있다는 것은 장점이지만, 이런 유연함이 실수로 이어질 수 있다는 것은 단점입니다.

자바스크립트가 this를 다루는 방식이 좋은지, 나쁜지는 우리가 판단할 문제가 아닙니다. 개발자는 this의 동작 방식을 충분히 이해하고, 장점을 취하면서 실수를 피하는 데만 집중하면 됩니다.

참조 타입

심화 학습

이번 절은 특정 에지 케이스(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)(); // Error: Cannot read property 'name' of undefined

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

()가 있어서 메서드 hi가 즉시 호출될 것이라 예상했는데, 에러가 발생했습니다.

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

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

user.hi(); // object dot method

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

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

이유가 뭘까요? 원인을 알려면 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() 등을 이용하면 해결 할 수 있는데, 이에 대해선 추후에 알아보도록 하겠습니다.

"this"가 없는 화살표 함수

화살표 함수는 일반 함수와는 다르게 “고유한” this를 가지지 않습니다. 화살표 함수에서 this를 참조하면, (화살표 함수가 아닌) “평범한” 외부 함수에서 this 값을 가져옵니다.

아래 예시에서 함수 arrow()this는 외부 함수 user.sayHi()this를 사용합니다.

let user = {
  firstName: "Ilya",
  sayHi() {
    let arrow = () => alert(this.firstName);
    arrow();
  }
};

user.sayHi(); // Ilya

별개의 this가 만들어지는 건 원하지 않고, 외부 컨텍스트에 있는 this를 이용하고 싶은 경우 화살표 함수가 유용합니다. 이에 대한 자세한 내용은 별도의 챕터, 화살표 함수에 대한 재고에서 다루겠습니다.

요약

  • 객체 프로퍼티에 저장된 함수를 "메서드"라고 부릅니다.
  • object.doSomthing()은 객체를 "행동"할 수 있게 해줍니다.
  • 메서드는 this로 객체를 참조합니다.

this 값은 런타임에 결정됩니다.

  • 함수를 선언할 때 this를 사용할 수 있습니다. 다만, 함수가 호출되기 전까지 this엔 값이 할당되지 않습니다.
  • 함수를 복사해 객체 간 전달할 수 있습니다.
  • 함수를 객체 프로퍼티에 저장해 object.method()같이 “메서드” 형태로 호출하면 thisobject를 참조합니다.

화살표 함수는 자신만의 this를 가지지 않는다는 점에서 독특합니다. 화살표 함수 안에서 this를 사용하면, 외부에서 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에 대한 정보가 누락됩니다.

중요도: 5

함수 makeUser는 객체를 반환합니다.

이 객체의 ref에 접근하면 어떤 결과가 발생하고, 그 이유는 뭘까요?

function makeUser() {
  return {
    name: "John",
    ref: this
  };
};

let user = makeUser();

alert( user.ref.name ); // 결과가 어떻게 될까요?

Answer: an error.

Try it:

function makeUser() {
  return {
    name: "John",
    ref: this
  };
};

let user = makeUser();

alert( user.ref.name ); // Error: Cannot read property 'name' of undefined

That’s because rules that set this do not look at object definition. Only the moment of call matters.

Here the value of this inside makeUser() is undefined, because it is called as a function, not as a method with “dot” syntax.

The value of this is one for the whole function, code blocks and object literals do not affect it.

So ref: this actually takes current this of the function.

Here’s the opposite case:

function makeUser() {
  return {
    name: "John",
    ref() {
      return this;
    }
  };
};

let user = makeUser();

alert( user.ref().name ); // John

Now it works, because user.ref() is a method. And the value of this is set to the object before dot ..

중요도: 5

아래 3가지 메서드를 가진 calculator라는 객체를 만들어보세요.

  • read()는 프롬프트 창에서 두 값을 보여주고 객체의 프로퍼티로 저장합니다.
  • sum()은 저장된 두 값의 합을 반환합니다.
  • mul()은 저장된 두 값의 곱을 반환합니다.
let calculator = {
  // ... 여기에 답안 작성 ...
};

calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );

데모 실행하기

테스트 코드가 담긴 샌드박스를 열어 정답을 작성해보세요.

let calculator = {
  sum() {
    return this.a + this.b;
  },

  mul() {
    return this.a * this.b;
  },

  read() {
    this.a = +prompt('a?', 0);
    this.b = +prompt('b?', 0);
  }
};

calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );

테스트 코드가 담긴 샌드박스를 열어 정답을 확인해보세요.

중요도: 2

올라가기와 내려가기 메서드를 제공하는 객체 ladder가 있다고 합시다.

let ladder = {
  step: 0,
  up() {
    this.step++;
  },
  down() {
    this.step--;
  },
  showStep: function() { // 사다리에서 몇 번째 단에 올라와 있는지 보여줌
    alert( this.step );
  }
};

만일 메서드를 연이어 호출하고자 한다면 아래와 같이 코드를 작성할 수 있습니다.

ladder.up();
ladder.up();
ladder.down();
ladder.showStep(); // 1

아래와 같이 메서드 호출 체이닝이 가능하도록 up, down, showStep을 수정해보세요.

ladder.up().up().down().showStep(); // 1

이러한 방식은 자바스크립트 라이브러리에서 널리 사용됩니다.

테스트 코드가 담긴 샌드박스를 열어 정답을 작성해보세요.

메서드를 호출할 때마다 객체 자신을 반환하게 하면 됩니다.

let ladder = {
  step: 0,
  up() {
    this.step++;
    return this;
  },
  down() {
    this.step--;
    return this;
  },
  showStep() {
    alert( this.step );
    return this;
  }
}

ladder.up().up().down().up().down().showStep(); // 1

메서드 호출 하나씩 한 줄에 작성할 수도 있습니다. 체이닝이 길어질 경우 이렇게 작성하면 가독성이 좋아집니다.

ladder
  .up()
  .up()
  .down()
  .up()
  .down()
  .showStep(); // 1

테스트 코드가 담긴 샌드박스를 열어 정답을 확인해보세요.

튜토리얼 지도

댓글

댓글을 달기 전에 마우스를 올렸을 때 나타나는 글을 먼저 읽어주세요.
  • 추가 코멘트, 질문 및 답변을 자유롭게 남겨주세요. 개선해야 할 것이 있다면 댓글 대신 이슈를 만들어주세요.
  • 잘 이해되지 않는 부분은 구체적으로 언급해주세요.
  • 댓글에 한 줄짜리 코드를 삽입하고 싶다면 <code> 태그를, 여러 줄로 구성된 코드를 삽입하고 싶다면 <pre> 태그를 이용하세요. 10줄 이상의 코드는 plnkr, JSBin, codepen 등의 샌드박스를 사용하세요.