Ayden's journal

프로토타입 오버라이드

프로토타입 오버라이드는 JavaScript에서 객체 지향 프로그래밍을 할 때 객체의 동작을 커스터마이징하거나 확장하기 위해 사용하는 기법 중 하나다. JavaScript의 모든 객체는 프로토타입 체인을 통해 부모 객체의 속성과 메서드를 상속받는데, 이때 기본 제공되는 객체의 프로토타입에 새로운 메서드를 추가하거나 기존 메서드를 수정하는 것을 프로토타입 오버라이드라고 부른다. 예를 들어, Array나 String 같은 빌트인 객체의 프로토타입에 새로운 메서드를 추가하면, 해당 타입의 모든 인스턴스에서 이를 사용할 수 있는 것이다.

프로토타입 오버라이드는 프로젝트 전반에 걸쳐 상당히 강력한 영향력을 행사한다. 올바르게 사용하면 중복 코드를 줄이고 개발 생산성을 높일 수 있지만, 잘못 사용하면 예기치 못한 동작이나 디버깅이 어려운 버그를 유발한다. 특히, 팀 기반 개발 환경에서는 프로토타입을 수정하는 코드가 다른 팀원의 코드와 충돌을 일으킬 가능성이 크기 때문에, 이를 사용할 때는 반드시 문서화와 사전 논의가 필요하다.

 

내 경우에는 특히 큐와 스택을 다루는 메서드의 동작을 자주 수정하는 편이다. 이는 개인적인 개발 편의에 의한 것인데, 나는 push나 unshift를 했을 때 수정된 배열이 아니라 배열의 길이가 리턴되는 것이 마음에 들지 않는다. 필요하다면 메서드 체이닝을 통해 배열 값을 조회할 수 있기 때문이다. 그래서 나는 아래와 같이 프로토타입을 오버라이드 하기로 했다.

declare global {
  interface Array<T> {
    pull(item: T): this;
    push(item: T): this;
  }
}

Array.prototype.pull = function(item) {
  this.unshift(item);
  return this;
}

Array.prototype.push = function(item) {
    this[this.length] = item;
    return this;
}

첫 번째 오버라이드인 pull 메서드의 경우에는 push의 반대말이 unshift인 것이 영 기억에 잘 남지 않고, 앞서 설명한 것처럼 배열의 길이 대신 배열을 리턴했으면 좋겠어서 새로운 메서드를 만들어 프로토타입에 추가한 것이다. 두 번째 오버라이드인 push 메서드의 경우에는 이미 존재하는 메서드의 동작을 바꾸는 것이다.

그런데 아뿔싸! 내가 push 메서드를 오버라이드 하니까 이 메서드를 사용하는 모든 라이브러리가 오작동을 하기 시작했다. 따라서 기존 메서드를 덮어써야 한다면, 내부 동작을 변경하더라도 변경 이전과 이후가 타입적으로 동일함을 보장해야 한다. 실제 프로젝트에서 나는 오직 새로운 메서드를 추가하는 방식으로만 프로토타입 오버라이드를 사용하고 있다.

 

Queue에서 아이템을 제거하려면 리스트의 맨 앞 아이템을 없애야 한다. 그런데 그 메서드가 unshift였는지 shift 였는지를 나는 늘 햇갈린다. 따라서 의미론(semantics)적으로 코드를 더 직관적으로 만들기 위해 나는 queueDelete와 같은 메서드를 여럿 만들어서 프로토타입에 추가하였다.

// global.d.ts
export {};

declare global {
  interface Array<T> {
    queueAdd(item: T): this;
    queueDelete(): T | undefined;
    stackAdd(item: T): this;
    stackDelete(): T | undefined;
  }
}

// arrayPrototypeOverride.ts
Array.prototype.queueAdd = function<T>(this: T[], item: T): T[] {
  this.push(item);
  return this;
};

Array.prototype.queueDelete = function<T>(this: T[]): T | undefined {
  return this.shift();
};

Array.prototype.stackAdd = function<T>(this: T[], item: T): T[] {
  this.push(item);
  return this;
};

Array.prototype.stackDelete = function<T>(this: T[]):  T | undefined {
  return this.pop();
};

 

arrayPrototypeOverride 파일을 import 하면 프로토타입 오버라이드 메서드를 사용할 수 있다. 일반적으로 오버라이드 파일은 프로젝트의 진입점(예: index.ts, main.ts)에 import 하는 것을 권장한다. 이렇게 하면 프로토타입 오버라이드가 프로토타입 오버라이드 메서드를 사용하는 코드보다 무조건 먼저 실행되기 때문이다. 만약 이 반대가 되면 정의되지 않은 메서드에 접근하며 런타임 에러가 발생하게 된다.

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기