new 바인딩의 우선순위는?

생성자 함수로 new를 사용하여 호출할 때 this가 새로 생성된 객체에 바인딩되는 특수한 동작과 우선순위를 이해합니다

중급 15분 new 생성자 우선순위 인스턴스

JavaScript에서 new 키워드로 함수를 호출하면 단순히 함수를 실행하는 것 이상의 일이 벌어집니다. JavaScript 엔진은 빈 객체를 새로 생성하고, 그 객체를 this로 설정한 뒤 함수 본문을 실행하며, 마지막으로 해당 객체를 반환합니다. 이 일련의 과정이 바로 new 바인딩이며, 객체 지향 프로그래밍에서 인스턴스를 만드는 핵심 메커니즘입니다. 앞서 배운 명시적 바인딩(call/apply/bind)도 this를 강제로 지정할 수 있지만, new 바인딩은 그보다 더 높은 우선순위를 가집니다. 이 우선순위 관계를 제대로 이해하지 못하면, 생성자 함수와 바인딩을 함께 사용하는 코드에서 의도치 않은 동작이 발생할 수 있습니다.

🔑 핵심 특징

  • new로 함수를 호출하면 JavaScript가 새 빈 객체를 자동으로 생성하고 this로 설정한다
  • 함수 실행 후 명시적으로 다른 객체를 반환하지 않으면, 새로 생성된 객체가 자동으로 반환된다
  • new 바인딩은 call, apply, bind를 통한 명시적 바인딩보다 우선순위가 높다
  • bind로 고정된 함수라도 new로 호출하면 바인딩된 this가 무시되고 새 객체가 사용된다
  • 이 동작은 JavaScript 클래스 문법의 기반이 되며, class 키워드도 내부적으로 동일하게 작동한다

실무에서의 영향

new 바인딩의 우선순위를 이해하는 것은 JavaScript 클래스와 생성자 패턴을 올바르게 사용하기 위한 필수 지식입니다. React 같은 프레임워크가 클래스 컴포넌트를 인스턴스화할 때, Vue가 컴포넌트 옵션 객체를 처리할 때, 또는 직접 유틸리티 클래스를 설계할 때 모두 이 메커니즘이 작동합니다. 특히 bind로 부분 적용(partial application)된 함수를 생성자로 사용하는 고급 패턴에서는 newbind를 덮어쓴다는 사실을 모르면 심각한 버그로 이어집니다. 또한 this 바인딩 우선순위 전체 규칙(new > 명시적 > 암묵적 > 기본)을 파악하는 데 있어 new 바인딩이 정점에 있다는 점은, 코드가 복잡해질수록 더욱 중요한 판단 기준이 됩니다. 실무에서 생성자 패턴, 팩토리 패턴, 또는 믹스인 패턴을 다룰 때 이 지식이 코드 예측 가능성과 유지보수성을 크게 높여 줍니다.


핵심 개념

new 키워드의 4단계 동작 과정

입문

new로 함수를 호출하면 JavaScript가 뒤에서 몰래 네 가지 일을 자동으로 해줘요. 이 덕분에 우리는 ‘새 물건 만들기’를 쉽게 할 수 있답니다!

🏭 공장에서 물건 만드는 과정처럼 공장에서 물건을 만들 때는 정해진 순서가 있어요. 먼저 빈 틀을 준비하고, 설계도를 연결하고, 재료를 채우고, 완성품을 내보내죠. new도 이 공장 과정과 똑같이 네 단계를 차례로 수행합니다.

📦 1단계: 빈 상자를 만들어요 new를 쓰면 제일 먼저 텅 빈 새 객체가 만들어져요. 마치 아무것도 담기지 않은 새 상자를 꺼내는 것과 같아요.

🔗 2단계: 설계도(프로토타입)를 연결해요 새로 만든 빈 상자에 ‘이 상자는 어떤 기능을 쓸 수 있어’라는 설계도를 연결해요. 이 덕분에 나중에 만들어진 물건들도 공통 기능을 모두 쓸 수 있답니다.

⚙️ 3단계: 내용물을 채워요 빈 상자를 this라고 부르며, 함수 안의 코드가 실행되면서 상자 안에 필요한 내용물이 채워져요. this.이름 = '철수'처럼 값이 하나씩 담기는 거예요.

🎁 4단계: 완성된 상자를 돌려줘요 내용물이 다 채워진 상자를 최종 결과물로 돌려줘요. 이게 바로 우리가 받게 되는 새 인스턴스(새로 만든 객체)예요.

중급

new 키워드로 함수를 호출하면 JavaScript 엔진은 다음 4단계를 자동으로 수행합니다.

  1. 빈 객체 생성: Object.create(Constructor.prototype)과 동등한 빈 객체를 생성합니다.
  2. 프로토타입 연결: 새 객체의 [[Prototype]]을 생성자 함수의 prototype 프로퍼티에 연결합니다.
  3. this 바인딩 후 함수 실행: 새 객체를 this로 설정하고 함수 본문을 실행합니다.
  4. 객체 반환: 함수가 명시적으로 다른 객체를 반환하지 않으면 새로 생성된 객체를 반환합니다.
function Person(name, age) {
  this.name = name; // 3단계: this(새 객체)에 프로퍼티 추가
  this.age = age;
}

const p = new Person('철수', 20);
// 1단계: 빈 객체 {} 생성
// 2단계: {}.__proto__ = Person.prototype 연결
// 3단계: this={}로 Person('철수', 20) 실행
// 4단계: 완성된 {name:'철수', age:20} 반환

console.log(p.name); // '철수'
console.log(p instanceof Person); // true (프로토타입 연결 확인)
function manualNew(Constructor, ...args) {
  const obj = Object.create(Constructor.prototype); // 1+2단계
  const result = Constructor.apply(obj, args);      // 3단계
  return result instanceof Object ? result : obj;   // 4단계
}

const p2 = manualNew(Person, '영희', 25);
console.log(p2.name); // '영희'
console.log(p2 instanceof Person); // true

심화

new 연산자의 동작은 ECMAScript 명세 Section 13.5.6.1 EvaluateNew와 Section 10.2.2 [[Construct]] 내부 메서드에 의해 정의됩니다. ECMAScript에서 new Constructor(...args)[[Construct]](args, Constructor) 내부 메서드 호출로 변환됩니다.

ECMAScript [[Construct]] 내부 메서드 OrdinaryConstruct(F, argumentsList, newTarget) 추상 연산(Section 10.2.2)은 다음 순서로 실행됩니다. 먼저 OrdinaryObjectCreate(F.[[Prototype]]) 호출로 새 객체를 생성하고, 이 객체를 thisArgument로 설정합니다. 이후 F.[[Call]](thisArgument, argumentsList)로 함수 본문을 실행하고, 반환값이 Object 타입이면 그 값을, 아니면 thisArgument를 반환합니다. 이 과정에서 new.targetnewTarget 파라미터로 전달되어 재귀적 생성자 호출 여부를 추적합니다.

프로토타입 체인 설정의 세부 동작 OrdinaryObjectCreate에 전달되는 프로토타입은 F.[[Prototype]]이 아니라 F.prototype 프로퍼티입니다. ECMAScript Section 10.1.13에 따르면, F.prototype이 Object가 아닌 경우(예: 원시값으로 덮어씌워진 경우) %Object.prototype%이 대신 사용됩니다. 이 예외 처리 덕분에 생성자의 prototype을 실수로 원시값으로 할당해도 최소한 Object.prototype 상속은 유지됩니다.

V8 엔진의 Hidden Class 최적화와 생성자 패턴 V8에서 new로 생성된 인스턴스는 생성자 함수마다 고유한 초기 Hidden Class(Internal Map)을 공유합니다. 생성자 함수 내에서 항상 동일한 순서로 프로퍼티를 설정하면 모든 인스턴스가 동일한 Hidden Class 전이(transition) 경로를 따르므로, 프로퍼티 접근 시 Monomorphic IC(단형 인라인 캐시)가 형성되어 최적 성능을 냅니다. 생성자 본문에서 조건부로 프로퍼티를 추가하거나 순서를 바꾸면 Hidden Class가 분기되어 Polymorphic 또는 Megamorphic IC로 저하됩니다.

생성자 함수의 반환값 규칙

입문

생성자 함수에서 무언가를 반환(return)하면 어떻게 될까요? 상황에 따라 결과가 달라지는 특별한 규칙이 있어요. 이 규칙을 모르면 나중에 의도치 않은 결과가 생길 수 있답니다!

🎁 원시값을 돌려줘도 무시돼요 숫자, 문자열, 참/거짓(불리언)처럼 단순한 값을 return해도 new는 이를 무시해요. 대신 원래 계획대로 새로 만들어진 객체를 돌려줍니다. 마치 공장에서 “이 제품 말고 종이 한 장 드릴게요” 해도, 공장 규칙상 무조건 완성품을 내보내는 것처럼요.

🚨 객체를 돌려주면 그 객체가 결과예요 그런데 {}처럼 중괄호로 만든 객체나 배열을 return하면 이야기가 달라져요. 이때는 새로 만든 객체 대신 return한 그 객체가 결과로 나와요. 이 경우엔 new가 만들었던 새 객체가 사라져버려요!

💡 왜 이런 규칙이 있을까요? 이 규칙은 생성자 함수가 팩토리(공장) 역할을 할 수 있도록 설계된 거예요. 특별한 경우에만 다른 객체를 반환하고, 보통은 자동으로 새 객체를 반환하게 해서 사용하기 편리하게 만들어져 있어요.

🔍 실수를 막는 방법 이 규칙 때문에 생성자 함수에서 return을 쓸 때는 항상 주의해야 해요. 실수로 객체를 return하면 new로 만든 객체가 아닌 다른 것이 반환되어 버그가 생길 수 있거든요.

중급

생성자 함수의 반환값은 타입에 따라 다르게 처리됩니다.

  • 원시값 반환: new 바인딩이 우선합니다. 반환된 원시값은 무시되고 새로 생성된 객체가 반환됩니다.
  • 객체 반환: 반환된 객체가 우선합니다. 새로 생성된 객체 대신 명시적으로 반환된 객체가 결과가 됩니다. 이때 new 바인딩이 무효화되는 예외 상황이 발생합니다.

이 규칙은 생성자 함수를 팩토리 패턴처럼 활용할 수 있게 해주지만, 의도치 않게 객체를 반환하면 인스턴스 생성이 깨지는 버그로 이어집니다.

function Person(name) {
  this.name = name;
  return 42; // 원시값 반환 → 무시됨
}

const p = new Person('철수');
console.log(p.name); // '철수' - 새 객체가 정상 반환됨
console.log(p instanceof Person); // true
const externalObj = { type: 'external' };

function Person(name) {
  this.name = name;
  return externalObj; // 객체 반환 → 새 객체 대신 이게 반환됨
}

const p = new Person('철수');
console.log(p.name);           // undefined - externalObj에는 name이 없음
console.log(p === externalObj); // true - externalObj가 그대로 반환됨
console.log(p instanceof Person); // false - 프로토타입 연결 없음
function createUser(role) {
  if (role === 'admin') {
    return new AdminUser(); // 의도적으로 다른 객체 반환
  }
  this.role = role; // 일반 new 바인딩
}

심화

생성자 함수의 반환값 처리 규칙은 ECMAScript Section 10.2.2 OrdinaryConstruct의 마지막 단계에 명시됩니다. thisBindingStatus가 설정된 후 함수 본문이 실행되고 반환값 completionRecord를 평가할 때, Type(completionRecord.[[Value]]) === Object이면 해당 값을 반환하고, 그렇지 않으면 thisArgument(새로 생성된 객체)를 반환합니다.

반환값 타입 판별의 정확한 기준 ECMAScript Section 6.1에 따르면 Object 타입에는 일반 객체뿐 아니라 함수, 배열, 정규식, Date, Map, Set 등 모든 참조 타입이 포함됩니다. 따라서 return [], return function(){}, return new Date() 모두 새 객체 대신 반환된 값이 최종 결과가 됩니다. 반면 null은 typeof 연산에서 ‘object’를 반환하지만 ECMAScript 타입 시스템에서는 Null 타입이므로 원시값처럼 취급되어 새 객체가 반환됩니다.

instanceof 체크와의 관계 객체를 명시적으로 반환하면 반환된 객체의 [[Prototype]]은 생성자의 prototype이 아닌 자신의 원래 프로토타입을 유지합니다. 따라서 instanceof 검사가 실패하며, Object.getPrototypeOf(result) !== Constructor.prototype이 됩니다. 이는 타입 안전성 검증에 의존하는 코드에서 런타임 오류로 이어질 수 있어, 의도적 팩토리 패턴 외에는 생성자에서 객체를 반환하는 것을 강력히 지양해야 합니다.

TypeScript의 생성자 반환값 제약 TypeScript 컴파일러는 생성자(constructor)에서 원시값 반환을 컴파일 타임에 금지합니다. 객체 반환도 반환 타입이 인스턴스 타입과 호환되어야 허용됩니다. 이는 런타임 ECMAScript 명세의 반환 규칙을 타입 시스템 수준에서 보완하는 설계 결정으로, 실수로 인한 바인딩 무효화를 정적 분석 단계에서 차단합니다.

new 바인딩 vs 명시적 바인딩 우선순위

입문

bind로 this를 꽉 묶어둔 함수라도, new로 호출하면 어떻게 될까요? 놀랍게도 new가 더 강해서 bind의 약속을 무시해버린답니다! 이게 바로 우선순위예요.

🥊 두 규칙이 충돌하면? bind는 “이 함수의 this는 무조건 A야!”라고 약속한 거예요. 그런데 new는 “아니, 나는 항상 새 객체를 만들고 그걸 this로 써!”라는 규칙이 있어요. 이 둘이 충돌하면 new가 이깁니다.

🏆 우선순위 순서가 있어요 this를 결정하는 규칙에는 순위가 있어요. 제일 강한 것부터 나열하면: new로 호출 > call/apply/bind로 직접 지정 > 점(.)으로 메서드 호출 > 그냥 함수 호출 순서예요. new가 최강자예요!

🤔 왜 new가 더 강할까요? new의 핵심 목적은 ‘새 객체를 만들어서 그것을 this로 쓰는 것’이에요. 이 목적이 달성되지 않으면 new의 의미 자체가 사라지기 때문에, 어떤 바인딩보다 우선시하도록 설계된 거예요.

💡 실수를 예방하는 지식 bind로 고정된 함수를 실수로 new로 호출해도 예측 가능한 결과가 나와요. new가 항상 이기기 때문에 새 객체가 만들어집니다. 이 우선순위를 알면 복잡한 코드에서도 this가 무엇인지 정확히 예측할 수 있어요.

중급

bindthis를 고정한 함수라도 new로 호출하면 바인딩된 this는 무시되고, 새로 생성된 객체가 this로 사용됩니다. 이것이 new 바인딩이 명시적 바인딩보다 높은 우선순위를 갖는다는 의미입니다.

this 바인딩 우선순위 (높은 순서)

  1. new 바인딩 - new Fn()
  2. 명시적 바인딩 - fn.call(obj), fn.apply(obj), fn.bind(obj)()
  3. 암묵적 바인딩 - obj.fn()
  4. 기본 바인딩 - fn() (전역 또는 undefined)

이 우선순위는 bind를 활용한 부분 적용(partial application) 패턴과 생성자 패턴을 함께 사용할 때 중요한 의미를 가집니다.

function User(name) {
  this.name = name;
}

const boundFn = User.bind({ name: 'FORCED' }); // this를 고정된 객체로 bind

const u1 = boundFn();         // 일반 호출: this = { name: 'FORCED' }
const u2 = new boundFn('철수'); // new 호출: bind의 this 무시, 새 객체가 this

console.log(u1); // undefined - 전역이나 고정 객체에 name 설정
console.log(u2.name); // '철수' - 새 객체에 name 설정됨
console.log(u2 instanceof User); // true - 원본 User의 prototype 사용
function Point(x, y) {
  this.x = x;
  this.y = y;
}

// bind로 x를 0으로 고정 (부분 적용)
const PointOnYAxis = Point.bind(null, 0);

const p = new PointOnYAxis(5); // x=0(부분 적용 유지), y=5, this=새 객체
console.log(p.x); // 0 - 부분 적용된 인수는 유지됨
console.log(p.y); // 5
console.log(p instanceof Point); // true

심화

new 바인딩이 명시적 바인딩보다 우선하는 메커니즘은 ECMAScript Section 10.4.1.1 BoundFunctionExoticObject[[Construct]] 내부 메서드 구현에 명확히 나타납니다. bind가 반환하는 함수는 일반 함수가 아닌 Bound Function Exotic Object입니다.

Bound Function Exotic Object의 [[Construct]] 명세 ECMAScript Section 10.4.1.2에 따르면, Bound Function의 [[Construct]](argumentsList, newTarget) 호출 시 newTarget이 Bound Function 자신인 경우 newTarget을 원본 함수([[BoundTargetFunction]])로 교체한 뒤 원본 함수의 [[Construct]]를 호출합니다. 이 과정에서 [[BoundThis]]는 완전히 무시됩니다. 즉, bind의 this 고정은 [[Call]] 경로에서만 유효하고 [[Construct]] 경로에서는 작동하지 않도록 명세 자체에 설계되어 있습니다.

부분 적용 인수의 보존 new로 Bound Function을 호출할 때 [[BoundArguments]]는 무시되지 않고 새 인수 목록의 앞에 prepend됩니다(Section 10.4.1.2, step 5). 이것이 new boundFn(y)에서 x가 bound된 값으로 고정되면서도 새 객체가 this로 사용되는 이유입니다. 이는 부분 적용 패턴과 생성자 패턴을 합성할 때 의도적으로 활용할 수 있는 강력한 특성입니다.

instanceof 체크와 prototype 연결 new로 Bound Function을 호출하면 instanceof 판별은 원본 함수([[BoundTargetFunction]])의 prototype을 기준으로 수행됩니다. ECMAScript Section 22.2.3 OrdinaryHasInstance가 프로토타입 체인을 탐색할 때 Bound Function을 unwrap하기 때문입니다. 따라서 new BoundFn() instanceof OriginalFntrue를 반환하며, 바인딩 여부와 무관하게 타입 체크가 일관되게 동작합니다.

new.target과 생성자 호출 감지

입문

함수를 만들 때 누군가 new 없이 실수로 그냥 호출하면 어떻게 될까요? new.target이라는 특별한 마법 도구를 쓰면 “지금 new로 불렸어? 아니면 그냥 불렸어?”를 알아낼 수 있답니다!

🔍 탐정처럼 호출 방식을 알아내요 함수 안에서 new.target이라는 걸 확인하면, 이 함수가 지금 new로 호출되었는지 알 수 있어요. new로 호출되면 함수 자신을 가리키고, 그냥 호출되면 아무것도 없는 상태(undefined)가 돼요.

🚫 실수 방지 문지기 “이 함수는 반드시 new로 써야 해!”라고 강제할 수 있어요. new.target이 없으면(그냥 호출이면) “잘못 사용했어요!”라고 알려주거나, 자동으로 new를 붙여서 다시 실행하게 만들 수 있답니다.

🏛️ 추상 클래스 흉내내기 “이 설계도는 직접 쓰면 안 되고, 반드시 발전된 버전에서만 써야 해!”를 구현할 수도 있어요. 부모 클래스에서 new.target이 부모 자신을 가리키면 오류를 내면서 직접 사용을 막을 수 있거든요.

📍 어디서든 확인할 수 있어요 new.target은 함수 안 어디서든 확인할 수 있어요. 생성자 함수 맨 위에서 확인하면 잘못된 사용을 일찍 발견해서 알려줄 수 있답니다.

중급

new.target은 함수가 new 연산자로 호출되었는지 감지하는 메타 프로퍼티(meta property)입니다. ES6에서 도입되었으며 함수 본문과 생성자 안에서만 접근 가능합니다.

  • new로 호출 시: new.target은 호출된 생성자 함수(또는 클래스) 자신을 가리킵니다.
  • 일반 호출 시: new.targetundefined입니다.
  • 상속된 생성자에서: new.targetnew로 호출된 가장 파생된 클래스를 가리킵니다.

new.target의 주요 활용 패턴으로는 new 강제(guard), 추상 클래스 구현, 팩토리 패턴이 있습니다.

function User(name) {
  // new 없이 호출하면 자동으로 new를 붙여 재실행
  if (!new.target) {
    return new User(name);
  }
  this.name = name;
}

const u1 = new User('철수');  // 정상: new.target = User
const u2 = User('영희');      // new 없이 호출 → 내부에서 new User('영희') 실행
console.log(u2 instanceof User); // true
class Shape {
  constructor() {
    // Shape를 직접 인스턴스화하면 오류
    if (new.target === Shape) {
      throw new Error('Shape는 추상 클래스입니다. 직접 인스턴스화할 수 없습니다.');
    }
  }
}

class Circle extends Shape {
  constructor(radius) {
    super(); // new.target = Circle → 오류 없이 통과
    this.radius = radius;
  }
}

// new Shape(); // Error: Shape는 추상 클래스입니다.
const c = new Circle(5); // 정상 동작

심화

new.target은 ECMAScript Section 13.3.12에 정의된 메타 프로퍼티(MetaProperty)입니다. 일반 프로퍼티 접근과 달리 파서 수준에서 특별하게 처리되는 구문으로, 점(.) 연산자가 아닌 키워드처럼 동작합니다.

실행 컨텍스트와 [[NewTarget]] 슬롯 new.target은 실행 컨텍스트(Execution Context)의 [[NewTarget]] 슬롯에서 값을 읽습니다. ECMAScript Section 9.4 GetNewTarget() 추상 연산에 따르면, 현재 실행 컨텍스트의 Function Environment Record에서 [[NewTarget]]을 가져옵니다. [[Construct]] 경로로 호출되면 newTarget 파라미터가 이 슬롯에 저장되고, [[Call]] 경로로 호출되면 undefined가 저장됩니다.

상속 체계에서의 new.target 동작 super() 호출 시 [[NewTarget]]은 파생 클래스(derived class)에서 기반 클래스(base class) 생성자로 그대로 전달됩니다(Section 10.2.1.1). 따라서 추상 클래스 패턴에서 new.target === BaseClass를 검사하면 파생 클래스의 생성자에서는 new.target이 파생 클래스를 가리키므로 통과하고, 기반 클래스를 직접 인스턴스화할 때만 오류가 발생합니다. 이 메커니즘은 인터페이스 강제나 템플릿 메서드 패턴 구현의 기반이 됩니다.

화살표 함수와 new.target 화살표 함수는 자체 실행 컨텍스트를 생성하지 않으므로(lexical this와 동일한 원리) new.target도 렉시컬로 결정됩니다. 화살표 함수 안의 new.target은 화살표 함수를 감싸는 가장 가까운 일반 함수의 [[NewTarget]]을 참조합니다. 또한 화살표 함수는 [[Construct]] 내부 메서드가 없으므로 new로 호출하면 TypeError: ... is not a constructor가 발생합니다.