React의 진화: 클래스에서 Hooks, 그리고 컴파일된 React로의 전환

최소희·2024년 4월 21일
0

프론트엔드 학습

목록 보기
12/13

react-compiler

컴파일된 React는 hook의 주요 문제를 해결한다.

React를 컴파일 할 때도, 다음 React 원칙은 변치 않는다.

  1. 리액트 상태는 불변이다.
  2. UI는 상태의 함수이다.
  3. 상태가 변경되면 리렌더링하여 새로운 UI를 생성한다.

버전 번호를 제외하면, React는 3가지 뚜렷한 시대를 가지고 있다.

  • 클래스 컴포넌트 시대 (추상화를 위한 원시성이 없음)
  • 훅의 시대(memoization 필요)
  • 컴파일 시대(자동 memoization)

클래스의 시대

초기에는 코드 추상화에 어려움이 있었고, 클래스 컴포넌트를 사용하여 코드를 작성했다. 그러나 코드 재사용 측면에서 제한이 있어, 커뮤니티에서 HoC(Higher Order Component) 와 Render Props와 같은 패턴을 만들었으나, 이는 이상적이지 않았다.

HoC

HoC에서 제공하는 상위 컴포넌트는 재사용 가능한 상태와 기능을 갖춘 컴포넌트

해당 상위 구성 요소는 props를 통해 하위 구성 요소에 상태 및 기능에 대한 액세스를 제공

// MyMenu.js
export default withToggle(MyMenu)

// ActivateUser.js
export default withToggle(ActivateUser)

Issues with HoC

1. Prop Collisions (sometimes) - prop 충돌

  • 부모 컴포넌트에서 전달된 prop이 자식 컴포넌트에서 사용하려는 prop과 충돌할 때 발생
<MyComponent name="Brad" />;

export default someHoC(anotherHoc(coolHoc(MyComponent)));

2. Can't use the HoC twice - HoC 2회 사용 불가

  • 동일한 HoC를 두 번 이상 사용하면 예상치 못한 동작이 발생할 수 있다.
    export default withToggle(withToggle(MyComponent));

3. Indirection - 간접성

  • HoC를 통해 컴포넌트를 래핑하면 간접적으로 데이터 흐름이 발생하여 코드의 이해가 어려워진다.

  • 예시 코드: 어느 props가 어느 HoC에서 왔는지 알 수 있는가? 어느 props가 <MyComponent name="Brad" /> 처럼 HoC에서 오지 않고 컴포넌트에 직접 할당한 props인가?

    function MyComponent({
      name,
      onClick,
      setValue,
      time,
      date,
      isActive,
      isRemoved,
    }) {
      // ...
    }
    
    export default someHoC(anotherHoc(coolHoc(MyComponent)));

4. Composing happens at build-time (can cause issues) - 빌드 타임 때 조합됨 (문제 발생 가능)

  • 빌드 타임 때 HoC가 조합되기 때문에 런타임에 문제가 발생할 수 있다.

  • 런타임에서 컴포넌트를 동적으로 조합하는 경우에 발생할 수 있다.

  • 예시 코드: fetchData라는 HoC를 사용하여 데이터를 가져오는 기능을 추상화한다고 해보자.

  • uid와 같은 동적인 값이 HoC의 경로에 필요한 위치에 동적으로 삽입되어야 하는데, 빌드 시간에는 uid가 정적으로 할당되어 있기 때문에 이를 해결할 수 없다. 즉, fetchData의 경로를 동적으로 설정할 수 없다.

    // Case 1 첫 번째 인자로 전달된 경로에서 데이터를 가져와 BrowseUsers에 전달
    export default fetchData('/users', BrowseUsers)
    
    // Case 2 런타임에 할당되는 uid는 빌드 때는 정적으로 할당되기 때문에 해결 불가
    function UserProfile({ uid }) {
      // ...
    }
    
    export default fetchData('/users/???', UserProfile)

Render Prop

HoC 대안으로 나온 패턴

재사용 가능한 상태 및 함수를 가진 부모 래퍼를 생성하지만 이것을 컴포넌트의 래퍼로서 사용하지는 않는다.

function MyComponent() {
  return (
    <div>
      <h1>My Page</h1>
      <Toggle
        render={(on, toggle) => {
          return (
            <button onClick={toggle}>The toggle is {on ? "on" : "off"}</button>
          );
        }}
      />
      <footer>footer</footer>
    </div>
  );
}

export default MyComponent;

코드 설명:

Render Props 패턴을 사용한 재사용 가능한 Toggle 컴포넌트이다.

내부적으로 토글 값을 관리하는 모든 상태와 기능을 가졌다.

그러나 실제로 JSX를 가지는 것이 아닌, this.props.render의 반환값을 사용한다.

이 패턴을 통해 Toggle은 상태와 기능을 관리하지만 JSX의 제어를 MyComponent 외부로 위임한다.

Render Props의 장점

1. props 충돌이 발생하지 않는다.

  • render props가 같은 이름(name)의 변수를 사용해야한다면, 다른 alias로 지정할 수 있다.

    function MyComponent({ name }) {
      console.log(name);
      return (
        <div>
          <SomeRenderPropsThing>
            // 변수명 바꿔 할당하여 혼동 방지
            {({ name: otherName }) => {
              // ...
            }}
          </SomeRenderPropsThing>
        </div>
      );
    }
    
    export default MyComponent;

2. 동일한 랜더 프롭스를 두 번 사용해도 충돌이 발생하지 않는다.

  • 두개의 다른 함수를 렌더링하기 때문이다.

    function MyComponent({ name }) {
      console.log(name);
      return (
        <div>
          <h1>My Page</h1>
          <Toggle
            render={(on, toggle) => {
              return (
                <button onClick={toggle}>
                  The toggle is {on ? "on" : "off"}
                </button>
              );
            }}
          />
          <footer>footer</footer>
          <Toggle
            render={(on, toggle) => {
              return (
                <button onClick={toggle}>
                  The toggle is {on ? "on" : "off"}
                </button>
              );
            }}
          />
        </div>
      );
    }
    
    export default MyComponent;

3. 간접성이 없다.

  • MyComponent에 전달된 모든 props는 직접적으로 전달 받기 때문이다.

Issues with Render Props

  • Ugly (deep nesting) - 못생김(깊은 중첩)
    • 깊은 중첩으로 인해 코드가 못생겨질 수 있다.
    • Render Props 패턴을 사용하면 컴포넌트의 중첩이 깊어질 수 있으며, 이는 가독성을 해칠 수 있다.
  • Scoping Issues (sometimes) - scope 문제
    • Render Props로 넘겨받은 함수 내에서 변수를 선언하면 그 변수는 해당 함수 내에서만 유효하다.
    • 이 함수 외부에서는 그 변수에 접근할 때 수 없고, 이는 컴포넌트 코드를 작성할 때 혼란을 초래한다.

훅의 시대

함수형 컴포넌트와 Hooks의 등장으로 코드 추상황에 개선이 있었지만, 모든 코드를 함수로 결합하면서 메모이제이션 문제가 발생하였다.

Hooks로 해결된 HoC의 문제점들:

  • 변수 충돌이 없다.
    • HoC를 사용하면 변수 충돌이 생겼으나, Hooks를 사용하면 각 컴포넌트가 자체적으로 상태를 관리하기 때문에 변수 충돌 문제가 사라졌다.
  • 간접성 없다.
    • HoC를 사용할 때는 컴포넌트에 HoC를 적용하기 위해 간접적으로 코드를 작성해야 했다.
    • 그러나 Hooks를 사용하면 컴포넌트 자체가 상태와 렌더링 로직을 갖기 때문에 간접성이 사라졌다.
  • 동일한 커스텀 훅을 두 번 사용할 수 있다.
  • 컴포넌트의 최상위에서 props를 사용할 수 있기 때문에 실행시에 합성이 발생합니다(eg. uid).

Hooks로 해결된 Render Props의 문제점:

  • 가독성이 좋아지며, 깊은 중첩을 유발하지 않는다.
  • 커스텀 훅에서 제공된 값은 JSX의 제한된 위치가 아닌 컴포넌트 자체의 최상위 범위에 스코프가 지정된다.
    • Hooks를 사용하면 컴포넌트 내부에서 상태와 함수를 선언할 수 있으며, 이는 컴포넌트 전체에서 해당 상태와 함수에 접근할 수 있음을 의미한다.

컴파일 시대

React는 상태가 변경될 때 많은 리랜더링이 일어나기도 한다. 그래서 현재 API에서는 useMemo, useCallback, memo 를 사용하여 React가 상태 변경 시 언제 리렌더링을 일으킬지 수동으로 메모이제이션을 조절하였다.

그러나, 이러한 메모이제이션 API는 코드를 혼란스럽게 만들고, 잘못 사용하기도 쉬우며, 최신 작업을 유지하기 위해 추가 작업이 필요하다.

이러한 함수형 컴포넌트의 단점을 보완하기 위해 자동 메모이제이션을 제공하는 컴파일된 React로 발전하였다.

구조

React Compiler는 JavaScript와 React의 규칙을 모델링함으로써 코드를 안전하게 컴파일한다.

예를 들어, React 컴포넌트는 동일한 입력이 주어지면 항상 동일한 값을 반환해야 하며, props나 상태 값을 변경할 수 없다. 이러한 규칙은 개발자가 할 수 있는 것을 제한하고, 컴파일러가 최적화할 안전한 공간을 제공한다.

이처럼, 코드가 React의 규칙을 엄격하게 준수하지 않는 경우를 감지하고 안전한 경우에는 코드를 컴파일하거나 그렇지 않은 경우에는 컴파일을 건너뛰도록 설계되어 있다.

기대점

개발자가 React의 규칙을 준수하지 않은 경우를 대비해, 모델링 기반으로 안전하게 컴파일할 수 있게 된다.

이를 통해 안정적이고 향상된 코드 품질 기반으로 개발할 수 있는 환경이 될 것으로 기대된다.




참고 자료

https://react.dev/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024

https://reacttraining.com/blog/react-19-will-be-compiled

https://gist.github.com/bradwestfall/4fa683c8f4fcd781a38a8d623bec20e7

profile
프론트엔드 개발자 👩🏻‍💻

0개의 댓글