javascript 함수 참조와 직접 호출
근데 이제 this를 곁들인
서론
함수 참조와 직접 호출에 관한 문제를 겪으며 정리한 글입니다. 특히 React에서의 이벤트 핸들러 바인딩 관점에서 정리하였습니다.
const plugin = useRef(null)
// 동작x
onMouseEnter={plugin.current?.stop}
// 동작o
onMouseEnter={() => plugin.current?.stop()}
두 코드 모두 onMouseEnter
이벤트 발생시 plugin.current의 stop 함수를 호출해라 라는 의도를 지니고 있습니다만 실제로는 다른 동작을 하게 되었습니다.
디스야 사랑해
Javascript관점의 함수 참조 vs 직접 호출
함수 참조와 this 바인딩
자바스크립트에서 obj.method
형태의 함수를 변수에 할당하여 전달할 때(함수 자체의 참조를 넘길 때), 그 함수 내부의 this
는 원본 객체를 가리키지 않을 수 있습니다.
const obj = {
value: 10,
getValue() {
console.log(this.value);
},
};
const ref = obj.getValue; // 함수 참조를 따로 빼옴
ref(); // undefined (또는 글로벌 객체의 value)
위 예시에서 obj.getValue
를 ref
에 할당하여 나중에 ref()
를 호출하면 this
가 obj
를 가리키지 않게 됩니다.
React에서 onMouseEnter={obj.getValue}
와 같이 함수 참조를 바로 넘겨주는 경우도 마찬가지로, 내부에서 this
가 달라질 수 있습니다.
plugin.current?.stop의 경우
참조만 던져줄 경우
onMouseEnter={plugin.current?.stop}
처럼 참조만 넘겨주면, 실제로 이벤트가 발생할 때 이 함수를 누가, 어떤 컨텍스트에서 부르느냐에 따라 this
가 달라집니다.
만약 stop
함수 내부에서 this
에 의존해 무언가를 해야 한다면, this
가 plugin.current
가 아니라서 에러가 발생하거나 전혀 동작하지 않을 수 있습니다.
함수 내부에서
this
가plugin.current
가 아니라,undefined
또는 다른 컨텍스트가 됩니다. 함수 참조만 던져주어 컨텍스트를 잃어버리기 때문입니다.
고차함수를 만들 경우
반면 onMouseEnter={() => plugin.current?.stop()}
는 익명 함수를 새로 만들어서, 그 내부에서 plugin.current?.stop()
를 직접 호출하는 방식입니다.
이렇게 하면 호출 시점에 plugin.current
를 메서드 형태로 부르는 것이 아니라 자바스크립트의 함수 호출 규칙에 맞게 plugin.current
를 정확히 바인딩하고 메서드를 실행할 수 있게 됩니다.
React 관점의 이벤트 핸들러와 함수 바인딩
React에서 이벤트 핸들러(onClick
, onMouseEnter
등)에 함수를 넘길 때는 함수 자체를 넘겨주는 것이 기본입니다.
-
고차 함수(익명 함수로 감싸기)
<button onClick={() => doSomething()} />
이 방식은 클릭할 때마다 매번 새로운 함수를 생성한다는 단점이 있습니다.
반면, 함수 내에서 필요한 데이터를 즉시 참조하고,this
나 내부 상태를 원하는 방식으로 쉽게 다룰 수 있다는 게 장점입니다.안전하며, 함수가 올바른 컨텍스트에서 호출될 수 있도록 보장합니다.
-
함수 참조 전달
<button onClick={doSomething} />
이 경우,
doSomething
함수가 내부에서this
를 사용하지 않거나, 이미this
가 적절히 바인딩된 상태라면 문제 없이 동작합니다.만약
doSomething
이 객체의 메서드 형태를 유지해야(this
바인딩 필요) 한다면, 호출하는 쪽에서.bind(obj)
등을 따로 해주거나,arrow function
으로 감싸야 합니다.컨텍스트 문제가 발생할 가능성이 있으므로, 내부적으로
this
를 사용하는 메서드에서는 사용하지 않는 것이 좋습니다. 매번.node_modules
를 깔 순 없으니.. 게으르지만 익명함수를 던져주는 쪽을 택하겠습니다.
plugin.current가 생성/마운트 시점에 따라 달라지는 이슈
React에서 ref
는 렌더 단계에서 아직 할당되지 않았을 수 있습니다.
즉, 처음 렌더 때는 plugin.current
가 null
인 경우가 다분합니다.
onMouseEnter={plugin.current?.stop}
plugin.current?.stop
은 함수 참조만 넘기기 때문에 호출 문맥을 잃어버립니다.
this
가 plugin.current
가 아니라 전역 컨텍스트(undefined
또는 window
/globalThis
)를 가리킬 수 있습니다.
이처럼 바로 함수 참조를 넘기면 렌더 시점에는 존재하지 않는 함수 참조가 들어가게 될 가능성이 생겨버립니다. 그리고 추후 이 값이 바뀌더라도, 이미 이벤트 핸들러로 등록된 것이 바뀌지 않아 이벤트 발생 시 아무런 동작도 안 할 수 있습니다.
onMouseEnter={() => plugin.current?.stop()}
반면 위의 코드는 이벤트가 실제로 발생하는 순간(마우스가 올라가는 시점)에 가장 최신의 plugin.current
값을 참조하여 함수를 호출하게 됩니다.
렌더 시점에 plugin.current
가 null
이라 하더라도, 렌더 이후에 ref
가 채워졌다면 이벤트 시점에 올바른 함수가 존재하게 됩니다.
여기에선 화살표 함수를 사용하여 이벤트가 발생할 때 plugin.current?.stop()
을 명시적으로 호출합니다.
화살표 함수 내부에서는 독립적인 this
를 사용하지 않기 때문에, plugin.current
를 명확히 참조할 수 있습니다. (상위 스코프의 this
를 사용하기 때문에)
JavaScript에서 메서드를 호출할 때, 그 앞의 객체(
plugin.current
)가this
로 설정됩니다.
따라서stop
내부의 this는plugin.current
가 됩니다.
왜 컨텍스트를 잃어버리는가…
javascript
의 함수는 호출될 때 this
가 설정됩니다. 함수가 어떻게 호출되었는지(호출 문맥)에 따라 런타임에 동적으로 결정되는 것 입니다.
참조된 함수를 호출하면, 호출 문맥이 없으므로 this
는 기본적으로 window
나 undefined
(strict mode)를 가르킵니다. (plugin.current가 null일 때의 메서드를 넘긴 후 호출할 때)
직접 호출
plugin.current?.stop(); // this는 plugin.current
- 호출 시점에
plugin.current
가this
로 설정됩니다.
함수 참조
const stop = plugin.current?.stop; // this를 잃음
- 함수가 호출될 때, 호출 컨텍스트가 없어
this
가 설정되지 않거나 전역 컨텍스트를 가리킵니다.
함수의 레퍼런스만을 가르키기 때문에stop
을 누가 어떻게 호출하느냐에 따라this
가 설정됩니다.
화살표 함수 사용
onMouseEnter={() => plugin.current?.stop()} // 안전
- 화살표 함수 내부의
this
는 상위 스코프를 따르므로plugin.current
와 직접적으로 연결된 메서드를 호출하는 것이 보장됩니다. 이벤트 리스너에 콜백 함수를 화살표 함수의 형태로 넘겨주면 클릭 즉시this
가plugin.current
로 바인딩됩니다.
회고
this너무어렵습니다 자바스크립트 개발자 중 한 명인 더글라스 크락포드가 설계상의 오류라고 지적했다합니다. 애초에 잘 만들었어야지 왜 30년이 지나도록 고치질 않아 힘들게하는걸까요
react 함수형 컴포넌트는 this가 없습니다. 없어서.. 사용을 안해서 잊고 있었는데 다시 문제를 직면하니 왜 잊고 있었는지 다시 깨닫게되었습니다.
컨텍스트 컨텍스트 타이핑을 하다 이전에 가상메모리를 공부할 때 문맥 교환, 컨텍스트 스위치를 문맥 스위치라 잘못 말해 아직까지 고통받고있는데 이 기억 또한 생각이 났습니다.
만 원을 안내기 위해 일단 투고를 하지만 추후 수정할 글입니다. 틀린 부분이 있다면 역시 지적 부탁드립니다.