Low Love - King Gnu

cd

obvoso

star

javascript 함수 참조와 직접 호출

근데 이제 this를 곁들인

개발
React
js
thumbnail

서론

함수 참조직접 호출에 관한 문제를 겪으며 정리한 글입니다. 특히 React에서의 이벤트 핸들러 바인딩 관점에서 정리하였습니다.

const plugin = useRef(null)

// 동작x
onMouseEnter={plugin.current?.stop}

// 동작o
onMouseEnter={() => plugin.current?.stop()}

두 코드 모두 onMouseEnter 이벤트 발생시 plugin.current의 stop 함수를 호출해라 라는 의도를 지니고 있습니다만 실제로는 다른 동작을 하게 되었습니다.

video

디스야 사랑해

Javascript관점의 함수 참조 vs 직접 호출

함수 참조와 this 바인딩

자바스크립트에서 obj.method 형태의 함수를 변수에 할당하여 전달할 때(함수 자체의 참조를 넘길 때), 그 함수 내부의 this원본 객체를 가리키지 않을 수 있습니다.


const obj = {
  value: 10,
  getValue() {
    console.log(this.value);
  },
};

const ref = obj.getValue; // 함수 참조를 따로 빼옴
ref(); // undefined (또는 글로벌 객체의 value)


위 예시에서 obj.getValueref에 할당하여 나중에 ref()를 호출하면 thisobj를 가리키지 않게 됩니다.

React에서 onMouseEnter={obj.getValue}와 같이 함수 참조를 바로 넘겨주는 경우도 마찬가지로, 내부에서 this가 달라질 수 있습니다.

plugin.current?.stop의 경우

참조만 던져줄 경우

onMouseEnter={plugin.current?.stop}처럼 참조만 넘겨주면, 실제로 이벤트가 발생할 때 이 함수를 누가, 어떤 컨텍스트에서 부르느냐에 따라 this가 달라집니다.

만약 stop 함수 내부에서 this에 의존해 무언가를 해야 한다면, thisplugin.current가 아니라서 에러가 발생하거나 전혀 동작하지 않을 수 있습니다.

함수 내부에서 thisplugin.current가 아니라, undefined 또는 다른 컨텍스트가 됩니다. 함수 참조만 던져주어 컨텍스트를 잃어버리기 때문입니다.

고차함수를 만들 경우

반면 onMouseEnter={() => plugin.current?.stop()}는 익명 함수를 새로 만들어서, 그 내부에서 plugin.current?.stop()를 직접 호출하는 방식입니다.

이렇게 하면 호출 시점에 plugin.current를 메서드 형태로 부르는 것이 아니라 자바스크립트의 함수 호출 규칙에 맞게 plugin.current를 정확히 바인딩하고 메서드를 실행할 수 있게 됩니다.

React 관점의 이벤트 핸들러와 함수 바인딩

React에서 이벤트 핸들러(onClick, onMouseEnter 등)에 함수를 넘길 때는 함수 자체를 넘겨주는 것이 기본입니다.

  1. 고차 함수(익명 함수로 감싸기)

    <button onClick={() => doSomething()} />
    

    이 방식은 클릭할 때마다 매번 새로운 함수를 생성한다는 단점이 있습니다.
    반면, 함수 내에서 필요한 데이터를 즉시 참조하고, this나 내부 상태를 원하는 방식으로 쉽게 다룰 수 있다는 게 장점입니다.

    안전하며, 함수가 올바른 컨텍스트에서 호출될 수 있도록 보장합니다.

  2. 함수 참조 전달

    <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은 함수 참조만 넘기기 때문에 호출 문맥을 잃어버립니다.

thisplugin.current가 아니라 전역 컨텍스트(undefined 또는 window/globalThis)를 가리킬 수 있습니다.

이처럼 바로 함수 참조를 넘기면 렌더 시점에는 존재하지 않는 함수 참조가 들어가게 될 가능성이 생겨버립니다. 그리고 추후 이 값이 바뀌더라도, 이미 이벤트 핸들러로 등록된 것이 바뀌지 않아 이벤트 발생 시 아무런 동작도 안 할 수 있습니다.

onMouseEnter={() => plugin.current?.stop()}

반면 위의 코드는 이벤트가 실제로 발생하는 순간(마우스가 올라가는 시점)에 가장 최신의 plugin.current 을 참조하여 함수를 호출하게 됩니다.

렌더 시점에 plugin.currentnull이라 하더라도, 렌더 이후에 ref가 채워졌다면 이벤트 시점에 올바른 함수가 존재하게 됩니다.

여기에선 화살표 함수를 사용하여 이벤트가 발생할 때 plugin.current?.stop()을 명시적으로 호출합니다.

화살표 함수 내부에서는 독립적인 this를 사용하지 않기 때문에, plugin.current를 명확히 참조할 수 있습니다. (상위 스코프의 this를 사용하기 때문에)

JavaScript에서 메서드를 호출할 때, 그 앞의 객체(plugin.current)가 this로 설정됩니다.
따라서 stop 내부의 this는 plugin.current가 됩니다.

왜 컨텍스트를 잃어버리는가…

javascript의 함수는 호출될 때 this가 설정됩니다. 함수가 어떻게 호출되었는지(호출 문맥)에 따라 런타임에 동적으로 결정되는 것 입니다.

참조된 함수를 호출하면, 호출 문맥이 없으므로 this는 기본적으로 windowundefined(strict mode)를 가르킵니다. (plugin.current가 null일 때의 메서드를 넘긴 후 호출할 때)

직접 호출

plugin.current?.stop(); // this는 plugin.current
  • 호출 시점에 plugin.currentthis로 설정됩니다.

함수 참조

const stop = plugin.current?.stop; // this를 잃음
  • 함수가 호출될 때, 호출 컨텍스트가 없어 this가 설정되지 않거나 전역 컨텍스트를 가리킵니다.
    함수의 레퍼런스만을 가르키기 때문에 stop을 누가 어떻게 호출하느냐에 따라 this가 설정됩니다.

화살표 함수 사용

onMouseEnter={() => plugin.current?.stop()} // 안전
  • 화살표 함수 내부의 this는 상위 스코프를 따르므로 plugin.current와 직접적으로 연결된 메서드를 호출하는 것이 보장됩니다. 이벤트 리스너에 콜백 함수를 화살표 함수의 형태로 넘겨주면 클릭 즉시 thisplugin.current로 바인딩됩니다.

회고

this너무어렵습니다 자바스크립트 개발자 중 한 명인 더글라스 크락포드가 설계상의 오류라고 지적했다합니다. 애초에 잘 만들었어야지 왜 30년이 지나도록 고치질 않아 힘들게하는걸까요

react 함수형 컴포넌트는 this가 없습니다. 없어서.. 사용을 안해서 잊고 있었는데 다시 문제를 직면하니 왜 잊고 있었는지 다시 깨닫게되었습니다.

컨텍스트 컨텍스트 타이핑을 하다 이전에 가상메모리를 공부할 때 문맥 교환, 컨텍스트 스위치를 문맥 스위치라 잘못 말해 아직까지 고통받고있는데 이 기억 또한 생각이 났습니다.

만 원을 안내기 위해 일단 투고를 하지만 추후 수정할 글입니다. 틀린 부분이 있다면 역시 지적 부탁드립니다.

레퍼런스

star

이전 포스트

Suspense와 useSearchParams

Next.js의 app router와 hydrate

2025년 1월 5일