React · React Native 상태 관리 · 1.0

상태 공유,훅 하나로끝.

선언 한 줄로 스토어를 만들고 어느 컴포넌트에서나 쓰세요. Provider·액션·리듀서가 필요 없습니다. 컴포넌트는 자신이 고른 값이 바뀔 때만 렌더하고, 저장은 원할 때만 일어납니다.

Terminal
npm install @kkwaenggwari/restate

실시간 — 실제 restate 스토어

useCart.set(cart => ({ ...cart, items: cart.items + 1, total: cart.total + 12 }))

items와 total이 바뀌어 세 카드만 렌더합니다. 메모는 그대로입니다.

01 — 편함

같은 기능, 더 적은 개념.

같은 과제를 네 가지 방식으로 나란히 구현했습니다. 숫자는 여기 표시한 코드에서 사이트를 빌드할 때 직접 계산합니다.

공유 카운터

두 컴포넌트가 숫자 하나를 읽고 바꿉니다.

Redux Toolkit React context로 제공한 스토어에 액션을 dispatch하고 slice에서 갱신을 처리합니다. 훌륭한 DevTools와, 큰 팀에 맞는 엄격하고 예측 가능한 갱신 흐름이 강점입니다.

Zustand create()로 스토어 훅을 만들고 setState로 바꿉니다. 아주 작고 유연하며, 영속 저장과 DevTools를 위한 미들웨어가 있습니다.

Jotai atom과 파생 atom을 조합하고, 필요하면 Provider 안에서 씁니다. 작은 상태 조각 사이의 세밀한 의존 관계를 잘 표현합니다.

Redux Toolkit29 줄 · 5 개 API
JavaScript / JSX
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useDispatch, useSelector } from 'react-redux';

const slice = createSlice({
  name: 'count',
  initialState: 0,
  reducers: { increment: (count) => count + 1 },
});
const store = configureStore({
  reducer: { count: slice.reducer },
});

function Counter() {
  const count = useSelector((state) => state.count);
  const dispatch = useDispatch();
  return <button onClick={() => dispatch(slice.actions.increment())}>{count}</button>;
}

export default function App() {
  return (
    <Provider store={store}>
      <Counter />
      <Counter />
    </Provider>
  );
}
Zustand16 줄 · 1 개 API
JavaScript / JSX
import { create } from 'zustand';

const useCount = create(() => ({ count: 0 }));
const increment = () =>
  useCount.setState((s) => ({
    count: s.count + 1,
  }));

function Counter() {
  const count = useCount((s) => s.count);
  return <button onClick={increment}>{count}</button>;
}

export default function App() {
  return (
    <>
      <Counter />
      <Counter />
    </>
  );
}
Jotai13 줄 · 2 개 API
JavaScript / JSX
import { atom, useAtom } from 'jotai';

const countAtom = atom(0);

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  return <button onClick={() => setCount((n) => n + 1)}>{count}</button>;
}

export default function App() {
  return (
    <>
      <Counter />
      <Counter />
    </>
  );
}

restate 선언 하나가 스토어이자 훅입니다.

restate13 줄 · 1 개 API
JavaScript / JSX
import { restate } from '@kkwaenggwari/restate';

const useCount = restate(0);

function Counter() {
  const count = useCount();
  return <button onClick={() => useCount.set((n) => n + 1)}>{count}</button>;
}

export default function App() {
  return (
    <>
      <Counter />
      <Counter />
    </>
  );
}

이 과제에서는 Zustand와 Jotai도 작고 Provider가 필요 없습니다. 차이는 저장과 문서별 상태가 들어오면서 커집니다.

필드 하나 선택

본문만 바뀔 때 제목 컴포넌트는 렌더하지 않아야 합니다.

Redux Toolkit React context로 제공한 스토어에 액션을 dispatch하고 slice에서 갱신을 처리합니다. 훌륭한 DevTools와, 큰 팀에 맞는 엄격하고 예측 가능한 갱신 흐름이 강점입니다.

Zustand create()로 스토어 훅을 만들고 setState로 바꿉니다. 아주 작고 유연하며, 영속 저장과 DevTools를 위한 미들웨어가 있습니다.

Jotai atom과 파생 atom을 조합하고, 필요하면 Provider 안에서 씁니다. 작은 상태 조각 사이의 세밀한 의존 관계를 잘 표현합니다.

Redux Toolkit26 줄 · 4 개 API
JavaScript / JSX
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useSelector } from 'react-redux';

const article = createSlice({
  name: 'article',
  initialState: { title: 'Hello', body: '' },
  reducers: {
    editBody: (i) => {
      i.body += '!';
    },
    editTitle: (i) => {
      i.title += '!';
    },
  },
});
const store = configureStore({ reducer: article.reducer });

function Title() {
  const title = useSelector((i) => i.title);
  return <h3>{title}</h3>;
}

export default function App() {
  return (
    <Provider store={store}>
      <Title />
      <button onClick={() => store.dispatch(article.actions.editBody())}>body + !</button>
      <button onClick={() => store.dispatch(article.actions.editTitle())}>title + !</button>
    </Provider>
  );
}
Zustand17 줄 · 1 개 API
JavaScript / JSX
import { create } from 'zustand';

const useArticle = create(() => ({ title: 'Hello', body: '' }));

function Title() {
  const title = useArticle((i) => i.title);
  return <h3>{title}</h3>;
}

export default function App() {
  return (
    <>
      <Title />
      <button
        onClick={() =>
          useArticle.setState((i) => ({
            body: i.body + '!',
          }))
        }
      >
        body + !
      </button>
      <button
        onClick={() =>
          useArticle.setState((i) => ({
            title: i.title + '!',
          }))
        }
      >
        title + !
      </button>
    </>
  );
}
Jotai19 줄 · 3 개 API
JavaScript / JSX
import { atom, useAtomValue, useSetAtom } from 'jotai';

const articleAtom = atom({ title: 'Hello', body: '' });
const titleAtom = atom((get) => get(articleAtom).title);

function Title() {
  const title = useAtomValue(titleAtom);
  return <h3>{title}</h3>;
}

export default function App() {
  const setArticle = useSetAtom(articleAtom);
  return (
    <>
      <Title />
      <button
        onClick={() =>
          setArticle((i) => ({
            ...i,
            body: i.body + '!',
          }))
        }
      >
        body + !
      </button>
      <button
        onClick={() =>
          setArticle((i) => ({
            ...i,
            title: i.title + '!',
          }))
        }
      >
        title + !
      </button>
    </>
  );
}

restate 이미 쓰는 같은 훅에 selector를 넘깁니다.

restate17 줄 · 1 개 API
JavaScript / JSX
import { restate } from '@kkwaenggwari/restate';

const useArticle = restate({ title: 'Hello', body: '' });

function Title() {
  const title = useArticle((i) => i.title);
  return <h3>{title}</h3>;
}

export default function App() {
  return (
    <>
      <Title />
      <button
        onClick={() =>
          useArticle.set((i) => ({
            ...i,
            body: i.body + '!',
          }))
        }
      >
        body + !
      </button>
      <button
        onClick={() =>
          useArticle.set((i) => ({
            ...i,
            title: i.title + '!',
          }))
        }
      >
        title + !
      </button>
    </>
  );
}

여기 모든 도구가 선택을 지원합니다. restate는 별도 선택 훅이나 파생 atom 대신 같은 훅 호출을 씁니다.

초안 저장

시작할 때 저장된 초안을 복원하고, 버튼으로 저장하고, 저장 중·저장 안 됨·실패 상태를 보여 줍니다.

Redux Toolkit React context로 제공한 스토어에 액션을 dispatch하고 slice에서 갱신을 처리합니다. 훌륭한 DevTools와, 큰 팀에 맞는 엄격하고 예측 가능한 갱신 흐름이 강점입니다.

Zustand create()로 스토어 훅을 만들고 setState로 바꿉니다. 아주 작고 유연하며, 영속 저장과 DevTools를 위한 미들웨어가 있습니다.

Jotai atom과 파생 atom을 조합하고, 필요하면 Provider 안에서 씁니다. 작은 상태 조각 사이의 세밀한 의존 관계를 잘 표현합니다.

Redux Toolkit84 줄 · 4 개 API
JavaScript / JSX
import { useEffect, useState } from 'react';
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useSelector } from 'react-redux';

function openDraftDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('draft-example', 1);
    let blocked = false;
    request.onupgradeneeded = () => {
      request.result.createObjectStore('drafts');
    };
    request.onblocked = () => {
      blocked = true;
      reject(new Error('Close other tabs to upgrade storage'));
    };
    request.onerror = () => reject(request.error);
    request.onsuccess = () => {
      const db = request.result;
      if (blocked) {
        db.close();
        return;
      }
      db.onversionchange = () => db.close();
      resolve(db);
    };
  });
}

async function storedDraft(mode, text) {
  const db = await openDraftDB();
  try {
    return await new Promise((resolve, reject) => {
      const tx = db.transaction('drafts', mode);
      const store = tx.objectStore('drafts');
      const request = mode === 'readonly' ? store.get('draft') : store.put(text, 'draft');
      tx.oncomplete = () => resolve(request.result);
      tx.onabort = () => reject(tx.error || new Error('Storage aborted'));
    });
  } finally {
    db.close();
  }
}

const draft = createSlice({
  name: 'draft',
  initialState: '',
  reducers: { edit: (_, action) => action.payload },
});
const store = configureStore({ reducer: { draft: draft.reducer } });
const hydrated = storedDraft('readonly').then((saved) => {
  if (saved === undefined) return;
  if (typeof saved !== 'string') throw new Error('Invalid draft');
  const value = saved;
  store.dispatch(draft.actions.edit(value));
});
// Observe errors immediately, even before the component mounts.
hydrated.catch(() => {});

function DraftEditor() {
  const text = useSelector((state) => state.draft);
  const [ready, setReady] = useState(false);
  const [saving, setSaving] = useState(false);
  const [message, setMessage] = useState('Loading');
  useEffect(() => {
    let active = true;
    hydrated.then(
      () => {
        if (active) {
          setReady(true);
          setMessage('Ready');
        }
      },
      () => {
        if (active) setMessage('Storage error');
      },
    );
    return () => {
      active = false;
    };
  }, []);
  async function save() {
    setSaving(true);
    try {
      await storedDraft('readwrite', store.getState().draft);
      setMessage('Saved');
    } catch {
      setMessage('Save failed');
    } finally {
      setSaving(false);
    }
  }
  return (
    <>
      <label>
        Draft
        <textarea
          value={text}
          disabled={!ready || saving}
          onChange={(event) => {
            const value = event.target.value;
            store.dispatch(draft.actions.edit(value));
            setMessage('Edited');
          }}
        />
      </label>
      <output>{text}</output>
      <button disabled={!ready || saving} onClick={save}>
        Save
      </button>
      <p>{message}</p>
    </>
  );
}

export default function App() {
  return (
    <Provider store={store}>
      <DraftEditor />
    </Provider>
  );
}
Zustand79 줄 · 1 개 API
JavaScript / JSX
import { useEffect, useState } from 'react';
import { create } from 'zustand';

function openDraftDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('draft-example', 1);
    let blocked = false;
    request.onupgradeneeded = () => {
      request.result.createObjectStore('drafts');
    };
    request.onblocked = () => {
      blocked = true;
      reject(new Error('Close other tabs to upgrade storage'));
    };
    request.onerror = () => reject(request.error);
    request.onsuccess = () => {
      const db = request.result;
      if (blocked) {
        db.close();
        return;
      }
      db.onversionchange = () => db.close();
      resolve(db);
    };
  });
}

async function storedDraft(mode, text) {
  const db = await openDraftDB();
  try {
    return await new Promise((resolve, reject) => {
      const tx = db.transaction('drafts', mode);
      const store = tx.objectStore('drafts');
      const request = mode === 'readonly' ? store.get('draft') : store.put(text, 'draft');
      tx.oncomplete = () => resolve(request.result);
      tx.onabort = () => reject(tx.error || new Error('Storage aborted'));
    });
  } finally {
    db.close();
  }
}

const useDraft = create(() => ({ text: '' }));
const hydrated = storedDraft('readonly').then((saved) => {
  if (saved === undefined) return;
  if (typeof saved !== 'string') throw new Error('Invalid draft');
  const value = saved;
  useDraft.setState({ text: value });
});
// Observe errors immediately, even before the component mounts.
hydrated.catch(() => {});

function DraftEditor() {
  const text = useDraft((state) => state.text);
  const [ready, setReady] = useState(false);
  const [saving, setSaving] = useState(false);
  const [message, setMessage] = useState('Loading');
  useEffect(() => {
    let active = true;
    hydrated.then(
      () => {
        if (active) {
          setReady(true);
          setMessage('Ready');
        }
      },
      () => {
        if (active) setMessage('Storage error');
      },
    );
    return () => {
      active = false;
    };
  }, []);
  async function save() {
    setSaving(true);
    try {
      await storedDraft('readwrite', useDraft.getState().text);
      setMessage('Saved');
    } catch {
      setMessage('Save failed');
    } finally {
      setSaving(false);
    }
  }
  return (
    <>
      <label>
        Draft
        <textarea
          value={text}
          disabled={!ready || saving}
          onChange={(event) => {
            const value = event.target.value;
            useDraft.setState({ text: value });
            setMessage('Edited');
          }}
        />
      </label>
      <output>{text}</output>
      <button disabled={!ready || saving} onClick={save}>
        Save
      </button>
      <p>{message}</p>
    </>
  );
}

export default function App() {
  return <DraftEditor />;
}
Jotai80 줄 · 3 개 API
JavaScript / JSX
import { useEffect, useState } from 'react';
import { atom, createStore, useAtomValue } from 'jotai';

function openDraftDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('draft-example', 1);
    let blocked = false;
    request.onupgradeneeded = () => {
      request.result.createObjectStore('drafts');
    };
    request.onblocked = () => {
      blocked = true;
      reject(new Error('Close other tabs to upgrade storage'));
    };
    request.onerror = () => reject(request.error);
    request.onsuccess = () => {
      const db = request.result;
      if (blocked) {
        db.close();
        return;
      }
      db.onversionchange = () => db.close();
      resolve(db);
    };
  });
}

async function storedDraft(mode, text) {
  const db = await openDraftDB();
  try {
    return await new Promise((resolve, reject) => {
      const tx = db.transaction('drafts', mode);
      const store = tx.objectStore('drafts');
      const request = mode === 'readonly' ? store.get('draft') : store.put(text, 'draft');
      tx.oncomplete = () => resolve(request.result);
      tx.onabort = () => reject(tx.error || new Error('Storage aborted'));
    });
  } finally {
    db.close();
  }
}

const draftAtom = atom('');
const store = createStore();
const hydrated = storedDraft('readonly').then((saved) => {
  if (saved === undefined) return;
  if (typeof saved !== 'string') throw new Error('Invalid draft');
  const value = saved;
  store.set(draftAtom, value);
});
// Observe errors immediately, even before the component mounts.
hydrated.catch(() => {});

function DraftEditor() {
  const text = useAtomValue(draftAtom, { store });
  const [ready, setReady] = useState(false);
  const [saving, setSaving] = useState(false);
  const [message, setMessage] = useState('Loading');
  useEffect(() => {
    let active = true;
    hydrated.then(
      () => {
        if (active) {
          setReady(true);
          setMessage('Ready');
        }
      },
      () => {
        if (active) setMessage('Storage error');
      },
    );
    return () => {
      active = false;
    };
  }, []);
  async function save() {
    setSaving(true);
    try {
      await storedDraft('readwrite', store.get(draftAtom));
      setMessage('Saved');
    } catch {
      setMessage('Save failed');
    } finally {
      setSaving(false);
    }
  }
  return (
    <>
      <label>
        Draft
        <textarea
          value={text}
          disabled={!ready || saving}
          onChange={(event) => {
            const value = event.target.value;
            store.set(draftAtom, value);
            setMessage('Edited');
          }}
        />
      </label>
      <output>{text}</output>
      <button disabled={!ready || saving} onClick={save}>
        Save
      </button>
      <p>{message}</p>
    </>
  );
}

export default function App() {
  return <DraftEditor />;
}

restate persistedRestate가 자동으로 복원하고, useStatus가 저장 중·미저장 변경·오류를 알려 줍니다.

restate18 줄 · 1 개 API
JavaScript / JSX
import { persistedRestate } from '@kkwaenggwari/restate';

const useDraft = persistedRestate('', {
  key: 'draft',
  validate: (value) => typeof value === 'string',
});

export default function App() {
  const text = useDraft();
  const { dirty, operation, error } = useDraft.useStatus();
  const saving = operation === 'save';
  const message = error ? 'Save failed' : saving ? 'Saving…' : dirty ? 'Edited' : 'Saved';

  return (
    <>
      <label>
        Draft
        <textarea value={text} onChange={(event) => useDraft.set(event.target.value)} />
      </label>
      <output>{text}</output>
      <button disabled={saving} onClick={() => useDraft.save().catch(() => {})}>
        Save
      </button>
      <p>{message}</p>
    </>
  );
}

다른 구현은 같은 IndexedDB 코드를 직접 작성합니다. 각 도구의 영속 미들웨어나 저장소 유틸리티에 어댑터를 쓰면 줄일 수 있으며, 그 코드는 여기서 세지 않았습니다.

문서별 상태

두 문서가 독립된 상태를 갖고, 하나를 닫으면 그 상태를 해제합니다.

Redux Toolkit React context로 제공한 스토어에 액션을 dispatch하고 slice에서 갱신을 처리합니다. 훌륭한 DevTools와, 큰 팀에 맞는 엄격하고 예측 가능한 갱신 흐름이 강점입니다.

Zustand create()로 스토어 훅을 만들고 setState로 바꿉니다. 아주 작고 유연하며, 영속 저장과 DevTools를 위한 미들웨어가 있습니다.

Jotai atom과 파생 atom을 조합하고, 필요하면 Provider 안에서 씁니다. 작은 상태 조각 사이의 세밀한 의존 관계를 잘 표현합니다.

Redux Toolkit42 줄 · 4 개 API
JavaScript / JSX
import { useState } from 'react';
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useSelector } from 'react-redux';

const draft = createSlice({
  name: 'draft',
  initialState: '',
  reducers: { edit: (_, action) => action.payload },
});
const selectionSlice = createSlice({
  name: 'selection',
  initialState: 0,
  reducers: { next: (index) => index + 1 },
});
function createDocument() {
  return {
    store: configureStore({
      reducer: {
        draft: draft.reducer,
        selection: selectionSlice.reducer,
      },
    }),
  };
}

function Document({ doc, name }) {
  const text = useSelector((state) => state.draft);
  const selection = useSelector((state) => state.selection);
  return (
    <section>
      <label>
        {name}
        <input
          value={text}
          onChange={(event) => doc.store.dispatch(draft.actions.edit(event.target.value))}
        />
      </label>
      <output>{text}</output>
      <button onClick={() => doc.store.dispatch(selectionSlice.actions.next())}>
        Selection: {selection}
      </button>
    </section>
  );
}

function DocumentSlot({ name }) {
  const [doc] = useState(createDocument);
  return (
    <Provider store={doc.store}>
      <Document doc={doc} name={name} />
    </Provider>
  );
}

export default function App() {
  const [leftOpen, setLeftOpen] = useState(true);
  return (
    <>
      <button onClick={() => setLeftOpen((open) => !open)}>
        {leftOpen ? 'Close left' : 'Open left'}
      </button>
      {leftOpen && <DocumentSlot name="Left" />}
      <DocumentSlot name="Right" />
    </>
  );
}
Zustand35 줄 · 2 개 API
JavaScript / JSX
import { useState } from 'react';
import { useStore } from 'zustand';
import { createStore } from 'zustand/vanilla';

function createDocument() {
  return {
    draft: createStore(() => ({ text: '' })),
    selection: createStore(() => ({ index: 0 })),
  };
}

function Document({ doc, name }) {
  const text = useStore(doc.draft, (state) => state.text);
  const selection = useStore(doc.selection, (state) => state.index);
  return (
    <section>
      <label>
        {name}
        <input
          value={text}
          onChange={(event) => doc.draft.setState({ text: event.target.value })}
        />
      </label>
      <output>{text}</output>
      <button onClick={() => doc.selection.setState((s) => ({ index: s.index + 1 }))}>
        Selection: {selection}
      </button>
    </section>
  );
}

function DocumentSlot({ name }) {
  const [doc] = useState(createDocument);
  return <Document doc={doc} name={name} />;
}

export default function App() {
  const [leftOpen, setLeftOpen] = useState(true);
  return (
    <>
      <button onClick={() => setLeftOpen((open) => !open)}>
        {leftOpen ? 'Close left' : 'Open left'}
      </button>
      {leftOpen && <DocumentSlot name="Left" />}
      <DocumentSlot name="Right" />
    </>
  );
}
Jotai33 줄 · 3 개 API
JavaScript / JSX
import { useState } from 'react';
import { atom, createStore, useAtomValue } from 'jotai';

const draftAtom = atom('');
const selectionAtom = atom(0);
function createDocument() {
  return { store: createStore() };
}

function Document({ doc, name }) {
  const text = useAtomValue(draftAtom, { store: doc.store });
  const selection = useAtomValue(selectionAtom, { store: doc.store });
  return (
    <section>
      <label>
        {name}
        <input value={text} onChange={(event) => doc.store.set(draftAtom, event.target.value)} />
      </label>
      <output>{text}</output>
      <button onClick={() => doc.store.set(selectionAtom, (n) => n + 1)}>
        Selection: {selection}
      </button>
    </section>
  );
}

function DocumentSlot({ name }) {
  const [doc] = useState(createDocument);
  return <Document doc={doc} name={name} />;
}

export default function App() {
  const [leftOpen, setLeftOpen] = useState(true);
  return (
    <>
      <button onClick={() => setLeftOpen((open) => !open)}>
        {leftOpen ? 'Close left' : 'Open left'}
      </button>
      {leftOpen && <DocumentSlot name="Left" />}
      <DocumentSlot name="Right" />
    </>
  );
}

restate useRestateScope가 스토어를 만들고 언마운트 뒤에 파괴합니다.

restate27 줄 · 2 개 API
JavaScript / JSX
import { useState } from 'react';
import { restate, useRestateScope } from '@kkwaenggwari/restate';

function Document({ name }) {
  const doc = useRestateScope((scope) => ({
    useDraft: restate('', { scope }),
    useSelection: restate(0, { scope }),
  }));
  const text = doc.useDraft();
  const selection = doc.useSelection();
  return (
    <section>
      <label>
        {name}
        <input value={text} onChange={(event) => doc.useDraft.set(event.target.value)} />
      </label>
      <output>{text}</output>
      <button onClick={() => doc.useSelection.set((n) => n + 1)}>Selection: {selection}</button>
    </section>
  );
}

export default function App() {
  const [leftOpen, setLeftOpen] = useState(true);
  return (
    <>
      <button onClick={() => setLeftOpen((open) => !open)}>
        {leftOpen ? 'Close left' : 'Open left'}
      </button>
      {leftOpen && <Document name="Left" />}
      <Document name="Right" />
    </>
  );
}

Jotai의 Provider나 store, Zustand의 vanilla store로도 상태를 격리할 수 있습니다. 만들고 연결하는 일은 직접 합니다.

숫자를 세는 방법

표시한 코드의 빈 줄을 뺀 줄 수와, 각 상태 라이브러리에서 가져온 이름의 수입니다(React import는 세지 않습니다). 모든 구현은 같은 저장소·로딩·오류 상태와 같은 UI를 렌더합니다. restate 예제는 앱 시작에서 await restateManager.ready를 마쳤다고 가정합니다. 각 프로젝트의 공식 문서로 확인한 출처:

  • Redux Toolkit — Redux Toolkit · Quick Start, React Redux · useSelector, Redux Toolkit · configureStore. 감사 가능한 단일 액션 기록과 큰 미들웨어 생태계가 가장 중요하다면 좋은 선택입니다
  • Zustand — Zustand · Introduction, Zustand · persist. 가장 작은 코어에 미들웨어를 골라 조합하고 싶다면 좋은 선택입니다
  • Jotai — Jotai · atom, Jotai · Storage, Jotai · Provider. 상태가 원래 작은 파생 값들의 그래프라면 좋은 선택입니다

02 — 빠름

바뀐 곳만 렌더하고, 저장은 한 번만.

두 패널 모두 브라우저에서 실제 restate 패키지를 실행합니다. 누르고 입력하면서 숫자를 지켜보세요.

렌더 지도

같은 네 필드를 두 가지 방식으로 읽습니다. 필드 하나를 바꾸고 렌더 횟수를 세어 보세요.

스토어 전체를 읽음
  • 이름
  • 테마
  • 카운트
  • 초안
필드 하나만 선택
  • 이름
  • 테마
  • 카운트
  • 초안

저장 합치기

autoSave는 입력이 멈출 때까지 기다렸다가 한 번 씁니다. 연달아 부른 save()도 대기 중인 한 번의 쓰기를 함께 씁니다.

persistedRestate('', { key: 'note', autoSave: { delayMs: 400 } });

숫자는 이 페이지의 실제 React 렌더와 쓰기 횟수를 세는 저장소 어댑터에서 나옵니다. 다른 라이브러리도 값을 선택할 수 있습니다. restate에서는 선택이 곧 기본 읽기 방식입니다.

03 — 안정

저장소가 실패해도 데이터는 그대로.

상태를 잃는 곳은 저장입니다. 느린 디스크, 막힌 저장소, 닫힌 탭. 아래는 조언이 아니라 restate 1.0이 보장하는 동작입니다.

장애 실험실 열기 안정성 설계 가이드 읽기
  1. 01

    저장은 순서대로

    스토어마다 저장소 작업을 하나씩 차례로 실행합니다. 느린 저장이 끝난 뒤 오래된 값이 새 값을 덮어쓰는 일이 없습니다.

    save() · restore()
  2. 02

    대기 중인 저장은 최신 값으로

    다른 저장이 큐에서 기다리는 동안 save()를 부르면 쓰기를 쌓지 않고 그 저장에 합류합니다.

    save(): Promise<T>
  3. 03

    읽지 못한 기록은 실수로 덮어쓰지 않음

    기록을 불러오지 못했다면 autoSave와 일괄 저장은 그 기록을 건드리지 않고 LOAD_FAILED를 알립니다. 명시적인 save()만 기록을 바꿀 수 있습니다.

    LOAD_FAILED
  4. 04

    언마운트해도 저장은 끝까지

    언마운트 전에 요청한 저장은 완료됩니다. 파괴된 스토어에 늦게 들어온 쓰기는 언마운트 후의 React 상태처럼 무시됩니다.

    useRestateScope
  5. 05

    저장소가 없어도 앱은 시작

    서버 렌더링 중이거나 저장소가 막혀도 스토어는 메모리로 동작하고 저장소를 쓸 수 없다고 알려 줍니다.

    status().available
  6. 06

    오류가 무엇이고 어디인지

    모든 restate 오류에는 타입이 있는 코드와 기록 키가 있어 알맞은 안내를 보여 줄 수 있습니다.

    error.code · error.key

앱과 함께 자라는 코드

선언만 바꾸세요. 컴포넌트는 그대로.

저장·자동 저장·검증은 같은 스토어의 옵션입니다. 오른쪽 컴포넌트는 한 줄도 바뀌지 않습니다.

  1. 메모리

    메모리에 있는 공유 값.

    import { restate } from '@kkwaenggwari/restate';
    
    export const useDraft = restate('');
  2. 저장

    자동으로 복원하고, save()를 부를 때 씁니다.

    import { persistedRestate } from '@kkwaenggwari/restate';
    
    export const useDraft = persistedRestate('', { key: 'draft' });
  3. 자동 저장

    입력이 500ms 멈추면 한 번 씁니다.

    import { persistedRestate } from '@kkwaenggwari/restate';
    
    export const useDraft = persistedRestate('', {
      key: 'draft',
      autoSave: { delayMs: 500 },
    });
  4. 검증

    오래된 기록은 쓰기 전에 마이그레이션하고 검사합니다.

    import { persistedRestate } from '@kkwaenggwari/restate';
    
    export const useDraft = persistedRestate('', {
      key: 'draft',
      autoSave: { delayMs: 500 },
      version: 2,
      migrate: (value) => String(value ?? ''),
      validate: (value) => typeof value === 'string',
    });

컴포넌트 — 모든 단계에서 그대로

function Editor() {
  const text = useDraft();

  return <textarea value={text} onChange={(event) => useDraft.set(event.target.value)} />;
}

React가 도는 곳이면 어디든

모든 React 환경에 패키지 하나.

  • react >=16.8.0

    React

    React 16.8 이상에서 브라우저와 서버 렌더링 모두 동작합니다.

  • react-native-mmkv >=2.0.0

    React Native

    같은 import를 씁니다. react-native-mmkv를 설치하면 MMKV로 저장합니다.

  • SSR

    서버 렌더링

    컴포넌트가 소유한 스토어는 props로 서버 스냅샷을 만들고 하이드레이션 뒤에 불러오기를 시작합니다.

  • index.d.ts

    TypeScript

    타입 선언이 포함되어 있습니다. selector, 상태 필드, 오류 코드까지 타입이 있습니다.

시작하기

첫 공유 상태까지 1분.

설치하고, 선언하고, 쓰세요. 저장은 앱에 필요해질 때 더하면 됩니다.

  1. 01
    설치
    Terminal
    npm install @kkwaenggwari/restate
  2. 02
    스토어 선언
    JavaScript / JSX
    export const useCart = restate({ items: 0, total: 0 });
  3. 03
    어느 컴포넌트에서나 읽기
    JavaScript / JSX
    const items = useCart((cart) => cart.items);

KKWAENGGWARI

프로젝트

문서 검색

모든 가이드의 제목·설명·본문을 검색합니다.