[꿀팁] React의 tearing 현상과 React v18의 동시성 렌더링 문제점

in-ch·2024년 1월 6일
0

꿀팁

목록 보기
6/14

서론


What is tearing
리액트의 토론 커뮤니티에 다음과 같은 글이 올라온 적이 있다.

간단하게 요약하자면

React 18의 SuspensestartTransition같은 동시적 렌더링을 사용할 때 다른 작업을 수행하기 위해 렌더링은 일시 중지할 수 있다. 이러한 일시 중지 사이에 다른 상태값의 업데이트가 몰래 삽입되어 렌더링에 사용되는 데이터가 변경될 수 있으며, 이로 인해 UI에 동일한 데이터에 대해 두 개의 다른 값이 표시될 수 있다.라는 내용이였다.

이 글을 통해 tearing이 무엇인지, 그리고 동시성 렌더링이 왜 tearing을 유발하는지, 마지막으로 이를 해결하기 위해 React v18은 무엇을 노력했는지 정리해보려고 한다. 🔥🔥🔥

Tearing이란?


Tearing이란 일반적으로 비디오에서 여러 프레임이 표시되어 비디오가 '찢어진 것'처럼 보이는 현상이다.
위의 이미지를 보면 Tear Point가 2개 있는데 찢어진 것 처럼 한 화면 다른 프레임이 보이는 것을 확인해 볼 수 있다.

그렇다면 React에서 Tearing이란 ?

일반적으로 UI 상에서 Tearing이라고 하면 동일한 상태값 상에서 다른 값들이 표현되는 것을 말한다.

좀 더 React 적으로 말하면 tearing은 애플리케이션의 상태가 비동기적으로 업데이트될 때 UI의 다른 부분이 일관성 없는 상태를 보이는 현상을 말한다.

즉, 애플리케이션의 상태가 변경되고 있지만, 이 변화가 동시에 모든 UI 컴포넌트에 반영되지 않아 일부 컴포넌트가 구식 상태를 표시하는 문제이다.

[이미지 필요]

왜 이러한 문제가 발생할까? 🤨


이러한 현상은 React에만 국한된 문제가 아니라 동시성의 필연적인 결과이다.

그렇다면 React v18 이전의 버전에서는 발생하지 않을까??

React v18 이전의 동기적 렌더링

React v18 이전의 동기적 렌더링의 동기적 렌더링은 다음과 같은 순서로 진행된다.

  1. Start Rendering
    React 트리 렌더링을 시작한다.
    이 단계에서 External Store v1(repository layer)에 요청하여 색상 값을 가져와서 blue를 전달한다. 따라서 해당 컴포넌트가 파랑색으로 렌더링된다.

  2. Continue Rendering
    이때 동시에 렌더링되지 않기 때문에 React의 다른 컴포넌트들도 중간에 렌더링을 멈추지 않고 계속 렌더링한다. 또한 멈추지 않았기 때문에 External Store v1(repository layer)의 상태값은 계속 blue를 가지고 있다. 따라서 다른 모든 컴포넌트는 동일한 상태값을 전달받게 된다.

  3. Finish Rendering
    모든 컴포넌트가 파란색으로 렌더링되고 모두 동일하게 보이는 것을 볼 수 있다. UI는 표시되는 모든 것이 화면의 모든 곳에서 동일한 값으로 렌더링되기 때문에 항상 일관된 상태로 표시된다.

  4. After render, the external store can update
    마지막으로 External Store가 v2로 업데이트 될 수 있다.
    이는 React가 완료되고 다른 작업이 발생할 수 있도록 했기 때문이다.
    External Store가 v2로 업데이트되었기 떄문에 다시 첫번째 단계로 돌아가서 1 ~ 3번 단계를 실행하게 된다.

다음과 같은 렌더링 과정을 보았을 때 Tearing이 발생할 여지가 보이지 않는다.

그렇다면 React v18의 동시성 렌더링은 어떻게 진행될까?

React v18의 동시적 렌더링

  1. Start Rendering
    위의 단계와 마찬가지로 React 트리 렌더링을 시작한다.
    이 단계에서 External Store v1(repository layer)에 요청하여 색상 값을 가져와서 blue를 전달한다. 따라서 해당 컴포넌트가 파랑색으로 렌더링된다.

  2. React yields store updated Data changes to red
    문제는 이 단계에서 발생할 수 있다.
    동시성 렌더링에서는 React가 렌더링을 차단하지 않고 페이지와 상호 작용할 수 있다.

    이 경우 User의 클릭 이벤트로 인해 갑자기 External Store v1(repository layer)External Store v2(repository layer)로 변경되어 값이 Red로 변경될 수가 있다.

  3. Continue Rendering
    다른 컴포넌트가 렌더링될 때 External Store v2로 변경되었기 때문에 값은 red이다. 따라서 첫번째 컴포넌트가 Blue로 렌더링된 것과 다르게 다른 컴포넌트들은 Red로 렌더링이 된다.

  4. UI is inconsistent
    최종적으로 렌더링된 결과를 보면 tearing 즉, 같은 상태값임에도 불과하고 화면 찢어짐 현상이 발생하는 것을 확인해 볼 수 있다.

Tearing이 발생할 수 있는 상황


주로 Tearing이 발생할 수 있는 상황은 주로 여러 컴포넌트가 동시에 상태를 공유할 때 발생한다.

간단한 카운터

const SharedCounter = createContext(0);

function App() {
  const [count, setCount] = useState(0);

  return (
    <SharedCounter.Provider value={{ count, setCount }}>
      <CounterDisplay />
      <CounterButton />
    </SharedCounter.Provider>
  );
}

function CounterDisplay() {
  const { count } = useContext(SharedCounter);
  return <div>Count: {count}</div>;
}

function CounterButton() {
  const { setCount } = useContext(SharedCounter);
  return <button onClick={() => setCount(c => c + 1)}>증가</button>;
}

이 예제에서는 SharedCounter라는 Context를 사용하여 count 상태를 공유하고 있다.
여러 컴포넌트가 이 상태를 동시에 참조하고 수정할 수 있으므로, 상태 업데이트가 비동기적으로 발생할 때 tearing이 발생할 수 있다.

데이터 패칭과 상태 업데이트

function App() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchData().then(data => setUser(data));
  }, []);

  return (
    <div>
      <UserProfile user={user} />
      <UserSettings user={user} setUser={setUser} />
    </div>
  );
}

function UserProfile({ user }) {
  if (!user) return <div>로딩...</div>;
  return <div>{user.name}</div>;
}

function UserSettings({ user, setUser }) {
  if (!user) return <div>로딩...</div>;

  const updateUser = () => {
    // ... Update user logic
  };

  return <button onClick={updateUser}>Update User</button>;
}

이 예제에서는 비동기적으로 사용자 데이터를 패치하고 있다.
데이터가 로드되는 동안 UserProfileUserSettings 컴포넌트는 일관성 없는 상태를 보일 수 있다. 예를 들어, 하나의 컴포넌트는 아직 데이터가 로드되지 않았지만, 다른 컴포넌트는 이미 데이터를 받았을 수 있다.

또한 useEffect를 통해 데이터 패칭을 진행하고 있다. useEffect는 대표적인 부수효과 기능이고 이 스코프 안에서 데이터 패칭을 진행할 경우 불필요한 데이터 패칭을 추가적으로 진행할 여지가 있다.

복잡한 상태 관리

function App() {
  const [state, setState] = useState({ count: 0, text: '' });

  return (
    <div>
      <Counter state={state} setState={setState} />
      <TextInput state={state} setState={setState} />
    </div>
  );
}

function Counter({ state, setState }) {
  return <button onClick={() => setState({ ...state, count: state.count + 1 })}>Increment</button>;
}

function TextInput({ state, setState }) {
  return <input value={state.text} onChange={e => setState({ ...state, text: e.target.value })} />;
}

이 예제에서는 state 객체를 여러 컴포넌트에 걸쳐 공유하고 있다.
Counter 컴포넌트와 TextInput 컴포넌트가 동시에 state를 업데이트하려 할 때, 서로의 업데이트를 덮어쓸 수 있으며, 이는 Tearing으로 이어질 수 있다.

인터럽트 가능한 렌더링(Interruptible Rendering)


그렇다면 사용자 인터럽트가 가능한 렌더링은 무엇일까? 🤨

인터럽트 가능한 렌더링(Interruptible Rendering)은 React 18의 동시성 모델의 핵심 기능 중 하나로 React가 렌더링 작업을 중단하고, 더 중요한 작업에 우선적으로 자원을 할당할 수 있게 한다. 이를 통해 애플리케이션의 반응성을 향상시킬 수 있다.

  • 작업 우선 순위: 사용자 인터렉션과 같은 높은 우선 순위의 작업은 즉시 처리된다.
  • 작업 중단 및 재개: 낮은 우선 순위의 렌더링 작업은 높은 우선 순위의 작업이 발생할 때 중단될 수 있다. 이후 중단된 작업은 다시 재개되어 완료된다.

Auto Batching은 해결 방안이 아닌가?


Tearing과 같은 동시성의 문제점을 해결하기 위한 방안 중 하나로 Auto Batching이 있다고 생각했는데 정확히 말하면 Auto Batching는 렌더링 최적화를 위한 방안일 뿐이지 Tearing과는 상관이 없다.

왜그럴까 ?!?!

Auto Batching이란 React가 더 나은 성능을 위해 여러 개의 state 업데이트를 하나의 리렌더링 (re-render)로 묶는 것을 의미한다.

예를 들어서 다음과 같은 예제가 있다고 하자.
코드 출처

function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    fetchSomething().then(() => {
      setCount(c => c + 1);
      setFlag(f => !f);
    });
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}

해당 코드의 React 17과 React 18의 렌더링 과정을 그림으로 표현하면 다음과 같다.

React 17에서 과정

  1. setCount 호출로 인해 State가 변경됨. -> 1차 렌더링 발생 !!!
  2. 1차 렌더링이 진행되는 과정에서 setFlag 호출 -> 2차 렌더링 발생 !!

총 2번의 렌더링이 발생한다.

React 18에서 과정

  1. setCount 호출 -> 아직 렌더링 미발생
  2. setFlag 호출 -> 렌더링 발생 !!

총 한 번의 렌더링이 발생한다.

한계

단, 문제가 있는데 Auto Batching같은 경우 같은 스코프 내에서만 가능하다.

즉, 동시성 렌더링에서는 각각의 업데이트가 서로 독립적으로 실행될 수 있고, 이로 인해 여러 업데이트가 동시에 일어날 수 있는데, 동시성 렌더링 환경에서는 auto batching이 여전히 존재하지만, 각각의 업데이트가 병렬로 처리될 수 있어서 모든 업데이트가 한 번에 묶여서 처리되는 것은 아니다.

마무리


동시성 렌더링은 매우 좋은 기능이긴 하지만 Tearing같은 현상이 발생할 수 있음을 유의하자 !!

끝 !!

profile
인치

0개의 댓글