React18의 Tearing현상과 이를 해결하기 위한 useSyncExternalStore에 대해 알아봅시다.
원문 : https://blog.saeloun.com/2021/12/30/react-18-usesyncexternalstore-api
Concurrent rendering and startTransition API
Internal stores
Tearing
concurrent(동시) 렌더링에서 스토어의 데이터 'blue'를 이용해 처음에 렌더링한 색상은 파란색입니다.
렌더링 도중 React가 스토어를 업데이트하면 데이터는 'red'으로 업데이트됩니다.
React는 업데이트된 값 red를 활용하여 렌더링을 계속합니다.
이는 '티어링'으로 알려진 UI의 불일치를 유발합니다.
기존 라이브러리들은 렌더링 시 ref를 이전 렌더링에서 사용한 데이터로 참조하는 패턴을 사용하고 있었지만,
리액트 코어 팀은 ref를 공유 데이터, 전역변수와 같이 불안전한 데이터로 생각하고 있었습니다.
그래서 렌더링 중간에 ref를 읽고 쓰는 것은 당연히 버그가 생기는게 맞다고 생각했었습니다.
- 렌더링 중에 ref 값을 읽거나 쓰는 것은 지연 초기화 패턴(the lazy initialization pattern을 구현하는 경우에만 안전합니다.
- ref는 다른 것과 마찬가지로 변경 가능한 소스(mutable source)이므로 다른 타입의 읽기는 안전하지 않습니다(unsafe).
- 지연 초기화 패턴이 아닌 writing는 사실상 부작용(side effects)이 있기 때문에 안전하지 않습니다(unsafe).
이는 수 많은 기존 오픈소스 라이브러리 메인테이너들에과 사용자에게 어려움을 줄 것이었으므로,
많은 논의 끝에 useMutableSource 훅이 재설계되고 이름이 useSyncExternalStore로 변경되었습니다.
useSyncExternalStore는 이름처럼 동기적으로 렌더링하게 되었고,
이는 기존 오픈소스 라이브러리들에 큰 영향을 끼치지 않게 되었습니다.
Understanding useSyncExternalStore hook
마이그레이션을 단순화하기 위해 React는 새로운 패키지 use-sync-external-store를 제공합니다.
To help simplify the migration, React provides a new package use-sync-external-store
이 패키지의 shim(/shim)는 어떤 리액트의 버전과도 잘 동작합니다.
import {useSyncExternalStore} from 'react';
// or
// Backwards compatible shim
import {useSyncExternalStore} from 'use-sync-external-store/shim';
//Basic usage. getSnapshot must return a cached/memoized result
useSyncExternalStore(
subscribe: (callback) => Unsubscribe
getSnapshot: () => State
) => State
// Selecting a specific field using an inline getSnapshot
const selectedField = useSyncExternalStore(store.subscribe, () => store.getSnapshot().selectedField);
useSyncExternalStore 훅은 두 개의 함수를 필요로 합니다.
- 콜백 함수를 등록하는 'subscribe' 함수
- 'getSnapshot'은 구독 값이 마지막 시간 이후 변경되었는지, 렌더링되었는지 확인하는 데 사용됩니다.
- 문자열이나 숫자와 같은 변경할 수 없는 값이거나 캐시/메모된 객체여야 합니다.
- 그런 다음 훅에서 변경할 수 없는 값을 반환합니다.
getSnapshot 결과를 자동으로 메모하는 API 버전입니다.
import {useSyncExternalStoreWithSelector} from 'use-sync-external-store/with-selector';
const selection = useSyncExternalStoreWithSelector(
store.subscribe,
store.getSnapshot,
getServerSnapshot,
selector,
isEqual
);
Daishi Kato의 React 18 for External Store Libraries talk 강연에서 논의된 예를 확인해 보겠습니다.
import React, { useState, useEffect, useCallback, startTransition } from "react";
// library code
const createStore = (initialState) => {
let state = initialState;
const getState = () => state;
const listeners = new Set();
const setState = (fn) => {
state = fn(state);
listeners.forEach((l) => l());
}
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
}
return {getState, setState, subscribe}
}
const useStore = (store, selector) => {
const [state, setState] = useState(() => selector(store.getState()));
useEffect(() => {
const callback = () => setState(selector(store.getState()));
const unsubscribe = store.subscribe(callback);
callback();
return unsubscribe;
}, [store, selector]);
return state;
}
//Application code
const store = createStore({count: 0, text: 'hello'});
const Counter = () => {
const count = useStore(store, useCallback((state) => state.count, []));
const inc = () => {
store.setState((prev) => ({...prev, count: prev.count + 1}))
}
return (
<div>
{count} <button onClick={inc}>+1</button>
</div>
);
}
const TextBox = () => {
const text = useStore(store, useCallback((state) => state.text, []));
const setText = (event) => {
store.setState((prev) => ({...prev, text: event.target.value}))
}
return (
<div>
<input value={text} onChange={setText} className='full-width'/>
</div>
);
}
const App = () => {
return(
<div className='container'>
<Counter />
<Counter />
<TextBox />
<TextBox />
</div>
)
}
import { useSyncExternalStore } from 'react';
const useStore = (store, selector) => {
return useSyncExternalStore(
store.subscribe,
useCallback(() => selector(store.getState(), [store, selector]))
)
}
외부 저장소에서 useSyncExternalStore 훅으로 마이그레이션하는 것은 쉽고 잠재적인 문제를 피하기 위해 권장됩니다.
모든 인터페이스가 동일하기 때문에, 다른 라이브러리로 마이그레이션 하는것도 쉬울듯...
어떤 라이브러리들이 concurrent rendering의 영향을 받을까요?
- 렌더링하는 동안 변경 가능한 외부 데이터에 액세스하지 않고 React 프롭, 상태 또는 컨텍스트를 사용하여 정보만 전달하는 컴포넌트 및 사용자 정의 훅이 있는 라이브러리는 영향을 받지 않습니다.
- 데이터 가져오기, 상태 관리 또는 스타일링(Redux, MobX, Relay)을 처리하는 라이브러리가 영향을 받습니다.
- React 외부에 상태를 저장하기 때문입니다.
- 동시 렌더링을 사용하면 React가 알지 못하는 사이에 이러한 외부 데이터 저장소를 렌더링 중에 업데이트할 수 있습니다.
'FrontEnd' 카테고리의 다른 글
CSS 애니메이션 성능 분석 및 개선하기 (0) | 2022.08.02 |
---|---|
자바로 알아보는 결합도(커플링) (0) | 2022.08.01 |
리액트의 의존성 주입 with Context API (dependency injection) (0) | 2022.08.01 |
리액트 프로젝트의 결합도를 관리하는 방법 (3) | 2022.07.31 |
리액트 컴포넌트의 응집도를 관리하는 방법 (0) | 2022.07.30 |