Skip to main content

나중에정리해라

Zustand store 설계

배열에서 주의할 점은 이겁니다.

const completedTodos = useTodoStore((state) => state.todos.filter((todo) => todo.done), );

filter는 매번 새 배열을 만듭니다.

[] === [] // false

그래서 unrelated state 변경에도 selector가 다시 실행되면서 새 배열을 반환하면 리렌더링될 수 있습니다.

이럴 때는 방법이 몇 가지 있습니다.

첫째, shallow 비교를 씁니다.

import { useShallow } from 'zustand/react/shallow';

const completedTodos = useTodoStore( useShallow((state) => state.todos.filter((todo) => todo.done)), );

둘째, 더 작은 primitive를 구독합니다.

const completedCount = useTodoStore( (state) => state.todos.filter((todo) => todo.done).length, );

셋째, 리스트 렌더링에서는 id 목록과 item 구독을 나눕니다.

const todoIds = useTodoStore( useShallow((state) => state.todos.map((todo) => todo.id)), );

그리고 각 row에서 자기 todo만 구독합니다.

function TodoRow({ id }: { id: string }) { const todo = useTodoStore((state) => state.todos.find((todo) => todo.id === id), );

return <div>{todo?.title}</div>;

}

이렇게 하면 리스트 전체가 아니라 개별 row 단위로 리렌더링 범위를 줄일 수 있습니다.

structural sharing, 구조적 공유

Immer는 내부에서 draft를 Proxy로 감싸고, 어떤 부분이 실제로 변경됐는지 추적합니다

예를 들어:

  const nextState = produce(prevState, (draft) => {
draft.user.name = 'Kim';
});

prevState !== nextState
prevState.user !== nextState.user
prevState.settings === nextState.settings

Zustand을 꼭 써야할 때 ?

Zustand는 내부적으로 external syncstore 개념을 사용한다.

  • react context는 리액트 외부에서 상태값 변경이 불가능하다. 하지만 Zustand는 애초에 외부 스토어이므로 상태변경이 가능하다.
  • render function + non render function간의 상태 동기화가 필요할 때 zustand를 사용한다.
  • 대표적으로 언어 설정, market 설정 등 유즈케이스에서 리액트의 렌더러와 함수 포멧터(react 밖)의 상태 동기가 필요 할 때

use context selector 라이브러리 사용법 최소 정리

React 기본 useContext는 Provider의 value 참조가 바뀌면 해당 Context를 읽는 컴포넌트들이 넓게 리렌더링될 수 있다.
use-context-selector는 Context에서 필요한 조각만 selector로 구독하게 해준다.

import {
createContext,
useContextSelector,
} from 'use-context-selector';

type Store = {
count: number;
text: string;
increase: () => void;
setText: (text: string) => void;
};

const StoreContext = createContext<Store | null>(null);

Provider에서는 일반 Context처럼 값을 내려준다.

function StoreProvider({ children }: { children: React.ReactNode }) {
const [count, setCount] = useState(0);
const [text, setText] = useState('');

const value = useMemo(
() => ({
count,
text,
increase: () => setCount((prev) => prev + 1),
setText,
}),
[count, text],
);

return (
<StoreContext.Provider value={value}>
{children}
</StoreContext.Provider>
);
}

Consumer에서는 전체 Context를 받지 않고 필요한 값만 고른다.

function Counter() {
const count = useContextSelector(StoreContext, (state) => state?.count);
const increase = useContextSelector(StoreContext, (state) => state?.increase);

return <button onClick={increase}>{count}</button>;
}

function TextInput() {
const text = useContextSelector(StoreContext, (state) => state?.text);
const setText = useContextSelector(StoreContext, (state) => state?.setText);

return (
<input
value={text ?? ''}
onChange={(event) => setText?.(event.target.value)}
/>
);
}

핵심은 selector 결과가 바뀐 컴포넌트만 리렌더링 대상이 된다는 점이다.

useContext(Context)
=> Provider value가 바뀌면 넓게 리렌더링될 수 있음

useContextSelector(Context, selector)
=> selector가 반환한 값이 바뀔 때만 리렌더링

주의할 점:

const state = useContextSelector(Context, (state) => ({
count: state.count,
text: state.text,
}));

selector에서 새 객체를 만들면 매번 새 참조가 될 수 있다.
가능하면 primitive나 기존 참조를 그대로 반환하는 selector를 사용한다.

const count = useContextSelector(Context, (state) => state.count);
const text = useContextSelector(Context, (state) => state.text);