본문 바로가기
React

React 18 useSyncExternalStore를 이용하여 전역 상태 구현하기 ex) recoil, zustand, jotai

by 도현위키 2023. 6. 3.

react 18

 

들어가며

React 18에 새로 나온 useSyncExternalStore를 이용하여 recoil jotai, zustand 같은 전역 상태 라이브러리를 간단하게 만들어보는 작업을 하려고 한다. 먼저 useSyncExternalStore에 대해서 알아보자

 

useSyncExternalStore는 저장소(store)에 대한 업데이트를 강제 동기화하여 외부 저장소가 concurrent reading(동시 읽기)를 지원할 수 있도록 하는 hook이다.

 

쉽게 말해서, useSyncExternalStore hook은 react가 아닌 외부 저장소(ex, 바닐라 자바스크립트) 값을 읽어드리고 구독할 때 사용된다. 외부 저장소 값의 변화를 추적하고, 리액트의 동시성 상태 변화에 대응할 수 있다.

 

 

useSyncExternalStore

아래는 useSyncExternalStore의 Type이다.

export function useSyncExternalStore<Snapshot>(
        subscribe: (onStoreChange: () => void) => () => void,
        getSnapshot: () => Snapshot,
        getServerSnapshot?: () => Snapshot,
): Snapshot;

 

먼저 subscribe는 onStoreChange라는 함수를 인자로 받는다. onStoreChange 함수가 실행되면 리액트에서 변경된 상태를 렌더링을 시켜주는 함수이다. 그렇다면 이 함수를 외부 저장소에 등록을 해주고, 외부 저장소의 상태가 변화했을 때 해당 함수를 실행시켜 주면 리액트에서 렌더링을 시켜준다.

 

다음은 getSnapshot이다. getSnapshot은 현재 외부 저장소의 상태를 넣어주면 된다. 그러면 리액트에서 snapshot으로 해당 상태를 가지고 있는다.

 

마지막으로 getServerSnapshot은 서버사이드 렌더링 시 가지고 있던 snapshot을 리턴하는 함수이다. 

 

 

외부 저장소

그럼 이제 전역 상태 관리를 하는 외부 저장소를 만들어보자. 아래는 간단하게 만든 외부 저장소이다.

 

type Fn = () => void;
type UpdateFn<T> = (state: T) => T;

export type State<T> = {
  getState: () => T;
  setState: (update: UpdateFn<T> | T) => void;
  subscribe: (callback: Fn) => Fn;
};

function isUpdateFn<T>(value: UpdateFn<T> | T): value is UpdateFn<T> {
  return typeof value === 'function';
}

export function externalStore<T>(initialState: T): State<T> {
  let state = initialState;
  const callbacks = new Set<Fn>();
  
  function subscribe(callback: Fn): Fn {
    callbacks.add(callback);
    return () => callbacks.delete(callback);
  }

  function getState() {
    return state;
  }

  function setState(update: UpdateFn<T> | T) {
    state = isUpdateFn<T>(update) ? update(state) : update;
    callbacks.forEach((cb) => cb());
  }

  return {
    getState,
    setState,
    subscribe,
  };
}

외부 저장소 externalStore는 initialState 값을 받아와서 state 변수에 저장한다. 그리고 함수들을 하나씩 설명하면 다음 과 같다.

 

- subscribe: 외부에서 callback 함수 (onChangeStore)를 받아서 callbacks 변수에 저장하는 함수

- getState: 현재 상태를 반환하는 함수 (getSnapshot)

- setState: 매개변수로 함수 또는 값을 받아와서 현재 상태를 변경하는 함수, useSyncExternalStore에서 전달받은 onChangeStore 함수를 실행시켜 react에게 상태가 변경되었다는 것을 알리는 역할을 함

 

 

이렇게 만든 외부 저장소를 useSyncExternalStore와 함께 사용해보겠다.

export function useStore<T>(state: State<T>) {
    const store = useSyncExternalStore(
        state.subscribe,
        state.getState,
        state.getState
    )
    
    return [store, state.setState] as const
}

 

useStore에 인자로는 위에서 생성한 외부 저장소 값이 들어갈 것이고, 외부저장소의 subscribe, getState를 useSyncExternalStore에 넣어준다. 이렇게 되면 간단한 전역 상태 라이브러리를 만들게 되었다.  그럼 방금 만든 전역 상태 라이브러리를 이용해서, 간단한 투두리스트를 만들어보자.

 

 

 

투두리스트 앱 만들기

 

투두리스트 전역 상태 만들기

아래는 useTodos hook에 todo 전역 상태와, todo 상태를 변경시켜 주는 addTodo, deleteTodo를 만들었다.

위에서 만든 externalStore를 이용하여 todoStore를 생성하였고, 생성된 todoStore를 useStore hook에 넣어줘서 useSyncExternalStore와 연결시켜 주었다.

// hoods/useTodos.ts

interface Todo {
  id: number
  title: string
}

const todoStore = externalStore<Todo[]>([])

export const useTodos = () => {
    const [todos, setTodos] = useStore(todoStore)
    
    const addTodo = (todo: Todo) => {
        setTodos((prev) => [...prev, todo])
    }
    
    const deleteTodo = (todoId: number) => {
        setTodos((prev) => prev.filter((todo) => todo.id !== todoId))
    }
    
    return {
      todos,
      addTodo,
      deleteTodo
    }
}

 

 

투두리스트 ui 만들기

 

아래에서 Todos 컴포넌트에서는 todos를 이용하여 todo list를 보여주고 있고, AddTodo 컴포넌트에서는 addTodo를 이용하여 todo 리스트에 새로운 todo를 추가하는 작업을 하고 있다.

 

이렇게 서로 다른 컴포넌트에서 하나의 전역 상태를 관리할 수 있는 전역 상태를 만들게 되었다.

// components/Todos.tsx

import { useState } from "react";
import { useTodos } from "./useTodoStore";

export const Todos = () => {
  const { todos } = useTodos();

  return (
    <div>
      {todos.map((todo) => (
        <div key={todo.id}>{todo.title}</div>
      ))}
      <AddTodo />
    </div>
  );
};

export const AddTodo = () => {
  const { addTodo } = useTodos();
  const [value, setValue] = useState("");

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  };

  const onAddTodo = () => {
    addTodo({
      id: Math.random(),
      title: value,
    });
  };

  return (
    <div>
      <input value={value} onChange={onChange} />
      <button onClick={onAddTodo}>추가</button>
    </div>
  );
};

 

 

댓글