자바스크립트 퀴즈북 리마인드 Day 13: 모듈은 파일 묶음이.
ESM live binding, default/named export, circular dependency, dynamic import, CJS와 ESM 차이를 프론트엔드 코드 리뷰 관점에서 정리합니다.
오늘의 질문: “객체 안에 있던 메서드를 콜백으로 넘기면
this도 같이 따라갈까?”
const user = {
name: "Jing",
sayName() {
return this.name;
},
};
const say = user.sayName;
console.log(user.sayName());
console.log(say());
첫 번째는 "Jing"입니다. 두 번째는 strict mode 기준으로 TypeError가 나거나, 환경에 따라 전역 객체를 보면서 undefined처럼 보일 수 있습니다. 같은 함수처럼 보여도 호출 형태가 달라졌기 때문입니다.
this는 “함수가 어디에 적혀 있었는가”보다 함수가 호출될 때 왼쪽에 어떤 객체가 남아 있는가를 먼저 봅니다.
객체 안에 함수를 넣으면 this가 그 객체에 영구히 묶인다고 생각하기 쉽습니다. 하지만 자바스크립트에서 함수는 값입니다. 값을 변수에 담거나 콜백으로 넘기는 순간, 원래 객체와의 연결은 자동으로 따라가지 않습니다.
const profile = {
name: "Jing",
print() {
console.log(this.name);
},
};
setTimeout(profile.print, 0); // 의도한 this가 아니다
이 코드는 “나중에 profile.print()를 호출한다”가 아닙니다. profile.print라는 함수 값을 꺼내서 timer에게 넘긴 것입니다. timer가 나중에 그 함수를 어떻게 호출하는지는 profile 객체와 별개입니다.
| 호출 모양 | this 판단 | 리뷰 질문 |
|---|---|---|
obj.method() | this는 보통 obj | 왼쪽 객체가 호출 순간에 남아 있나? |
const fn = obj.method; fn() | 메서드 연결이 사라짐 | 값만 분리한 뒤 호출하지 않았나? |
fn.call(obj) / bind(obj) | 명시적으로 this를 지정하거나 고정 | 의도한 객체를 정말 고정해야 하나? |
() => this.method() | arrow가 자기 this를 만들지 않고 바깥 값을 캡처 | 바깥 this가 안전한 위치인가? |
this 문제는 대부분 문법 암기보다 “함수 값이 이동했는가?”를 추적하면 잡힙니다.
프론트엔드 코드에서 자주 보는 실수는 클래스나 객체 메서드를 이벤트 핸들러로 바로 넘기는 경우입니다.
class Counter {
count = 0;
increment() {
this.count += 1;
}
}
const counter = new Counter();
button.addEventListener("click", counter.increment);
increment는 Counter 안에 정의되어 있지만, 이벤트 리스너로 넘어갈 때는 함수 값만 전달됩니다. 그래서 this가 counter라고 보장되지 않습니다.
의도가 “항상 counter를 증가시킨다”라면 고정해야 합니다.
button.addEventListener("click", counter.increment.bind(counter));
또는 클래스 필드 arrow function처럼 처음부터 바깥 인스턴스를 캡처하는 방식도 있습니다.
class Counter {
count = 0;
increment = () => {
this.count += 1;
};
}
둘 중 무엇이 낫다는 규칙보다, 리뷰에서는 “이 함수가 어디로 전달되고, 호출자가 누구인가?”를 먼저 봐야 합니다.
arrow function은 자기만의 this를 만들지 않습니다. 이 말은 메서드로 쓰면 오히려 의도가 틀어질 수 있다는 뜻이기도 합니다.
const user = {
name: "Jing",
sayName: () => this.name,
};
여기서 this는 user가 아닙니다. 객체 리터럴이 새 this를 만들어 주지 않기 때문입니다. arrow function은 “호출 지점의 this를 무시하고 바깥 this를 쓰겠다”는 선택에 가깝습니다.
그래서 리뷰 기준은 단순합니다.
bind를 고려한다.this를 쓰지 않아도 되면 아예 인자를 명시적으로 받는 함수가 더 읽기 쉽다.bind를 보면 “this를 맞췄으니 해결”이라고 넘기기 쉽습니다. 하지만 bind는 새 함수 값을 만듭니다. 그래서 이벤트 등록/해제 코드에서는 함수 identity까지 같이 봐야 합니다.
button.addEventListener("click", user.print.bind(user));
// 나중에 제거하려고 해도 다른 함수 값이다.
button.removeEventListener("click", user.print.bind(user));
위 코드는 removeEventListener가 기대한 대로 동작하지 않습니다. 두 번의 bind가 서로 다른 함수 객체를 만들었기 때문입니다. 고정된 콜백을 변수나 필드에 보관해야 제거할 수 있습니다.
const handleClick = user.print.bind(user);
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick);
클래스에서도 같은 문제가 생깁니다.
class Counter {
count = 0;
constructor(button) {
this.handleClick = this.increment.bind(this);
button.addEventListener("click", this.handleClick);
}
increment() {
this.count += 1;
}
destroy(button) {
button.removeEventListener("click", this.handleClick);
}
}
리뷰 포인트는 두 겹입니다. 첫째, 호출될 때 this가 의도한 객체인가? 둘째, 그 함수 값이 등록·해제·memoization 경계에서 같은 identity로 유지되는가? 프론트엔드에서는 두 번째 질문이 빠지면 메모리 누수나 중복 이벤트 핸들러가 남습니다.
Day 7에서 본 클로저는 함수가 만들어진 렉시컬 환경의 바인딩을 따라갑니다. this는 그와 다릅니다. 일반 함수의 this는 보통 호출 형태로 정해지고, arrow function은 아예 자기 this를 만들지 않고 바깥 this를 캡처합니다.
const panel = {
id: "settings",
openLater() {
setTimeout(function () {
console.log(this.id);
}, 0);
},
};
이 코드에서 timer 콜백의 this는 panel이 아닙니다. 바깥 메서드가 panel.openLater()로 호출됐다는 사실이 안쪽 일반 함수 호출까지 자동으로 전달되지 않기 때문입니다.
const panel = {
id: "settings",
openLater() {
setTimeout(() => {
console.log(this.id);
}, 0);
},
};
arrow callback을 쓰면 안쪽 함수는 바깥 openLater 실행의 this를 캡처합니다. 이 경우에는 자연스럽습니다. 하지만 객체 리터럴의 메서드 자체를 arrow로 만들면 이야기가 달라집니다. 객체 리터럴은 새 this를 만들지 않으므로, arrow가 캡처할 “객체 자신”이 없습니다.
그래서 리뷰할 때는 “arrow가 더 최신 문법인가?”가 아니라 “이 함수는 호출자가 정하는 this를 받아야 하는가, 생성 위치의 this를 붙잡아야 하는가?”로 물어야 합니다.
React 함수 컴포넌트에서는 대개 this를 직접 다루지 않습니다. 상태와 props는 렉시컬 바인딩으로 읽고, 이벤트 핸들러는 closure로 만들어집니다.
function SaveButton({ articleId, onSave }) {
const handleClick = () => {
onSave(articleId);
};
return <button onClick={handleClick}>저장</button>;
}
이 코드는 this를 고정할 필요가 없습니다. 필요한 값인 articleId와 onSave가 명시적으로 함수 안에 들어옵니다. 그래서 새 코드에서는 메서드의 this를 살리는 것보다 인자와 closure로 의도를 드러내는 편이 더 단순할 때가 많습니다.
하지만 라이브러리 어댑터나 DOM API를 감싸는 코드는 다릅니다.
type ResizeCallback = (this: HTMLElement, rect: DOMRect) => void;
function onResize(element: HTMLElement, callback: ResizeCallback) {
const observer = new ResizeObserver(() => {
callback.call(element, element.getBoundingClientRect());
});
observer.observe(element);
return () => observer.disconnect();
}
이런 API는 this를 계약으로 제공할 수도 있습니다. 이 지점은 Effective TypeScript Day 12의 public API 타입과 연결됩니다. 콜백이 this를 받는 API라면 문서와 타입, 실제 호출 방식이 모두 맞아야 합니다. 반대로 React props로 공개하는 callback이라면 this 없는 함수 타입이 더 안전합니다.
AI는 this 에러를 만나면 세 가지 방향으로 과하게 고치는 편입니다.
.bind(this)를 붙인다.셋 다 정답일 때가 있지만, 무조건 적용하면 다른 문제가 생깁니다. .bind는 함수 identity를 바꾸고, class field arrow는 인스턴스마다 함수를 만들며, 객체 리터럴 arrow는 객체 자신을 this로 받지 않습니다. 리뷰에서는 먼저 실패한 호출 형태를 찾고, 그 호출 형태를 유지할지 바꿀지 정해야 합니다.
작은 규칙으로 정리하면 이렇습니다.
| 상황 | 우선 검토할 선택 | 이유 |
|---|---|---|
| 객체의 동작을 메서드로 호출 | 일반 메서드 | obj.method() 호출 형태가 의도를 잘 드러냄 |
| 메서드를 이벤트/타이머로 전달 | 저장된 bound 함수 또는 wrapper | this와 함수 identity를 함께 관리 |
| class callback이 인스턴스를 계속 봐야 함 | class field arrow 또는 constructor bind | 생성 시점의 인스턴스 this를 고정 |
| React 함수 컴포넌트 | 명시적 인자와 closure | this 없이 props/state 의존성을 읽음 |
| public callback API | this 타입 또는 this: void 명시 | 사용자가 어떤 호출 방식을 기대해야 하는지 드러냄 |
this 연결이 끊기지 않았는가?this가 실제 호출자를 기준으로 안전한가?bind가 렌더마다 새 함수를 만들며 불필요한 리렌더나 removeEventListener 실패를 만들지 않는가?this가 객체라고 착각하지 않았는가?this 대신 명시적 인자와 closure로 더 단순하게 표현할 수 없는가?this를 제공하는 계약인지, this를 쓰면 안 되는 계약인지 타입과 문서가 일치하는가?this는 특별한 마법이라기보다 호출 규칙입니다. 코드 리뷰에서는 “이 함수가 정의된 자리”보다 “나중에 누가 어떤 모양으로 부르는지”를 따라가면 훨씬 덜 헷갈립니다.
this는 호출 형태를 봅니다.this를 public API 타입으로 어떻게 드러낼지 다룹니다.const say = user.sayName;
say();Post Q&A
자바스크립트 퀴즈북 리마인드 Day 9: this는 호출한 쪽이 정한다 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!
ESM live binding, default/named export, circular dependency, dynamic import, CJS와 ESM 차이를 프론트엔드 코드 리뷰 관점에서 정리합니다.
DOM 이벤트의 capture/bubble, delegation, default action, React synthetic event 경계를 프론트엔드 코드 리뷰 관점에서 정리합니다.
프로토타입 조회, class 문법, 인스턴스 필드와 prototype 메서드 차이를 프론트엔드 코드 리뷰 관점에서 정리합니다.
고차 함수, 순수 함수, 부수 효과, async 함수의 Promise 반환을 코드 리뷰에서 어떻게 읽을지 정리합니다.
this 바인딩을 정의 위치가 아니라 호출 형태, 메서드 분리, arrow function, bind 기준으로 읽는 방법을 코드 리뷰 관점에서 정리합니다.
콜 스택, 태스크 큐, 마이크로태스크 큐를 기준으로 Promise와 setTimeout 실행 순서를 코드 리뷰 관점에서 정리합니다.
클로저가 값 복사가 아니라 렉시컬 환경 참조라는 점을 stale closure, 루프 콜백, React 이벤트 리뷰 관점에서 정리합니다.
스코프 체인, 렉시컬 환경, var/let/const 호이스팅 차이를 콜백·상태 버그 리뷰 관점에서 다시 정리합니다.
객체 참조와 얕은 복사, Object와 Map/Set의 차이, WeakMap/WeakSet이 필요한 메모리 상황을 프론트엔드 코드 리뷰 관점에서 정리합니다.
객체 프로퍼티를 key/value가 아니라 descriptor로 읽는 법, getter/setter와 defineProperty, preventExtensions/seal/freeze의 경계를 코드 리뷰 관점에서 정리합니다.
객체가 원시값으로 바뀌는 ToPrimitive 흐름, + 연산자의 문자열 연결과 숫자 덧셈 분기, Symbol.toPrimitive가 코드 리뷰에서 왜 중요한지 정리합니다.
==, ===, Object.is, SameValueZero가 각각 어떤 비교 알고리즘을 쓰는지 정리하고, includes와 indexOf, Map/Set 키 비교에서 생기는 프론트엔드 리뷰 포인트를 잡습니다.
자바스크립트의 Number가 왜 정수처럼 보여도 부동소수점 모델 위에서 움직이는지, NaN과 -0, safe integer, BigInt를 프론트엔드 코드 리뷰 관점에서 다시 정리합니다.