PUBLISHED

this

작성일: 2024.12.13

this

내가 혼자 개발하고 있는 onef에 댓글 기능을 추가하기로 했다. 댓글 작성 기능을 구현한 뒤 수정 기능이 필요하다는 사실을 깨달았을 때, 처음 든 생각은 “로직은 거의 같고 메서드만 바꾸면 되겠네”였다. 실제로 댓글 작성과 수정은 HTTP 메서드만 post와 put으로 다를 뿐, 요청 구조나 성공 이후의 동작은 동일했다. 그래서 중복을 줄이기 위해 댓글 작성 컴포넌트에서 mutation 로직을 분리하고, 필요한 동작을 외부에서 주입받는 구조로 바꾸기로 했다. 이때 선택한 방법이 Context였다.

Context를 통해 postComment와 putComment 중 하나를 내려주고, 컴포넌트에서는 그 함수만 호출하도록 설계했다. 이론적으로는 전혀 문제가 없어 보였다. 실제로 Context를 통해 내려보낸 함수들을 콘솔에 찍어보아도 정상적인 함수였고, 타입 역시 의도한 대로 유지되고 있었다.

CommentMutation.ts
TS
export class CommentMutation extends MutationFn {
  constructor() {
    super();
  }

  postComment() {
    return ({
      parentId,
      value,
      depth,
    }: {
      parentId: string;
      value: string;
      depth: number;
    }) =>
      this.mutationFn<{ id: string }>(
        `/comments/${parentId}`,
        "post",
        { comment: value, depth },
      );
  }

  putComment() {
    return ({
      parentId,
      value,
      depth,
    }: {
      parentId: string;
      value: string;
      depth: number;
    }) =>
      this.mutationFn<{ id: string }>(
        `/comments/${parentId}`,
        "put",
        { comment: value, depth },
      );
  }
}
CommentMutationContext.ts
TS
export const CommentMutationContext = createContext<{
  mutationFn: typeof commentMutation.postComment;
  parentId: string;
  onSuccessBehavior?: () => void;
}>({
  parentId: "",
  mutationFn: commentMutation.postComment,
});
useCommentMutation.ts
TS
const useCommentMutation = ({ ... }: { ... }) => {
  const queryClient = useQueryClient();

  const {
    parentId,
    mutationFn,
    onSuccessBehavior,
  } = useContext(CommentMutationContext);

  const { mutate, isPending } = useMutation({
    mutationFn: mutationFn(),
    onSuccess: () => {
      setValue("");

      if (onSuccessBehavior) {
        onSuccessBehavior();
      }

      queryClient.invalidateQueries({ queryKey: ["comment"] });
    },
  });

  return { mutate };
};

mutate result undefined

그런데 결과는 완전히 예상 밖이었다. 댓글 수정 기능이 동작하지 않는 것은 물론이고, 기존에 잘 되던 댓글 작성 기능까지 같이 죽어버렸다. Context를 사용하기 전에는 아무 문제도 없었기 때문에, 원인은 거의 확실하게 Context를 도입한 변경점 어딘가에 있다고 판단했다. 하지만 Context를 사용해서 내려보낸 postComment 함수와 putComment 함수를 콘솔에 찍어보니 별 문제가 없어보였다.

다음으로 의심한 것은 React Query였다. 요청이 실패하면 onError나 onSettled가 실행되어야 하는데, 실제로는 onSuccess만 호출되지 않았다. 하지만 네트워크 탭을 확인해보니 더 이상했다. 요청이 실패한 것이 아니라, 요청 자체가 아예 보내지지 않고 있었다. 성공도 실패도 아닌 상태, 말 그대로 아무 일도 일어나지 않은 것이다.

이쯤 되면 mutation 함수 자체가 실행되지 않는다고 봐야 했다. 그래서 submit 로직부터 mutation 함수, Context에서 내려오는 값까지 하나씩 전부 콘솔에 찍어가며 확인하기 시작했다. 그러다 아주 결정적인 이상함을 발견했다. mutate를 호출한 결과가 undefined였던 것이다.

왜 이런 일이 발생했을까?

React Query에서 useMutation은 전달받은 mutationFn을 내부적으로 감싸서 mutate라는 메서드로 반환한다. 따라서 mutationFn이 정상적인 함수라면, mutate 역시 동일한 동작을 해야 한다. 이 구조를 그대로 풀어 쓰면, mutate는 결국 postComment가 반환한 함수와 동일하다고 볼 수 있다.

CommentMutation.ts
TS
export class CommentMutation extends MutationFn {
  constructor() {
    super();
  }

  postComment() {
    return ({
      parentId,
      value,
      depth,
    }: {
      parentId: string;
      value: string;
      depth: number;
    }) =>
      this.mutationFn<{ id: string }>(
        `/comments/${parentId}`,
        "post",
        { comment: value, depth },
      );
  }
}

// useCommentMutation.ts
const {
  parentId,
  mutationFn,
  onSuccessBehavior,
} = useContext(CommentMutationContext);

const { mutate, isPending } = useMutation({
  mutationFn: mutationFn(),
});

// 따라서 mutate 메소드는
// ({
//   parentId,
//   value,
//   depth,
// }: {
//   parentId: string;
//   value: string;
//   depth: number;
// }) =>
//   this.mutationFn<{ id: string }>(
//     `/comments/${parentId}`,
//     "post",
//     { comment: value, depth },
//   );
// 와 같다

그렇다면 mutate가 실행되었을 때 이 내부 로직이 수행되어야 한다. 하지만 실제로는 아무것도 실행되지 않았고, 그 결과가 undefined였다. 여기서 핵심적인 질문이 생긴다. 이 코드에서의 this는 과연 무엇인가?

나는 자연스럽게 thisCommentMutation 클래스의 인스턴스를 가리키고 있을 것이라고 생각했다. 하지만 그 가정이 틀렸다는 사실을 깨닫는 데까지 꽤 시간이 걸렸다. 문제는 Context를 사용하는 순간, 이 메서드가 더 이상 “메서드”가 아니라 “독립적인 함수”가 되어버린다는 점이었다.

untitled
XML
<CommentMutationContext.Provider
  value={{
    parentId: id,
    mutationFn: commentMutation.postComment,
  }}
>

이 코드는 얼핏 보면 아무 문제 없어 보인다. 하지만 commentMutation.postComment를 그대로 넘기는 순간, 자바스크립트 입장에서는 객체와의 연결이 끊어진 함수 레퍼런스가 된다. 즉, 이 함수는 더 이상 commentMutation에 속한 메서드가 아니다.

따라서 mutationFn() 호출은 어떤 객체에도 속하지 않은 상태로 실행된다. 자바스크립트에서 이런 호출 방식은 this 바인딩을 완전히 잃는다. strict mode라면 thisundefined가 되고, 그렇지 않다면 전역 객체를 가리키게 된다. 어느 쪽이든 this.mutationFn 같은 코드는 정상적으로 동작할 수 없다.

결국 useMutation({ mutationFn: mutationFn() })는 내부적으로 useMutation({ mutationFn: undefined })가 되었고, React Query는 실행할 함수가 없는 상태가 되어버린 것이다. 그래서 네트워크 요청도, 성공 콜백도, 실패 콜백도 전부 발생하지 않았다.

this 바인딩을 고정하는 방법

이 문제를 해결하는 방법은 크게 두 가지였다. 하나는 bind를 사용해 명시적으로 this를 고정하는 방식이다. 이 방식은 확실하게 동작하지만, 매번 바인딩을 신경 써야 하고 코드가 다소 지저분해질 수 있다. 특히 Context나 props로 메서드를 자주 넘기는 구조라면 유지보수 부담이 커진다. 그래서 선택한 방식이 두 번째 방법, 즉 메서드를 화살표 함수로 선언하는 것이었다.

untitled
TS
export class CommentMutation extends MutationFn {
  postComment = () => {
    return ({
      parentId,
      value,
      depth,
    }: {
      parentId: string;
      value: string;
      depth: number;
    }) =>
      this.mutationFn(
        `/comments/${parentId}`,
        "post",
        { comment: value, depth },
      );
  };
}

화살표 함수는 실행 컨텍스트가 생성될 때 this 바인딩을 하지 않는다. 대신 선언 당시의 상위 스코프에 있는 this를 그대로 캡처한다. 클래스 필드로 선언된 화살표 함수의 경우, 그 this는 항상 인스턴스를 가리킨다. 따라서 Context를 통해 함수가 분리되어 전달되더라도 this가 깨지지 않는다.

이 방식은 별도의 바인딩 코드가 필요 없고, 의도 역시 훨씬 명확하다. “이 메서드는 항상 이 인스턴스를 참조해야 한다”는 사실이 코드 레벨에서 드러난다.

다만 화살표 함수가 언제나 정답은 아니다. 객체 리터럴에서 화살표 함수로 메서드를 정의하면 오히려 문제가 생긴다.

untitled
JS
const a = {
  a: 1,
  setA: (value) => {
    this.a = value;
  },
};

여기서 this는 객체 a가 아니라 상위 스코프의 this, 즉 전역 객체를 가리킨다. 화살표 함수는 메서드를 만들기 위한 도구가 아니라, this를 고정해야 하는 함수를 만들기 위한 도구라는 점을 항상 염두에 두어야 한다.

이번 에러의 원인은 React도, Context도, React Query도 아니었다. 전부 자바스크립트의 this 바인딩 규칙으로 귀결된다. 클래스 메서드를 함수처럼 다루는 순간, 자바스크립트는 가차 없이 this를 끊어낸다.

“필요할 때 찾아보면 되지”라는 수준의 이해로는 이런 문제를 빠르게 해결하기 어렵다. 이번 경험을 통해 this를 단순한 키워드가 아니라, 호출 방식의 결과물로 정확히 이해해야 한다는 확신이 들었다. 언젠가가 아니라, 지금쯤은 정말 제대로 정리해둘 필요가 있는 개념이다.

자바스크립트에서의 this binding

자바스크립트에서 this를 이해하기 어려운 가장 큰 이유는, this가 단 하나의 규칙으로 결정되지 않기 때문이다. this는 “함수 선언 방식”이 아니라 함수가 호출되는 순간의 상황에 따라 서로 다른 규칙을 적용받는다. 그리고 이 규칙들은 우선순위를 가지며, 특정 상황에서는 이전 규칙을 덮어쓴다.

1. 기본 바인딩 (Default Binding)

가장 먼저 적용되는 규칙은 기본 바인딩이다. 어떤 함수가 아무 객체에도 속하지 않은 채로 호출되면, 이 함수의 this는 기본 바인딩 규칙을 따른다.

untitled
TSX
function foo() {
  console.log(this);
}

foo();

이 코드는 겉보기에는 단순하지만, 실행 환경에 따라 결과가 달라진다. strict mode가 아닌 경우 this는 전역 객체(window, global)를 가리키고, strict mode에서는 undefined가 된다. 중요한 점은, 어느 쪽이든 의도한 객체를 가리키지 않는다는 사실이다.

이번 문제에서 mutationFn() 호출이 바로 이 케이스였다. Context를 통해 전달된 함수는 더 이상 어떤 객체의 메서드도 아니었고, 결과적으로 기본 바인딩이 적용되었다. 그 순간 this는 인스턴스를 잃었고, 내부의 this.mutationFn은 의미 없는 접근이 되어버렸다.

2. 암시적 바인딩 (Implicit Binding)

기본 바인딩 다음으로 가장 흔하게 등장하는 규칙이 암시적 바인딩이다. 함수가 객체의 프로퍼티로서 호출되면, this는 그 객체에 바인딩된다.

untitled
JS
const obj = {
  value: 1,
  print() {
    console.log(this.value);
  },
};

obj.print(); // 1

여기서 중요한 것은 함수가 어디에 정의되었느냐가 아니라, 어떤 형태로 호출되었느냐이다. printobj의 프로퍼티로 호출되었기 때문에 thisobj를 가리킨다.

클래스 메서드도 동일하다.

untitled
TSX
commentMutation.postComment();

이 호출이 유지되는 한, this는 항상 commentMutation 인스턴스를 가리킨다. 문제는 이 호출 형태가 Context를 거치며 깨졌다는 점이다.

3. 암시적 바인딩의 손실 (Implicit Binding Loss)

자바스크립트에서 this 버그의 상당수는 이 규칙에서 발생한다. 암시적 바인딩은 아주 쉽게 깨진다.

untitled
JS
const obj = {
  value: 1,
  print() {
    console.log(this.value);
  },
};

const print = obj.print;
print(); // undefined

print는 여전히 같은 함수다. 하지만 호출 시점에 더 이상 obj.print() 형태가 아니기 때문에, 암시적 바인딩이 적용되지 않는다. 이 순간 기본 바인딩으로 떨어진다.

이번 Context 문제는 이 패턴과 구조적으로 완전히 동일하다.

untitled
TSX
mutationFn: commentMutation.postComment

이 코드는 메서드를 “전달”하는 것처럼 보이지만, 실제로는 메서드에서 객체를 분리해 함수 레퍼런스만 전달한다. 이후 mutationFn()으로 호출되는 순간, 암시적 바인딩은 이미 사라진 상태다.

4. 명시적 바인딩: call / apply

자바스크립트는 이런 문제를 해결하기 위해 명시적 바인딩 수단을 제공한다. callapply는 함수를 호출하면서 this를 직접 지정한다.

untitled
TSX
function foo() {
  console.log(this.value);
}

const obj = { value: 1 };

foo.call(obj);   // 1
foo.apply(obj);  // 1

이 방식은 호출 시점에 강제로 this를 지정하기 때문에, 기본 바인딩이나 암시적 바인딩보다 우선한다. Context를 사용하는 구조에서도 이 방식은 유효하다. 다만 매번 호출부에서 call이나 apply를 사용해야 하기 때문에 실전 코드에서는 번거롭다.

5. bind와 바인딩된 함수

bindcall, apply와 달리 함수를 즉시 실행하지 않는다. 대신 this가 고정된 새로운 함수를 반환한다.

untitled
JS
function foo() {
  console.log(this.value);
}

const obj = { value: 1 };
const boundFoo = foo.bind(obj);

boundFoo(); // 1

이 함수는 이후 어떤 방식으로 호출되든 this가 바뀌지 않는다. Context나 props로 함수를 넘기는 상황에서는 매우 강력한 해결책이다. 다만 이 함수는 원본 함수와 다른 함수이며, 내부적으로 [[BoundThis]], [[BoundArguments]] 같은 슬롯을 가진다. 이 부분은 다음 파트에서 더 깊게 다룰 수 있다.

6. new 바인딩

함수가 new와 함께 호출되면, 앞선 모든 규칙을 무시하고 새로운 규칙이 적용된다.

untitled
JS
function Person(name) {
  this.name = name;
}

const p = new Person("John");

이 경우 this는 새로 생성된 인스턴스를 가리킨다. 클래스 문법 역시 내부적으로는 이 규칙을 따른다. 그래서 생성자 내부의 this는 항상 인스턴스를 가리킨다.

중요한 점은 bind로 고정된 this조차 new 앞에서는 무시된다는 것이다. 즉, new 바인딩은 가장 강력한 규칙 중 하나다.

7. 화살표 함수의 this (렉시컬 바인딩)

마지막 규칙은 앞선 모든 규칙과 성격이 다르다. 화살표 함수는 아예 this 바인딩을 하지 않는다.

untitled
JS
const obj = {
  value: 1,
  print: () => {
    console.log(this.value);
  },
};

여기서 thisobj가 아니다. 화살표 함수는 선언된 위치의 상위 스코프에서 this를 캡처한다. 이 특성 때문에 클래스 필드로 선언된 화살표 함수는 항상 인스턴스를 가리킨다.

untitled
JS
class A {
  value = 1;

  print = () => {
    console.log(this.value);
  };
}

이번 문제에서 이 방식이 효과적이었던 이유는 명확하다. Context를 통해 함수가 분리되어 전달되더라도, this는 이미 렉시컬하게 고정되어 있었기 때문이다.

정리

지금까지 살펴본 여러 사례를 하나로 묶어보면, 자바스크립트에서 this는 결코 직관적인 키워드가 아니다. this는 변수도 아니고, 스코프에 의해 결정되는 값도 아니다. 대신 this함수가 호출되는 순간, 실행 컨텍스트를 만들면서 함께 결정되는 값이다. 이 말의 의미는 단순하다. 같은 함수라 하더라도 호출 방식이 달라지면, 그때마다 서로 다른 this를 가질 수 있다는 뜻이다.

이 때문에 자바스크립트 엔진은 this를 결정할 때 하나의 규칙만을 적용하지 않는다. 여러 규칙을 순차적으로 검토하면서, 해당 호출이 어떤 규칙에 해당하는지를 판별한다. 그리고 가장 먼저 매칭되는 규칙 하나만을 적용한다. 즉, this 바인딩 규칙들 사이에는 아래와 같은 명확한 우선순위가 존재한다.

이 표를 기준으로 보면, 이번 Context 사례는 명확하다. postComment는 원래 암시적 바인딩에 의해 인스턴스를 가리키고 있었지만, Context를 거치며 함수 레퍼런스로 분리되었고, 결국 기본 바인딩으로 떨어졌다. 그래서 this는 인스턴스가 아닌 undefined가 되었고, 모든 문제가 그 지점에서 시작되었다.

bind는 무엇을 고정하는가

앞에서 정리했듯이, this 바인딩 문제의 본질은 “호출 시점에 this가 바뀔 수 있다”는 데 있다. 그리고 Context, props, 콜백처럼 함수를 전달하는 구조에서는 이 문제가 거의 필연적으로 발생한다. 이런 상황에서 자바스크립트가 제공하는 가장 직접적인 해법이 바로 bind이다.

bind는 표면적으로 보면 단순하다. 함수의 this를 고정해주는 메서드처럼 보인다.

untitled
TS
function foo() {
  console.log(this.value);
}

const obj = { value: 1 };

const boundFoo = foo.bind(obj);
boundFoo(); // 1

하지만 여기서 중요한 점은, bind가 기존 함수를 “수정”하는 것이 아니라 완전히 새로운 함수를 만들어낸다는 사실이다. 이 차이를 이해하지 못하면, bind를 안전하게 쓰는 것도 어렵고, 예상치 못한 부작용을 겪게 된다.

bind가 호출되는 순간, 자바스크립트 엔진은 새로운 함수 객체를 생성한다. 이 함수는 원본 함수에 대한 참조를 내부에 유지하면서, 동시에 몇 가지 내부 슬롯을 함께 가진다. 대표적인 것이 [[BoundThis]][[BoundArguments]]이다.

이 때문에 bind는 this뿐 아니라 인자까지 미리 고정할 수 있다.

untitled
TS
function sum(a, b) {
  return a + b;
}

const add10 = sum.bind(null, 10);
add10(5); // 15

이 코드는 단순한 커링처럼 보이지만, 실제로는 [[BoundArguments]]10이 저장된 새로운 함수가 만들어진 결과다. 이후 add10을 호출하면, 자바스크립트 엔진은 내부적으로 sum(10, 5) 형태로 인자를 합쳐서 실행한다.

여기서 중요한 점은, 이렇게 만들어진 bound function은 호출 방식에 상관없이 this가 바뀌지 않는다는 것이다.

untitled
TS
const obj1 = { value: 1 };
const obj2 = { value: 2 };

function foo() {
  console.log(this.value);
}

const boundFoo = foo.bind(obj1);

boundFoo();               // 1
boundFoo.call(obj2);      // 1
boundFoo.apply(obj2);     // 1

이미 [[BoundThis]]가 결정된 함수이기 때문에, 이후의 call, apply는 아무런 영향을 주지 못한다. 이 점에서 bind는 기본 바인딩이나 암시적 바인딩보다 훨씬 강력하다. “이 함수의 this는 이것이다”라고 선언적으로 고정해버리는 셈이다.

이 특성 때문에 bind는 Context, 이벤트 핸들러, 비동기 콜백처럼 호출 주체가 사라지는 구조에서 매우 안정적인 해결책이 된다. 이번 댓글 mutation 문제를 bind로 해결한다면 다음과 같은 형태가 되었을 것이다.

untitled
TSX
<CommentMutationContext.Provider
  value={{
    parentId: id,
    mutationFn: commentMutation.postComment.bind(commentMutation),
  }}
>

이렇게 하면 postComment가 어떤 경로로 호출되든, this는 항상 commentMutation 인스턴스를 가리킨다.

하지만 bind가 항상 만능인 것은 아니다. 가장 중요한 예외가 바로 new와 함께 사용되는 경우다. 앞에서 정리했듯이, new 바인딩은 매우 강력한 규칙이다. 함수가 new와 함께 호출되면, 자바스크립트 엔진은 무조건 새로운 객체를 만들고, 그 객체를 this로 사용한다. 이 규칙은 bind보다 우선한다.

untitled
TS
function Person(name) {
  this.name = name;
}

const BoundPerson = Person.bind({ name: "ignored" });
const p = new BoundPerson("John");

console.log(p.name); // John

여기서 bind로 넘긴 { name: "ignored" }는 완전히 무시된다. 그 이유는 bound function이 내부적으로 “생성자로 호출되었는지”를 감지하기 때문이다. new와 함께 호출되면, [[BoundThis]] 대신 새로 생성된 인스턴스를 this로 사용한다. 이는 자바스크립트 설계 차원에서 의도된 동작이다.

이 지점에서 bind의 성격이 더 명확해진다. bind는 “무조건 this를 고정하는 도구”가 아니라, 일반 함수 호출 시점의 this를 고정하는 도구다. 생성자 호출이라는 의미를 침범하지는 않는다.

또 하나 짚고 넘어가야 할 점은, bind성능과 구조 측면에서 비용이 있다는 것이다. bind를 호출할 때마다 새로운 함수 객체가 생성되기 때문에, 렌더링마다 bind를 남발하면 불필요한 함수 생성이 누적될 수 있다. React에서 이벤트 핸들러를 렌더 함수 안에서 매번 bind하는 패턴이 지양되는 이유도 여기에 있다.

@thisBind 데코레이터

이 때문에 최근 코드베이스에서는 bind보다 this 바인딩을 더 상위의 구조에서 책임지는 방식이 선호된다. 그 대표적인 예가 데코레이터를 활용한 this 바인딩이다. 데코레이터를 사용하면 개별 호출 지점이나 렌더링 흐름에서 bind를 반복하는 대신, 클래스 정의 단계에서 “이 클래스의 메서드는 this-safe하다”는 정책을 선언할 수 있다.

untitled
TS

export function thisBind<T extends new (...args: any[]) => any>(
  value: T,
  context: ClassDecoratorContext,
): T | void {
  if (context.kind !== 'class')
    throw new Error('@thisBind can only be applied to classes');

  class cls extends value {
    constructor(...args: any[]) {
      super(...args);

      const isMethod = (v: PropertyDescriptor['value']): v is Function =>
        typeof v === 'function';
      const isArrowFunction = (fn: Function) => !fn.prototype;
      const bound = new Set<string>();

      let proto = value.prototype;
      while (proto && proto !== Object.prototype) {
        for (const name of Object.getOwnPropertyNames(proto)) {
          if (name === 'constructor' || bound.has(name)) continue;
          const descriptor = Object.getOwnPropertyDescriptor(proto, name);
          if (!descriptor || !isMethod(descriptor.value)) continue;

          Object.defineProperty(this, name, {
            value: descriptor.value.bind(this),
            writable: true,
            configurable: true,
            enumerable: false,
          });
          bound.add(name);
        }
        proto = Object.getPrototypeOf(proto);
      }

      for (const key of Object.getOwnPropertyNames(this)) {
        if (bound.has(key)) continue;
        const descriptor = Object.getOwnPropertyDescriptor(this, key);
        if (!descriptor?.value || !isMethod(descriptor.value)) continue;
        if (isArrowFunction(descriptor.value)) continue;

        Object.defineProperty(this, key, {
          value: descriptor.value.bind(this),
          writable: descriptor.writable,
          configurable: descriptor.configurable,
          enumerable: descriptor.enumerable,
        });
      }
    }
  }

  Object.defineProperty(cls, 'name', { value: 'thisBound' + value.name });

  return cls;
}

이 접근의 핵심은 바인딩 시점을 통제하는 데 있다. 렌더링이나 콜백 전달 과정에서 즉석으로 bind를 호출하지 않고, 인스턴스가 생성되는 시점에 한 번만 this 바인딩을 수행하거나, 필요할 때만 지연 바인딩을 수행한다. 그 결과 불필요한 함수 생성이 반복되지 않고, this 안정성 역시 구조적으로 보장된다.

또한 데코레이터 기반 바인딩은 코드의 의도를 훨씬 명확하게 드러낸다. 생성자 내부에 흩어져 있는 this.foo = this.foo.bind(this) 같은 코드를 읽지 않아도, 클래스 선언부의 데코레이터 하나만 보고도 “이 클래스는 메서드 this 바인딩을 내부적으로 처리한다”는 사실을 즉시 알 수 있다. 이는 단순한 편의성을 넘어, 유지보수성과 설계 일관성에 직접적인 영향을 준다.

무엇보다 이 방식은 화살표 함수 기반 해결책이 가지는 한계를 보완한다. 클래스 필드로 선언된 화살표 함수는 인스턴스 프로퍼티이기 때문에 데코레이터를 적용하기 어렵고, 상속이나 메타프로그래밍 관점에서도 제약이 있다. 반면 데코레이터를 사용하면 메서드를 프로토타입에 유지한 채로 this 바인딩 문제를 해결할 수 있어, 공통 로직 주입이나 횡단 관심사 처리와도 자연스럽게 결합된다.

결국 이 문제의 해법은 bind를 쓰느냐 마느냐의 문제가 아니다. this 바인딩을 언제, 어디서, 어떤 책임으로 처리할 것인가에 대한 설계의 문제다. 렌더링 흐름이나 호출부에 그 책임을 떠넘기지 않고, 클래스 자체가 책임지도록 만드는 방식으로서, 데코레이터 기반 this 바인딩은 충분히 설득력 있는 선택지라고 볼 수 있다.