[번역] React에 useEffectEvent가 나온다

이은서·2023년 2월 7일
12

사라졌지만 useEvent에 대해 알아보고 싶으신 분들은 여기를 클릭 해 주세요


1. useEvent는 종료됐지만, useEffectEvent가 오래 지속된다!

원글 보러 가기
6개월도 더 전에, React 팀은 새로운 useEvent hook과 관련된 Request For Comments(RFC)를 전 세계에 공유했습니다. useEvent는 reference가 동일한 함수를 반환(useCallback 처럼)하고 함수 안에서 접근하고 있는 state는 항상 컴포넌트의 현재 state와 동일합니다.(deps 없이 항상 현재 상태를 사용 가능) 이런 기능 덕분에 중복 렌더링을 최적화 할 수 있었습니다.

function Chat() {
  const [text, setText] = useState('');

  // 🟡 Text가 바뀔 때 마다 다른 함수 반환
  const onClick = useCallback(() => {
    sendMessage(text);
  }, [text]);

  return <SendButton onClick={onClick} />;
}
function Chat() {
  const [text, setText] = useState('');

  // ✅ Text가 바뀌어도 항상 같은 함수 반환
  const onClick = useEvent(() => {
    sendMessage(text);
  });

  return <SendButton onClick={onClick} />;
}

RFC는 활발하게 진행됐고 많은 논의가 있었습니다. 그 결과 안타깝게도 useEvent을 만든 사람들의 마음에 useEvent에 대한 의심이 생겼고 구현을 중단하기로 하였습니다. 대표적으로 두가지 이유가 있었습니다. 첫 번째, useEvent와 useCallback은 조금은 다른 기능을 수행하지만 사용자들은 useEvent를 더 나은 버전의 useCallback으로 받아드렸습니다. 두 번째, useEvent의 개발자들은 자동으로 메모이제이션이 가능한 컴파일러를 만들고 있습니다. (더 알아보고 싶다면 여기를 클릭해주세요). 두 기능을 동시에 사용하기에는 너무 번거로웠을 것입니다.

하지만 useEvent는 곧 변화된 형태로 우리에게 돌아올 것이라는 징조가 있었습니다. React Repository에는 의미심장한 PR이 올라왔습니다. 이것은 이름을 useEvent에서 useEffectEvent로 바꾸고 동작도 약간 변경되었습니다. 이제부터는 안정적인 함수 reference를 반환하지 않습니다. 하지만 dependencies array에 지정하지 않고 useEffect에서 참조할 수 있습니다.

function Chat() {
  const [text, setText] = useState('');
  const [state, setState] = useState<State>('INITIAL');

  const onHappened = useEffectEvent(() => {
    logValueToAnalytics(text);
  });
  
  useEffect(() => {
    onHappened();
  }, [state]);

  return (/*...*/);
}

현재로서는 그 Pull Request가 완전한 RFC를 동반하지 않기 때문에 useEffectEvent의 미래를 예측하거나 그 기능에 대해 보다 광범위한 결론을 내리기는 어렵습니다.


2. useEffectEvent 뜯어보기

function mountEvent<Args, Return, F: (...Array<Args>) => Return>(
  callback: F,
): F {
  const hook = mountWorkInProgressHook();
  const ref = {impl: callback};
  hook.memoizedState = ref;
  return function eventFn() {
    // 현재 렌더링 중인지 체크해서 렌더링 중이 아니면 에러가 남. 
    // 즉 이벤트 콜백함수로 setTimeout등의 콜백함수로 useEffectEvent를 
    // 사용할 수 없고 useEffect 내부에서만 사용할 수 있음
    if (isInvalidExecutionContextForEventFunction()) {
      throw new Error(
        "A function wrapped in useEffectEvent can't be called during rendering.",
      );
    }
    return ref.impl.apply(undefined, arguments);
  };
}

function updateEvent<Args, Return, F: (...Array<Args>) => Return>(
  callback: F,
): F {
  const hook = updateWorkInProgressHook();
  const ref = hook.memoizedState;
  useEffectEventImpl({ref, nextImpl: callback});
  return function eventFn() {
    
    if (isInvalidExecutionContextForEventFunction()) {
      throw new Error(
        "A function wrapped in useEffectEvent can't be called during rendering.",
      );
    }
    return ref.impl.apply(undefined, arguments);
  };
}

현재 실제 useEffectEvent에 대한 React 코드입니다. 크게 어려운 부분은 없는데요 한번 살펴보겠습니다.
mountEvent는 처음에 useEffectEvent를 호출하는 컴포넌트가 Mount될 때에만 호출되는 함수입니다. 이후엔 항상 updateEvent를 호출합니다. mountEvent에서는 ref.impl에 callback함수를 넣어주고 eventFn로 wrapping하여 함수 호출이 옳은 호출인지 먼저 확인합니다. 그 다음 eventFn에서는 클로저를 활용하여 ref.impl을 호출해줍니다.

updateEvent에서는 hook.memoizedState에서 값을 꺼내와 useEffectEventImpl함수를 호출해주는 것 말고 크게 다르지 않은데요

function useEffectEventImpl<Args, Return, F: (...Array<Args>) => Return>(
  payload: EventFunctionPayload<Args, Return, F>,
) {
  currentlyRenderingFiber.flags |= UpdateEffect;
  let componentUpdateQueue: null | FunctionComponentUpdateQueue =
    (currentlyRenderingFiber.updateQueue: any);
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    componentUpdateQueue.events = [payload];
  } else {
    const events = componentUpdateQueue.events;
    if (events === null) {
      componentUpdateQueue.events = [payload];
    } else {
      events.push(payload);
    }
  }
}

useEffectEventImpl은 조금 복잡해 보이지만 단순히 payload를 현재 렌더링 중인 Fiber의 updateQueue에 events값을 추가하는 함수입니다. events는 현재함수와 nextImpl(다음에 호출될 함수)을 가지고있습니다. events 값이 어떻게 소비되는지만 보면 될 것 같습니다.

function commitUseEffectEventMount(finishedWork: Fiber) {
  const updateQueue: FunctionComponentUpdateQueue | null =
    (finishedWork.updateQueue: any);
  const eventPayloads = updateQueue !== null ? updateQueue.events : null;
  if (eventPayloads !== null) {
    for (let ii = 0; ii < eventPayloads.length; ii++) {
      const {ref, nextImpl} = eventPayloads[ii];
      ref.impl = nextImpl;
    }
  }
}

CommitPhase에서 이 함수가 호출이 되고 이것은 useEffect의 callback 함수가 호출되는 것 보다 먼저 호출됩니다. ref.impl에 nextImpl을 넣어주면서 useEffect에서 useEffectEvent로 생성한 함수를 호출하면 현재 state의 값들을 사용할 수 있는 것입니다.

Summary

useEffectEvent는 현재 정식 기능이 아닙니다. 어쩌면 정식 출시가 아예 안될지도 모르죠.. 하지만 많은 불편함을 제거해 줄 기능이라고 생각하기 때문에 나오길 희망하면서 이 글을 번역하고 코드를 분석해서 적어보았습니다.

profile
취준 프론트엔드 개발자

0개의 댓글