[번역] React Compiler & React 19 - forget about memoization soon?

정호진·2024년 4월 24일

번역

목록 보기
2/2

원문: https://www.developerway.com/posts/react-compiler-soon


React Compiler & React 19 - 메모이제이션에 대해 잊어도 될까?

React Compiler가 사실상 React19가 아니라는 것을 알고 있나요? 그렇다면 React에서 memo에 대해 언제 잊어도 될까요? 그리고 Compiler 배포되었을 때 명확하게 바뀌게 될 것은 무엇일까요?

React 19와 이전에 React Foregt으로 알려진 React Compiler는 지난 한 달간 React에 대한 토론에 지배적인 주제였습니다. 우리는 곧 React의 메모이제이션에 대해 생각하지 않아도 될 가능성에 대해 (좋은 방향으로) 정신을 잃었습니다. 이게 진짜라고 생각하시나요? 앞으로 몇 달간 memo, useMemo, useCallback에 대해 기억하지 않아도 될까요? 그리고 React Compiler가 출시되면 어떤 변화가 생기고 우리가 그 변화 이후에 React에 대해 무엇을 배우고 가르쳐야 할까요?

한번 봐보자구요

React 19는 React Compiler가 아닙니다

가장 중요한 것을 명확하게 해보겠습니다: 메모이제이션은 어디도 가지 않으니 아직은 배우세요. React 19는 React Compiler가 아닙니다. React 팀은 React 19가 곧 출시 된다고 알리는 블로그 포스트에 Compiler에 대해 같이 알렸고, 모두는 흥분해서 서둘러서 결론을 내렸습니다.

그러나 React 팀 멤버의 트윗은 이 혼란에 대해 명확하게 얘기했습니다.

19 ≠ Compiler

React 19에서, 우리는 새로운 기능들을 여러개 볼 수 있습니다. 그러나 컴파일러가 나오기 전까지는 좀 더 기다려야 합니다. 이것은 현재 완벽하지 않지만, 다른 React 팀 멤버의 트윗에 의하면, 올해 안에 기능이 끝난다고 합니다.

개인적으로, 이 타임라인에는 약간 회의적입니다. React 팀 멤버가 소개하는 Compiler와 이 타임라인을 본다면 현재는 Compiler 완성의 중간단계 쯤 와 있는 것을 볼 수 있습니다.

이 기능은 2년전인 2021년에서 부터 개발이 시작되었습니다. Meta와 같이 큰 코드 베이스를 가진 회사가 근본적인 무엇인가를 옮기는데는 매우 복잡할 것입니다. 현재 개발 단계에서 release까지 가는데에는 2년 더 걸릴 수 있습니다.

그러나 누가 알겠어요, 아마도 React 팀이 올해 이 기능을 출시할지. 그것은 좋은 소식입니다. 이 비디오에서 언급한, 현재 Compiler에 대해 약속할 수 있는 것은 채택(선택)을 위해 어떤 코드도 변경할 필요가 없다는 것입니다. 이것은 그냥 동작할 것입니다. 만약 이 기능이 올해 말에 출시된다면, 그것은 정말로 아주 좋은 징조가 될 것이고, 나머지 사람들은 쉽고 빠르게 그것으로 전환이 가능해집니다.

그러나, Compiler가 그 정말 쉽고 어떤 부작용 없이 채택될 수 있도록 올해 출시된다고 해도, 지금 당장 useCallbackmemo를 잊으라는 것은 아닙니다. 언제나 “전환” 기간이 있고, “이미Compiler가 사용가능한 경우”에서 시작해서 , “아직 Compiler로 전환하지 않은 드문 경우” 시나리오로 천천히 전환됩니다.

내 생각에 클래스 컴포넌트에서 훅을 동반한 함수 컴포넌트로 개념이 전환되는 시기를 보면, 적어도 3년이 걸렸습니다. (2018년 부터 시작해서) - 모든 코스, 문서, 그리고 블로그와 대부분의 사람들이 React with hooks 버전으로 변경했고, 함수형 컴포넌트와 훅에 대해 기본적으로 얘기하기 시작했다. 그리고 6년 뒤인 오늘날, 여전히 상당한 양의 클래스 컴포넌트들이 여기 저기에 숨겨져 있다.

Compiler에도 유사한 타임라인을 적용한다면, 그것은 memouseMemo 그리고 useCallback에 대한 지식을 적어도 다음 3년동안은 갖고있어야 한다는 것을 의미합니다. 운 좋게 Compiler가 출시되지마자 마이그레이션 할 수 있는 최신 코드 베이스에서 작업할 수 있다면 이보다는 더 짧을 수 도 있습니다. 혹은 더 길 수도 있습니다, 당신이 React 강사거나 혹은 속도가 느린 대규모 코드 베이스에서 작업하는 경우도 마찬가지 입니다.

React Compiler에서의 변경점

그래서 정환하게 바뀌는게 무엇인가? 간단한 답은 - 모든게 메모된다 입니다. React Compiler는 전형적인 React 코드를 모든 훅의 의존성과, 컴포넌트의 Props들과 스스로 메모이제이션 된 코드로 변환하는 Babel 플러그인이 될 것입니다. 기본적으로 다음과 같습니다

const Component = () => {
  const onSubmit = () => {};
  const onMount = () => {};

  useEffect(() => {
    onMount();
  }, [onMount]);

  return <Form onSubmit={onSubmit} />;
};

아래는 onSubmitonMountuseCallback으로 래핑되고, FormReact.memo로 래핑된 코드입니다.

const FormMemo = React.memo(Form);

const Component = () => {
  const onSubmit = useCallback(() => {}, []);
  const onMount = useCallback(() => {}, []);

  useEffect(() => {
    onMount();
  }, [onMount]);

  return <FormMemo onSubmit={onSubmit} />;
};

당연하게도 Compiler는 저 코드들을 직접적으로 변환하지 않고 좀 더 복잡하고 고급된 방식을 사용합니다. 그러나 이것은 우리의 이해를 돕기 위한 멘탈모델입니다. 만약 정확한 상세 정보를 원한다면 React 코어팀의 멤버가 Compiler에 대해 소개하는 이 비디오를 모든 것을 추천합니다. 그리고 왜 useCallback이나 memo를 사용하는지 잘 모르겠다면, Youtube에 있는 React 시리즈 고급과정의 첫 6개 비디오를 보는 것을 추천합니다. 여기에는 리렌더링과 메모이제이션에 대한 모든 것이 있습니다. 대체재로 글을 읽고 싶다면, 여기 있는 모든 글을 읽으시면 됩니다.

리액트를 가르치고 배우는 방식에 있어서, 이러한 전환은 몇 가지를 의미합니다.

부모의 리렌더링은 자식의 리렌더링

현재, 부모 컴포넌트가 리렌더링 된다면 자식 컴포넌트 역시 모두 리렌더링 됩니다.

// if Parent re-renders
const Parent = () => {
  // Child will also re-render
  return <Child />;
};

많은 사람들은 현재 Child컴포넌트의 리렌더링은 props가 바뀔때만 발생한다고 생각합니다. 전 이것을 리렌더링에 대한 큰 신화라고 부릅니다. 현재로서, 이것은 사실이 아닙니다. Props는 기존 React의 동작과 상관이 없습니다.

충분히 재밌게도, 이것은 Compiler에서는 사실이 될 것입니다. 모든 것은 내부적으로 메모되기 때문에, 현재 신화는 사실 React의 기본 동작이 되어가고 있습니다. 몇 년안에, React 컴포넌트의 리렌더링은 오직 그것의 상태나 props의 변화에만 발생하고, 부모 컴포넌트가 리렌더링 되어도 상관이 없다고 가르칠 것입니다. 삶은 때때로 이상합니다.

더 이상 성능을 위해 구성할 필요가 없습니다.

현재, 우리는 리렌더링을 감소시키는 “moving state down” 이나 “passing components as children”과 같은 몇몇 구성 기술을 갖고 있습니다. 저는 보통 useCallback이나 memo를 사용하기 전에 사용하는 것을 추천합니다. 왜냐하면 리액트에서 제대로 메모이제이션을 하는 것은 매우 어렵기 때문입니다.

다음 코드를 예시로 보자:

const Component = () => {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <Button onClick={() => setIsOpen(true)}>
        open dialog
      </Button>
      {isOpen && <ModalDialog />}
      <VerySlowComponent />
    </>
  );
};

VerySlowComponent는 dialog가 열릴때마다 매번 리렌더링되어서 dialog가 지연되는 것을 야기합니다. 컴포넌트에서 dialog가 열리는 상태를 요약하자면 다음과 같습니다:

const ButtonWithDialog = () => {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <Button onClick={() => setIsOpen(true)}>
        open dialog
      </Button>
      {isOpen && <ModalDialog />}
    </>
  );
};

const Component = () => {
  return (
    <>
      <ButtonWithDialog />
      <VerySlowComponent />
    </>
  );
};

우리는 본질적으로VerySlowComponent의 불필요한 리렌더링을 메모이제이션을 사용하지 않고 없앴습니다.

Compiler가 출시한다면, 이 패턴을 성능을 위해 사용할 일은 없습니다. 우리는 아마 여전이 그것들을 구성과 목적에 따른 분리에만 사용할 것입니다. 그러나 더 이상 컴포넌트를 더 작게 분리하도록 강제하는 자연스러운 리렌더링 파워는 없을 것입니다. 우리의 컴포넌트는 부정적인 결과 없이 더 커질 것입니다.

더이상 useMemo/useCallback을 사용하지 않아도 됩니다.

당연히, 우리 코드를 괴롭히던 모든 useMemouseCallback은 없어질 것입니다. 이 부분이 가장 저를 신나게 합니다. 더 이상 단순히 하나의 onSubmit props callback을 메모하기 위해 여러 단계의 컴포넌트를 통해 props를 추적할 필요 없습니다. 서로에게 의존하며 이해할 수 없고, 읽기 어렵고 디버깅 하지 못하는 useMemouseCallback의 연쇄도 더 이상 없습니다. 자식 컴포넌트들이 메모이제이션되지 않아 깨지는 메모 문제도 아무도 알지 못합니다.

Diffing & reconciliation

우리는 React에서 diffing & reconciliation에 대한 설명을 바꿔야 할지 모릅니다. 현재 간략하게 된 설명은 우리가 자식 컴포넌트와 같이 “render” 한다면, 우리는 이것에 대한 Element를 만든다는 것입니다. 이 Element는 다음과 같은 모양을 띄는 객체입니다.

{
  "type": ...,
  "props": ...,
  // other react stuff
}

“type”은 문자열 또는 Component에 대한 참조입니다.

다음 코드를 보겠습니다

const Parent = () => {
  return <Child />;
};

Parent가 리렌더링 된다면, 이 함수는 실행되고, <Children/> 객체는 재생성 됩니다. React는 이전의 객체와 리렌더링 이후의 객체를 얕은 비교를 수행합니다. 그리고 참조가 변경된다면, 이것은 React에게 하위에 있는 sub-tree까지 모두 변경이 필요한 지표가 됩니다.

이것이 비록 아무 props 없이도 <Child/> 컴포넌트가 왜 매번 리렌더링이 되는지에 대한 이유입니다. <Child/>의 결과(React.createElement함수 호출의 문법적 설탕)은 언제나 재생성 되는 객체이고, 이 의미는 얕은 비교를 통과할 수 없다는 것입니다.

React Compiler를 사용한다면, Element, diffing 그리고 reconciliation의 컨셉은 동일하게 남겨집니다. 이것은 좋습니다. 그러나 는 props에 변화가 없다면 메모된 객체를 반환할 것입니다. 사실상, Compiler의 결과는 useMemo, 심지어는 Element에 포장된 모든 것과 같습니다.

const Parent = () => {
  const child = useMemo(() => <Child />, []);
  return child;
};

그러나 이것은 공개적으로 사용이 가능한 리소스에만 제한이 되도록 가정됩니다. 그래서 이 지점이 약간 걱정입니다. 어느 경우로든, 이것은 우리의 실제 코드와는 관련이 없는 구현 세부 사항입니다.


현재 모든것은 그대로 머물러 있습니다. 다른 컴포넌트 안에 컴포넌트를 만드는 것은 여전히 안티 패턴입니다. 우리는 여전히 element를 구분하거나 상태를 초기화 할 때 “key”속성을 사용합니다. Context는 여전히 고통을 관리해줍니다. 그리고 모든 데이터 페칭과 에러 핸들링은 대화의 일부가 아닙니다.

그러나 어쨋든, Compiler의 출시를 기다릴 수 없습니다. 비록 이것 때문에 나의 글 과 유튜브 영상을 새로 만들어야 해도 이것은 우리의 React 삶에 큰 향상을 가져다 줍니다.

0개의 댓글