본문으로 바로가기

javaScript 생성자 함수의 핵심 패턴

자바스크립트에는 클래스가 없기 때문에 상당히 유연합니다.

객체에 대해 사전에 알아두어야 하는 내용, 즉 클래스의 '청사진' 같은 것이 필요없기 때문입니다.

그러나 자바스크립트에도 자바와 같은 클래스 기반 객체 생성과 비슷한 문법을 가지는 생성자 함수가 존재합니다.




생성자 함수로 객체 생성하기

객체를 생성할 때는 사용자가 직접 만든 생성자 함수를 사용할 수도 있고, Object(), Date(), String() 등 내장 생성자를 사용할 수도 있습니다.


다음 예제는 동일한 객체를 생성하는 두 가지 방법을 보여줍니다.

javascript
// 첫 번째 방법 - 객체 리터럴 사용
var car = { goes : 'far'};

// 다른 방법 - 내장 생성자 사용
// 경고 : 이 방법은 안티패턴이다.
var car = new Object();
car.goes = 'far';

보다시피 첫 번째인 객체 리터럴 표기법의 명백한 이점은 더 짧다는 것입니다.

또한 객체란 그저 변형가능한 해시에 불과하며 어떤 '조리법'에 의해(즉 클래스로부터) 구워내야만 하는 특별한 것이 아님을 확실히 보여주고 있습니다.

이점도 리터럴로 객체를 생성하는 패턴의 장점 중 하나입니다.

리터럴 표기법을 사용하면 유효범위 판별 작업도 발생하지 않습니다. 

생성자 함수를 사용했다면 지역 유효범위에 동일한 이름의 생성자가 있을 수 있기 때문에 Object()를 호출한 위치에서부터 전역 Object 생성자까지 인터프리터가 쭈욱 거슬러 올라가며 유효범위를 검색해야 합니다.



객체 생성자의 함정

객체 리터럴을 사용할 수 있는 상황에서는 new Object() 생성자를 쓸 이유가 없지만, 다른 사람이 작성한 레거시(legacy) 코드를 물려받을 수 있기 때문에 이 생성자의 '기능'을 알아둘 필요가 있을 것입니다. (결국 생성자를 써서는 안 되는 이유이기도 하다.)

문제의 기능은 Object() 생성자가 인자를 받을 수 있다는 점입니다. 인자로 전달되는 값에 따라 생성자 함수가 다른 내장 생성자에 객체 생성을 위임할 수 있고, 따라서 개대한 것과는 다른 객체가 반환되기도 합니다.


다음은 new Object()에 숫자, 문자열, 불린 값을 전달한 몇 가지 예로, 예상한 바와 다른 생성자로 생성된 객체가 반환됩니다.

javascript
// 경고 : 모두 안티패턴

// 빈 객체
var o = new Object();
console.log(o.constructor === Object); // true

// 숫자 객체
var o = new Object(1);
console.log(o.constructor === Number); // true
o.toFixed(2); // '1.00'

// 문자열 객체
var o = new Object('I am a String');
console.log(o.constructor === String); // true
// 일반적인 객체에는 substring() 이라는 메서드가 없자민 문자열 객체에는 있다.
console.log(typeof o.substring); // 'function'

// 불린 객체
var o = new Object(ture);
console.log(o.constructor === Boolean); // true


Object() 생성자의 이 같은 동작 방식 때문에, 런타임이 결정하는 동적인 값이 생성자에 인자로 전달될 경우 예기치 않은 결과가 반환될 수 있다. 거듭 말하지만 결론적으로 new Object()를 사용하지 마라. 

더 간단하고 안정적인 객체 리터럴을 사용하라.



사용자 정의 생성자 함수

객체 리터럴 패턴이나 내장 생성자 함수를 쓰지 않고, 직접 생성자 함수를 만들어 객체를 생성할 수도 있습니다.


다음 예제를 살펴봅니다.

javascript
var Person = function (name) {
    this.name = name;
    this.say = function () {
        return 'I am ' + this.name;
    };
};

var jaehee = new Person('jaehee');
jaehee.say(); // 'I am jaehee'

위 코드는 사용자 Person 생성자 함수를 정의한 것입니다.

이 패턴은 자바에서 Person이라는 클래스를 사용하여 객체를 생성하는 방식과 상당히 유사합니다.

그러나 문법은 비슷해도 자바스크립트에는 클래스라는 것이  없으며 Person은 그저 보통의 함수일 뿐입니다.


new와 함께 생성자 함수를 호출하면 함수 안에서는 다음과 같은 일이 일어납니다.

  • 빈 객체(사실 빈 객체는 아니다)가 생성된다. 이 객체는 this라는 변수로 참조할 수 있고, 해당 함수의 프로토타입을 상속받는다.
  • this로 참조되는 객체에 프로퍼티와 메소드가 추가된다.
  • 마지막에 다른 객체가 명시적으로 반환되지 않을 경우, this로 참조된 이 객체가 반환된다.


즉 이면에서는 다음과 같이 진행된다고 할 수 있습니다.

javascript
var Person = function (name) {
// 객체 리터럴로 새로운 객체를 생성한다.
// var this = {};

// 프로퍼티와 메서드를 추가한다.
this.name = name;
this.say = function () {
    return 'I am ' + this.name;
};

// this를 반환한다.
// return this;
};

이 예제에서는 간단히 say() 라는 메소드를 this에 추가했습니다.

결과적으로 new Person()을 호출할 때마다 메모리에 새로운 함수가 생성되게 됩니다. say()라는 메소드는 인스턴스별로 달라지는 것이 아니므로 이런 방식은 명백히 비효율적입니다.

이 메소드는 Person의 프로토타입에 추가하는 것이 더 낫습니다.

javascript
Person.prototype.say = function () {
    return 'I am ' + this.name;
};

만전을 기하는 의미에서 한 가지 더 언급할 만한 사실이 있습니다.

먼저 생성자 내부의 이면에서는 다음과 같은 일이 벌어진다고 했습니다.

javascript
// var this = {};

그리고 이것이 다가 아닙니다. 왜냐하면 '빈' 객체라는게 실제로는 텅 빈 것이 아니기 때문입니다.

이 객체는 Person의 프로토타입을 상속받습니다.

즉 다음 코드에 더 가깝습니다.

javascript
// var this = Object.create(Person.prototype);



생성자의 반환값

생성자 함수를 new와 함께 호출하면 항상 객체가 반환됩니다.

기본값은 this 로 참조되는 객체입니다.

생성자 함수 내에서 아무런 프로퍼티나 메소드를 추가하지 않았다면 '빈'(즉, 생성자의 프로토타입에서 상속된 것 외에는 '비어있는') 객체가 반환될 것입니다.

함수 내에 return문을 쓰지 않더라도 생성자는 암묵적으로 this를 반환합니다.

그러나 반환 값이 될 객체를 따로 명시적으로 정할 수도 있습니다.


다음 예제에 새로운 객체를 생성하여  this로 참조하고 반환되는 것을 볼 수 있습니다.

javascript
var Objectmark = function () {
// 생성자가 다른 객체를 반환하기로 결정했기 때문에
// 다음의 'namme' 프로퍼티는 무시된다
this.name = 'This is it';

// 새로운 객체를 생성하여 반환한다.
var that = {};
that.name = "And that's that";
return that;
};

// TEST
var o = Objectmark();
console.log(o.name); // "And that's that"

이와 같이 생성자에서는 어떤 객체라도 (객체이기만 한다면)반환할 수 있습니다. 객체가 아닌 것(예를 들면 문자열이나 false 갑)을 반환하려고 시도한다면, 에러가 발생하진 않지만 그냥 무시되고 this에 의해 참조된 객체가 대신 반환됩니다.




new를 강제하는 패턴

앞서 언급했듯이 생성자란 new와 함께 호출될 뿐 여전히 별다를 것 없는 함수에 불과할 뿐입니다.

그렇다면 생성자를 호출할 때 new를 빼먹으면 어떻게 될까?

문법 오류나 런타임 에러가 발생하지는 않지만, 논리적인 요류가 생겨 예기치 못한 결과가 나올 수 있습니다.

new를 빼먹으면 생성자 내부의 this가 전역 객체를 가리키게 되기 때문입니다.(브라워저에서라면 thiswindow를 가리키게 된다)

생성자 내부에 this.member 와 같은 코드가 있을 경우 이 생성자를 new 없이 호출하면, 실제로는 전역 객체에 member라는 새로운 프로퍼티가 생성됩니다.

이 프로퍼티는 window.member 또는 그냥 member를 통해 접근할 수 있습니다. 알다시피 전역 네임스페이스는 항상 깨끗하게 유지해야 하기 때문에, 이런 동작 방식은 대단히 바람직하지 않습니다.

javascript
// 생성자
function Coffee() {
    this.tastes = 'dalcom';
}

// 새로운 객체
var morning_coffee = new Coffee();
console.log(typeof morning_coffee);  // 'object'
console.log(morning_coffee.tastes);  // 'dalcom'
javascript
// 안티 패턴
// 'new' 를 빼먹음
var morning_coffee = Coffee();
console.log(typeof morning_coffee);  // 'undefined'
console.log(window.tastes);  // 'dalcom'


ECMAScript5에서는 위와 같은 동작 방식의 문제에 대한 해결책으로, 스트릭트 모드에서는 this가 전역객체를 가리키지 않도록 했다. ES 5를 쓸 수 없는 상황이라면, 생성자 함수가 new 없이 호출되어도 항상 동일하게 동작하도록 보장하는 방법을 써야한다.


명명규칙

가장 간단한 대안은 명명규칙을 사용하는 것입니다. 

즉, 생성자 함수명의 첫글자를 대문자로 쓰고(Myconstructor) '일반적인' 함수와 메서드의 첫글자는 소문자를 사용(myFunction)하는 것입니다.



that 사용

명명 규칙을 따르는 것도 꽤 도움이 되지만 이는 올바른 동작 방식을 권고할 뿐 강제하지는 못합니다.

생성자가 항상 생성자로 동작하도록 해주는 패턴을 살펴보겠습니다.

this에 모든 멤버를 추가하는 대신, that에 모든 멤버를 추가한 후 that을 반환하는 것입니다.

javascript
function MyConstructor() {
    var that = {};
    that.name = 'jaehee';
    return that;
}

위 코드와 같이 간단한 객체라면 that이라는 지역 변수를 만들 필요도 없이 객체 리터럴을 통해 다음과 같이 객체를 반환해도 됩니다.

javascript
function MyConstructor() {
   return {
       name : 'jaehee'
   }
}


위의 MyConstructor() 구현 중 어느 것을 사용하더라도 호출 방법과 상관없이 항상 객체가 반환됩니다.

javascript
var first = new MyConstrutor(),
    second = MyConstrutor();

console.log(first.name); // 'jaehee'
console.log(second.name); // 'jaehee


하지만 위 패턴의 문제는 프로토타입과의 연결고리가 끊어지게 된다는 점입니다.

즉, MyConstructor() 프로토타입에 추가한 멤버를 객체에서 사용할 수 없게 됩니다.


that 이라는 변수명은 관습적인 것으로, 언어에 정의되어 있진 않다. 어떤 이름이라도 쓸 수 있다. 

흔히 사용되는 변수명으로는 selfme 등이 있다.




스스로를 호출하는 생성자

앞서 언급한 패턴의 문제점을 해결하고 인스턴스 객체에서 프로토타입의 프로퍼티들을 사용할 수 있기 하려면, 다음의 접근법을 고려하면 될 것입니다.

생성자 내부에서 this가 해당 생성자의 인스턴스인지를 확인하고, 그렇지 않은 경우 new와 함께 스스로를 재호출하는 것입니다.

javascript
function MyConstrutor() {
    if(!(this instanceof MyConstrutor)) {
       return new MyConstrutor();
    }

    this.name = 'jaehee';

}

MyConstrutor.prototype.anotherName = true;

// 호출확인
var first = new MyConstrutor(),
    second = MyConstrutor();

console.log(first.name); // 'jaehee'
console.log(second.name); // 'jaehee'

console.log(first.anotherName);
console.log(second.anotherName);


인스턴스를 판별하는 또다른 범용적인 방법은 생성자 이름을 하드코딩하는 대신 arguments.callee와 비교하는 것입니다.

javascript
function MyConstrutor() {

    if(!(this instanceof arguments.callee)) {
       return new arguments.callee();
    }

    this.name = 'jaehee';

}

이것은 모든 함수가 호출될 때, 내부적으로 arguments라는 객체가 생성되며, 이 객체가 함수에 전달된 모든 인자를 담고 있다는 점을 활용한 패턴입니다.

argumentscallee라는 프로퍼티는 호출된 함수를 가리킵니다. 


argument.callee 는 ES 5의 스트릭트 모드에서는 허용되지 않는다는 점에 유의하라.

향후의 사용은 제한하는 것이 좋고, 기존 코드(레거시코드)에서 발견되는 경우에는 제거해야 한다.



Jaehee's WebClub



댓글을 달아 주세요

  1. jin 2016.05.25 12:22

    글 감사히 읽었습니다~

  2. 랑호앙 2019.03.06 21:44

    공부하다 사용자 정의 생성자함수에서 두번째 그림 예시 코드에서 주석으로 객체 리터럴로 새로운 객체를 생성한다 라고 되어있는데 어떤 의미인지 모르겠어요 var Person = function (name) {} 이게 리터럴로 만든거라는 의미인가요?

  3. James 2019.03.24 21:07

    좋은 글 감사합니다. 많이 배우고 갑니다 :)

  4. BlogIcon 풋풋한 개발자 신고">2019.04.11 11:24 신고

    정리가 정말 잘되어있어서 참고하고 갈게요~ 감사합니다~

  5. sam 2020.02.08 16:14

    글 너무 잘봤습니다 ㅎㅎ 혹시 제 개인블로그에 출처 남기고 작성하신 일부분 인용해도 될까요?! 필기용도로 사용하는 블로그입니다!

  6. BlogIcon 일용직노동자 2020.03.06 18:30

    매우 좋은 글 잘보고갑니다. 혹시 자바스크립트 정리 포스팅을 할 예정인데, 출처 표기 후 몇몇 부분 인용을 해도 되겠습니까?