노트

상태 찢어짐(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로 비교하므로, 같은 객체를 고친 뒤 넘기면 바뀐 줄 모른다.

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

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

  • Effect가 필요 없는 경우

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

  • Suspense

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

  • 하이드레이션

    하이드레이션(Hydration)은 서버가 미리 렌더해 보낸 HTML(HyperText Markup Language)을 클라이언트에서 다시 만들지 않고, 그 DOM(Document Object Model)에 React 컴포넌트를 연결해 이벤트 핸들러와 상태를 붙이는 과정이다(hydrateRoot). Dan Abramov의 비유로는 "마른 HTML에 상호작용이라는 물을 주는 일"이다.

보기 옵션