React

[React] useSyncExternalStore 알아보기

joseph0926 2024. 11. 9. 08:14

외부 상태와 결합하기 위해서는?

React 앱을 구축하다 보면 종종 외부 상태와 결합해야 하는 상황이 찾아옵니다.

예를 들어 브라우저 API가 있습니다. (geolocation, localStorage)

또는 서드파티 라이브러리들도 있습니다. (WebSocket 연결을 관리하는 라이브러리)

 

이러한 외부 상태들을 React 앱에 통합하려 하면 일반적인 방법으로는 불가능합니다.

 

// 🚫 잘못된 방법
function OnlineStatus() {
    const [isOnline, setIsOnline] = useState(navigator.onLine);
    
    useEffect(() => {
        const updateStatus = () => {
            setIsOnline(navigator.onLine);
        };
        
        window.addEventListener('online', updateStatus);
        window.addEventListener('offline', updateStatus);
        
        return () => {
            window.removeEventListener('online', updateStatus);
            window.removeEventListener('offline', updateStatus);
        };
    }, []);
    
    return <div>상태: {isOnline ? '온라인' : '오프라인'}</div>;
}

 

왜냐하면 React의 핵심 원칙 중 하나는 단방향 데이터 흐름입니다. React 컴포넌트는 state가 변경될 때 자동으로 리렌더링됩니다. 하지만 외부 데이터는 이 시스템 밖에 있어서 React가 변경사항을 자동으로 감지할 수 없습니다.

 

이러한 문제를 해결해 주기 위해 나온 React 훅이 `useSyncExternalStore`입니다.

 

// ✅ useSyncExternalStore를 사용한 올바른 방법
function OnlineStatus() {
    const isOnline = useSyncExternalStore(
        (callback) => {
            window.addEventListener('online', callback);
            window.addEventListener('offline', callback);
            return () => {
                window.removeEventListener('online', callback);
                window.removeEventListener('offline', callback);
            };
        },
        () => navigator.onLine
    );
    
    return <div>상태: {isOnline ? '✅ 온라인' : '❌ 오프라인'}</div>;
}

 

`useSyncExternalStore`를 사용하면

1. React에게 외부 데이터의 변경사항을 알릴 수 있음

2. 변경이 있을 때 자동으로 컴포넌트를 리렌더링

3. React의 렌더링 시스템과 조화롭게 작동

 

useSyncExternalStore란?

useSyncExternalStore is a React Hook that lets you subscribe to an external store.

`useSyncExternalStore`는 외부 스토어의 데이터를 읽을 때 활용되는 React 훅입니다.

또한 이러한 솔루션을 사용하는 이유는 앞서 언급드렸듯이 React가 단방향 데이터 흐름 메커니즘을 사용하기 때문입니다. 

 

이 말을 듣고 이러한 의구심이 생길 수도 있습니다.

"useEffect를 사용하면 해결되지 않을까?"

실제로 이러한 외부 데이터 등과 같은 사이드 이펙트는 `useEffect` 등을 이용해서 처리하라고 공식 문서에도 나와 있습니다.

그렇다면 `useEffect`를 사용해서 해결하는 것과 `useSyncExternalStore`를 사용해서 해결하는 것에는 어떠한 차이점이 존재할까요?

 

useEffect vs useSyncExternalStore

 

`useEffect`로도 외부 데이터를 구독할 수는 있습니다. 하지만 이는 몇 가지 문제점을 가지고 있습니다

 

// 🚫 useEffect를 사용한 방식
function TodoList() {
    const [todos, setTodos] = useState([]);
    
    useEffect(() => {
        let ignore = false;
        
        todosStore.subscribe(newTodos => {
            if (!ignore) {
                setTodos(newTodos); // 경쟁 조건 발생 가능
            }
        });
        
        return () => {
            ignore = true;
        };
    }, []);
    
    return (
        <ul>
            {todos.map(todo => (
                <li key={todo.id}>{todo.text}</li>
            ))}
        </ul>
    );
}

 

1. 경쟁 조건(Race Condition) 문제

- `useEffect`는 비동기적으로 실행되어 여러 업데이트가 동시에 발생할 수 있음

- 이로 인해 예상치 못한 순서로 상태가 업데이트될 수 있음

 

2. 테어링(Tearing) 현상

- 동시에 여러 컴포넌트가 서로 다른 외부 데이터 버전을 보여줄 수 있음

- 이는 일관성 없는 UI를 초래할 수 있음

 

// ✅ useSyncExternalStore를 사용한 방식
function TodoList() {
    const todos = useSyncExternalStore(
        todosStore.subscribe,
        () => todosStore.getSnapshot(),
        () => [] // SSR을 위한 초기값
    );
    
    return (
        <ul>
            {todos.map(todo => (
                <li key={todo.id}>{todo.text}</li>
            ))}
        </ul>
    );
}

 

`useSyncExternalStore`가 경쟁 조건을 방지하는 방법

 

1. 동기적 스냅샷

   - `getSnapshot`은 항상 동기적으로 현재 값을 반환해야 함

   - 비동기 작업의 결과를 기다리지 않고 즉시 현재 상태를 반환

const todosStore = {
     todos: [],
     getSnapshot() {
       // 항상 동기적으로 현재 값 반환
       return this.todos;
     },
     addTodo(text) {
       this.todos = [...this.todos, { id: Date.now(), text }];
       this.notify();
     }
   };

 

2. 렌더링 커밋 전 검증

   - React는 렌더링을 커밋하기 전에 스냅샷이 여전히 최신인지 확인

   - 값이 변경되었다면 리렌더링을 다시 시작

 

3. 단일 진실 공급원(Single Source of Truth)

   - 외부 스토어는 단일 진실 공급원으로 동작

   - 모든 구독자는 동일한 스토어 인스턴스를 참조

 

4. 동기적 업데이트 전파

   - 스토어 변경 시 모든 구독자에게 동기적으로 알림

   - 비동기 작업의 결과는 스토어를 통해 안전하게 전파

 

즉,

- 모든 컴포넌트가 항상 일관된 데이터를 보게 됨

- 비동기 작업의 결과가 예측 가능한 순서로 처리됨

- 테어링(tearing) 현상이 방지

 

추가 고려사항

 

- 서버 사이드 렌더링(SSR) 지원: `getServerSnapshot` 파라미터를 통해 SSR 시 초기값 제공이 가능합니다.

- 커스텀 훅으로 추상화: 재사용 가능한 로직으로 분리하여 여러 컴포넌트에서 활용할 수 있습니다.

- 상태 관리 라이브러리와의 통합: Redux, MobX 등의 외부 상태 관리 라이브러리와 seamless한 통합이 가능합니다.

 

따라서 `useEffect`와 달리 수동으로 경쟁 조건을 관리할 필요가 없으며, React의 렌더링 시스템과 완벽하게 동기화된 상태를 유지할 수 있습니다.