노트

상태 찢어짐(Tearing)

Tearing

프론트엔드#react · 연결된 개념 5개

쉽게 말하면

상태 찢어짐은 한 화면 속 컴포넌트들이 같은 값을 서로 다른 순간에 읽어서 어긋나 보이는 현상이에요. 파노라마 사진을 찍는 사이 사람이 움직여 한 장에 같은 사람이 두 번 찍히는 것과 비슷하죠.

비유의 한계 useState처럼 React가 관리하는 상태에선 생기지 않아요. 동시성 렌더링이 중간에 양보하는 사이 외부 스토어 값이 바뀔 때 생기고, useSyncExternalStore가 렌더 동안 스냅샷 하나만 보게 해서 막아요.

Tearing은 같은 상태를 읽는 여러 컴포넌트가 한 화면 안에서 서로 다른 시점의 값을 보여 주는 UI 불일치다. 화면이 "찢어진" 것처럼 보인다고 해서 붙은 이름이다(그래픽스에서 반쯤 그린 프레임이 보이는 현상에서 따왔다).

렌더 시작: store = 0
CounterA 렌더 → 0 읽음
(React가 잠시 양보한 사이 store = 1로 변경)
CounterB 렌더 → 1 읽음      →  화면에 0과 1이 동시에
  • React 17까지는 거의 없었다. 렌더가 동기라 한 번 시작하면 끝까지 돌았고, 그 사이에 값이 바뀔 틈이 없었다
  • React 18의 동시성 렌더링에서 생길 수 있게 됐다. 렌더 도중 브라우저에 제어권을 넘기는 사이 외부 값이 바뀔 수 있기 때문이다
  • React 밖의 상태에서 생긴다: Redux·Zustand·MobX 같은 외부 스토어, window 값, 직접 만든 전역 객체. useState처럼 React가 관리하는 상태는 렌더 동안 같은 스냅샷이 보장된다
  • useEffect로 스토어를 구독해 setState로 옮겨 담는 옛 방식은 이 틈을 막지 못한다
  • 해결: useSyncExternalStore가 렌더 동안 하나의 스냅샷(Snapshot)만 보게 하고, 커밋 직전에 값이 바뀌었으면 다시 렌더한다. 결과는 전부 옛 값이거나 전부 새 값 둘 중 하나다

일관된 스냅샷을 보장하는 문제는 DB(Database)의 격리 수준(ACID)이나 분산 시스템의 최종 일관성와 같은 계열의 고민이다.

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • React 상태 갱신

    React는 상태를 직접 고치지 않고 새 값을 setState로 넘겨야 다시 그린다. 이전 값과 Object.is로 비교하므로, 같은 객체를 고친 뒤 넘기면 바뀐 줄 모른다.

  • react와 렌더러의 분리

    React는 컴포넌트를 정의하는 API(Application Programming Interface)를 담은 react 패키지와, 그 컴포넌트 트리를 특정 플랫폼의 화면으로 바꾸는 렌더러(renderer) 패키지로 나뉜다. 브라우저 DOM(Document Object Model)용이 react-dom, iOS·Android 네이티브 뷰용이 react-native다.

  • 서버 렌더에서 훅이 하는 일

    서버 렌더(Server-Side Rendering, SSR)에서도 컴포넌트 함수는 실제로 호출되고, 그 안의 훅도 호출은 된다. 다만 서버에는 커밋(DOM 반영)이 없어서, 렌더 중 동기적으로 값을 계산하는 훅만 일하고 커밋 뒤에 도는 훅은 아무것도 하지 않는다. "이 훅은 클라이언트에서만 동작한다"는 말은 대개 "Effect 안의 일은 클라이언트에서만 일어난다"는 뜻이다.

  • Effect가 필요 없는 경우

    Effect는 외부 시스템과 동기화할 때 쓰는 탈출구(Escape Hatch)다. 렌더 중에 계산할 수 있는 값이나 사용자 이벤트 처리에 Effect를 쓰면 렌더가 한 번 더 일어나고 흐름이 꼬인다. React 공식 문서가 이런 경우를 따로 정리해 두었다.

  • Suspense

    Suspense는 아직 준비되지 않은 하위 트리의 렌더를 잠시 보류(suspend)하고, 그동안 가장 가까운 경계의 fallback을 보여 주는 React의 비동기 렌더링 메커니즘이다. 로딩 상태를 컴포넌트마다 들고 있지 않고 경계에 위임한다.

보기 옵션