5장 리액트와 상태 관리 라이브러리

이번 장에서는 리액트 애플리케이션을 개발할 때 빠지지 않고 언급되는 상태 관리 라이브러리에 대해 알아본다. 많은 개발자들이 리액트 애플리케이션에 자신이 익숙한 상태 관리 라이브러리를 설치하는 것을 익숙해하지만 정작 왜 상태 관리가 필요한지, 또 이 상태 관리가 어떻게 리액트와 함께 작동하는지는 간과하는 경우가 많다. 이번 장에서는 상태 관리 라이브러리의 필요성부터 최근 많이 주목받고 있는 상태 관리 라이브러리가 어떻게 작동하는지 살펴본다.

5.1 상태 관리는 왜 필요 한가?

5.1.1 리액트 상태관리의역사

Flux 패턴의 등장
type storestate = {
count: number
}

type Action = { type: 'add'; payload: number }

function reducer(prevState: StoreState, action; Action) {
    const { type: ActionType } = action
    if (ActionType === 'add') {
        return {
            count: prevState.count + action.payload,
        }
    }
    throw new Error('Unexpected Action [${ActionType}]')
}

export default function App() {
    const [state, dispatcher] = useReducer(rediicer, { count 0 })
    function handleClick() {
        dispatcher({ type: 'add', payload 1 })
    }
    return (
        <div>
            <hl>{state.count}</hl>
            <button onClick={handleClick}>+</button>
        </div>
    )
}
Context API와 useContext

5.2 리액트 훅으로 시작하는 상태 관리

5.2.1 가장 기본적인 방법: useState와 useReducer

function useCounter(initCount: number = 0) {
    const [counter, setCounter] = useState(initCount)
    function inc() {
        setCounter((prev) => prev + 1)
    }

    return { counter, inc }
}
function useCounter(initCount: number = 0) {
    const [counter, setCounter] = useState(initCount)
    function inc() {
        setCounter((prev) => prev + 1)
    }
    return { counter, inc }
}
function Counter1() {
    const { counter, inc } = useCounter()
    return (
        <>
            <h3>Counter1: {counter}</h3>
            <button onClick={inc}>+</button>
        </>
    )
}
function Counter2() {
    const { counter, inc } = useCounter()
    return (
        <>
            <h3>Counter2: {counter}</h3>
            <button onClick={inc}>+</button>
        </>
    )
}
function Counter1({ counter, inc }: { counter: number; inc: () => void }) {
    return (
        <>
            <h3>Counterl: {counter}</h3>
            <button onClick={inc}>+</button>
        </>
    )
}
function Counter2({ counter, inc }: { counter: number; inc: () => void }) {
    return (
        <>
            <h3>Counter2: {counter}</h3>
            <button onClick={inc}>+</button>
        </>
    )
}

function Parent() {
    const { counter, inc } = useCounter()
    return (
        <>
            <Counter1 counter={counter} inc={inc} />
            <Counter2 counter={counter} inc={inc} />
        </>
    )
}

5.2.2 지역 상태의 한계를 벗어나보자: useState의 상태를 바깥으로 분리하기

// counter.ts
export type State = { counter: number }
// 상태를 아예 컴포넌트 밖에 선언했다. 각 컴포넌트가 이 상태를 바라보게 할 것이다.
let state: State = {
    counter: 0,
}
// getter
export function get(): State {
    return state
}
// usestate와 동일하게 구현하기 위해 게으른 초기화 함수나 값을 받을 수 있게 했다.
type Initializer<T> = T extends any ? T | ((prev: T) => T) : never

// setter
export function set<T>(nextState: Initializer<T>) {
    state = typeof nextState ==='function' ? nextState(state) : nextState
}

// Counter
function Counter() {
    const state = get()

    function handleClick() {
        set((prev: State) => ({ counter: prev.counter + 1 }))
    }
    return (
        <>
            <h3>{state.counter}</h3>
            <button onClick={handleClick}>+</button>
        </>
    )
}
function Counter1() {
    const [count, setCount] = useState(state)
    function handleClick() {
        // 외부에서 선언한 set 함수 내부에서 다음 상태값을 연산한 다음,
        // 그 값을 로컬 상태값에도 넣었다.
        set((prev State) => {
            const newState = { counter: prev.counter + 1 }
            // setcount가 호출되면서 컴포넌트 리렌더링을 야기한다.
            setCount(newState)
            return newState
        })
    }
    return (
        <>
            <h3>{count.counter}</h3>
            <button onClick={handleClick}>+</button>
        </>
    )
}

function Counter2() {
    const [count, setCount] = useState(state)
    // 위 컴포넌트와 동일한 작동올 추가했다.
    function handleClick() {
        set((prev: State) => {
            const newState = { counter: prev.counter + 1 }
            setcount(newState)
            return newState
        })
    }
    return (
        <>
            <h3>{count. coiinter}</h3>
            <button onClick={handleClick}>+</button>
        </>
    )
}
type Initializer<T> = T extends any ? T | ((prev: T) => T) : never
type Store<State> = {
    get: 0 => State
    set (action: Initializer<State>) => State
    subscribe: (callback () => void) => () => void
}
export const createStore = <State extends unknown>(
initialstate: Initializer<State>,
) Store<State> => {
    // usestate와 마찬가지로 초깃값을 게으른 초기화를 위한 함수 또한
    // 그냥 값올 받을 수 았도록 한다.
    // state의 값은 스토어 내부에서 보관해야 하므로 변수로 선언한다.
    let state = typeof initialstate !== 'function' ? initialstate : initialState()
    // callbacks는 자료형에 관계없이 유일한 값을 저장할 수 있는 Set을 사용한다.
    const callbacks = new Set<() => void>()
    // 언제든 get이 호출되면 최신값을 가져올 수 있도록 함수로 만든다.
    const get = () => state
    const set = (nextState: State | ((prev: State) => State)) => {
        // 인수가 함수라면 함수를 실행해 새로운 값을 받고,
        // 아니라면 새로운 값을 그대로 사용한다.
        state =
            typeof nextstate === 'function'
            ? (nextstate as (prev: State) => State)(state)
            : nextstate
        // 값의 설정이 발생하면 콜백 목록올 순회하면서 모든 콜백을 실행한다.
        callbacks.forEach((callback) => callback())

        return state
    }
    // subscribe는 콜백을 인수로 받는다.
    const subscribe = (callback: () => void) => {
        // 받은 함수를 콜백 목록에 추가한다.
        callbacks.add(callback)
        // 클린업 실행 시 이를 삭제해서 반복적으로 추가되는 것을 막는다.
        return () => {
            callbacks.delete(callback)
        }
    }
    return { get, set, subscribe }
}
export const useStore = <State extends unknown>(store Store<State>) => {
    const [state, setState] = useState<State>(() => store.get())
    useEffect(() => {
        const unsubscribe = store.subscribe(() => {
            setState(store.get())
        })
        return unsubscribe
    }, [store])
    return [state, store.set] as const
}
const store = createStore({ count: 0 })
function Counter1() {
    const [state, setState] = useStore(store)
    function handleClick() {
        setState((prev) => ({ count: prev.count + 1 }))
    }
    return (
        <>
            <h3>Counterl: {state.count}</h3>
            <button onClick={handleClick}>+</button>
        </>
    )
}

function Counter2() {
    const [state, setState] = useStore(store)
    function handleClick() {
        setState((prev) => ({ count: prev.count + 1 }))
    }
    return (
        <>
            <h3>Counter2: {state.count}</h3>
            <button onClick={handleClick}>+</button>
        </>
    )
}

export default function App() {
    return (
        <div className="App">
            <Counter1 />
            <Counter2 />
        </div>
    )
}
export const useStoreSelector = <State extends unknown, Value extends unknown>(
store Store<State>,
selector (state State) => Value,
) {
    const [state, setState] = useState(() => selector(store.get()))

    useEffect(() => {
        const unsubscribe = store.subscribe(() => {
            const value = selector(store.get())
            setState(value)
        })

        return unsubscribe
    }, [store, selector])

    return state
}
const store = createStore({ count: 0, text: 'hi' })

function Counter() {
    const counter = useStoreSelector(
    store,
    useCallback((state) => state.count, []),
    )

    function handleClick() {
        store.set((prev) => ({ ...prev, count; prev.count + 1 }))
    }

    useEffect(() => {
        console.log('Counter Rendered')
    })
    return (
        <>
            <h3>{counter}</h3>
            <button onClick={handleClick}>+</button>
        </>
    )
}

const textselector = (state: ReturnType<typeof store.get>) => state.text

function TextEditor() {
    const text = useStoreSelector(store, textselector)
    useEffect(() => {
        console.log('Counter Rendered')
    })

    function handleChange(e: ChangeEvent<HTMLInputElement>) {
        store.set((prev) => ({ ...prev, text e.target.value }))
    }

    return (
        <>
            <h3>{text}</h3>
            <input value={text} onChange={handleChange} />
        </>
    )
}
function NewCounter() {
    const subscription = useMemo(
    () => ({
        // 스토어의 모든 값으로 설정해 뒀지만 selector 예제와 마찬가지로
        // 특정한 값에서만 가져오는 것도 가능하다.
        getCurrentValue: () => store.get(),
        subscribe: (callback: ()) => void) => {
        const unsubscribe = store.subscribe(callback)
        return () => unsubscribe()
        },
    }),
    [],
    )

    const value = useSubscription(subscription)
    return <>{JSON.stringify(value)}</>
}

5.2.3 useState와 Context# 동시에 사용해 보기

const store1 = createStore({ count: 0 })
const store2 = createStore({ count: 0 })
const Store3 = createStore({ count: 0 })

끝!