useReducer
useReducer 기본 문법
- 
컴포넌트에
reducer를 추가하는 문법const [state, dispatch] = useReducer(reducer, initialArg, init?) - 
useReducer를 컴포넌트의 최상위에 호출하고, reducer를 이용해 state를 관리
import { useReducer } from 'react'; function reducer(state, action) { // ... } function MyComponent() { const [state, dispatch] = useReducer(reducer, { age: 42 }); // ... - 
매개변수
- reducer
- state가 어떻게 업데이트 되는지 지정하는 리듀서 함수
 - 반드시 순수함수여야 하며, state와 action을 인수로 받아야 하고, 다음 state를 반환해야 함
 
 - initialArg
- 초기 state가 계산되는 값
 
 - (optional) init
- 초기 state를 반환하는 초기화 함수
 
 
 - reducer
 - 
반환 값 : [state, dispatch]
- state
- 첫번째 렌더링에서의 state는 init(initialArg) 또는 initialArg로 설정
 
 - dispatch
- state를 새로운 값으로 업데이트하고 리렌더링을 일으킴
 
 
 - state
 - 
주의사항
- 컴포넌트의 최상위 또는 커스텀 훅에서만 호출 가능 (반복문, 조건문 X)
 - 우연한 비순수성을 찾아내기 위해 Strict Mode에서 두 번 호출됨
 
 
dispatch
- useReducer에 의해 반환되는 함수
 - state를 새로운 값으로 업데이트하고 리렌더링을 일으킴
 - 매개변수 : action
{type, payload}- type 프로퍼티 : action을 정의
 - payload 프로퍼티 : 추가적인 정보를 표현
 
 - 반환값
- 어떤 값도 반환하지 않음
 
 - 주의사항
- 오직 다음 렌더링에 사용할 state 변수만 업데이트함
- dispatch 함수를 호출한 직후 state 변수를 읽는다면 호출 이전의 최신화되지 않은 값을 참조
 
 Object.is비교를 통해 새롭게 제공된 값과 현재state를 비교한 값이 같을 경우, React는 컴포넌트와 해당 컴포넌트의 자식 요소들의 리렌더링을 건너뜀- React는 state의 업데이트를 batch
- 
배치 업데이트(Batch Update) : 컴포넌트가 여러 번 업데이트 되더라도, 실제 DOM 요소에 변경사항을 적용하는 것을 최소화하여 애플리케이션의 성능을 최적화하는 기능
 - 
이벤트 핸들러의 모든 코드가 수행되고
set함수가 모두 호출된 후에 화면을 업데이트⇒ 하나의 이벤트에 리렌더링이 여러번 일어나는 것을 방지
 - 
DOM 접근 등 이른 화면 업데이트를 강제해야할 특수한 상황이 있을 경우
flushSync(opens in a new tab)를 사용 
 - 
 
 - 오직 다음 렌더링에 사용할 state 변수만 업데이트함
 
useReducer는 언제 사용하는 게 좋을까?
- 
state 형태가 복잡할 때
- 로직이 파편화되면, 코드 가독성이 떨어짐
 - 복잡한 형태의 state를 사전에 정의된 dispatch로만 수정할 수 있게 만들어 줌
- state에 대한 접근은 컴포넌트에서만 가능
 - state의 업데이트 하는 방법에 대한 dispatch는 컴포넌트 밖에서 제한
 
 
⇒ state를 사용하는 로직과 이를 관리하는 비즈니스 로직을 분리할 수 있어 state를 관리하기가 한결 쉬워짐
 - 
분산된 setState 로직을 한 곳에 모으고 싶을 때
- 성격이 비슷한 여러 개의 state를 묶어 useReducer로
 
 - 
다른 state 기반으로 state를 업데이트할 때
 
useReducer의 동작
1. 위험한 상태 변경(setter)는 이제 그만!
- reduce : 기존(현재) 값을 바탕으로 추가 액션
- ex) [1,2,3].reduce((curSum, a) ⇒ curSum + a, 0)
 
 - reducer : 기존 값에 무엇인 액션을 해주는 함수
 - 가장 큰 장점은, setter를 직접적으로 노출하지 않으면서 함수를 컴포넌트 외부에 둘 수 있음
 
const totalPrice = 
	session.cart.map(item => item.price)
		.reduce((tot, price) => tot + price, 0);
     
// ⇒ ⇒ 이것을 활용해서 reducer를 만들면 기존 totalPrice값에 신규 item.price를 더하는 것!!
const addPrice = useReducer( (prePrice, newPrice) => prePrice + newPrice, 0);
const [totalPrice, addPrice]  ⇒ addPrice(100);2. 상태 관련 함수를 한 곳에
- reducer : 같은 상태를 변경하는 함수들을 한 곳에 모아 놓은 것
 - dispatch(action) : action을 전달하는 함수
 - 가장 큰 장점은, 컴포넌트 외부에 상태 변경하는 함수(reducer)를 둘 수 있음!!
 
const **reducer** = (state, action) => {...}
	// action: {type, payload} // payload는 신규 데이터
	
const [state, dispatch] = useReducer(**reducer**, 초깃값)
//or
const [state, dispatch] = useReducer(**reducer**, initArg, initFn);예시) plusCount, minusCount 함수를 useReducer로!
기존 코드
src/hooks/counter-context.tsx
import { createContext, PropsWithChildren, useContext, useState } from 'react';
 
const defCtx = { count: 0, plusCount: () => {}, minusCount: () => {} };
 
type CounterContextProps = typeof defCtx;
 
const CounterContext = createContext<CounterContextProps>(defCtx);
 
export const CounterProvider = ({ children }: PropsWithChildren) => {
  const [count, setCount] = useState(0);
  const plusCount = () => setCount((preCount) => preCount + 1);
  const minusCount = () => setCount((preCount) => preCount - 1);
  return (
    <CounterContext.Provider value={{ count, plusCount, minusCount }}>
      {children}
    </CounterContext.Provider>
  );
};
 
export const useCounter = () => useContext(CounterContext);변경 후
src/hooks/counter-context.tsx
import { createContext, PropsWithChildren, useContext, useReducer } from 'react';
 
const defCtx = {
  count: 0,
  plusCount: (_payload?: number) => {},
  minusCount: (_payload?: number) => {},
};
 
type CounterContextProps = typeof defCtx;
 
const CounterContext = createContext<CounterContextProps>(defCtx);
 
type Reducer = { type: string; payload: number };
 
const reducer = (count: number, { type, payload }: Reducer) => {
  if (type === 'plus') return count + payload;
  if (type === 'minus') return count - payload;
  return count;
};
 
export const CounterProvider = ({ children }: PropsWithChildren) => {
  const [count, dispatch] = useReducer(reducer, 0);
  const plusCount = (payload: number = 1) =>
    dispatch({ type: 'plus', payload });
  const minusCount = (payload: number = 1) =>
    dispatch({ type: 'minus', payload });
  return (
    <CounterContext.Provider value={{ count, plusCount, minusCount }}>
      {children}
    </CounterContext.Provider>
  );
};
 
export const useCounter = () => useContext(CounterContext);예시) login, logout, saveCartItem, removeCartItem 등 모든 함수를 useReducer를 이용하여 통합(reduce)하기
기존 코드
src/hooks/session-context.tsx
import { createContext, PropsWithChildren, useContext, useState } from 'react';
 
const SampleSession = {
  loginUser: null,
  cart: [
    { id: 100, name: '라면', price: 3000 },
    { id: 101, name: '컵라면', price: 2000 },
    { id: 200, name: '파', price: 5000 },
  ],
};
 
type Session = {
  loginUser: { id: number; name: string } | null;
  cart: typeof SampleSession.cart;
};
 
type SessionContextProps = {
  session: Session;
  login: (id: number, name: string) => void;
  logout: () => void;
  saveCartItem: (id: number, name: string, price: number) => void;
  removeCartItem: (itemId: number) => void;
};
 
const defaultSession: SessionContextProps = {
  session: SampleSession,
  login: () => {},
  logout: () => {},
  saveCartItem: () => {},
  removeCartItem: () => {},
};
 
const SessionContext = createContext<SessionContextProps>(defaultSession);
 
export const SessionProvider = ({ children }: PropsWithChildren) => {
  const [session, setSession] = useState<Session>(SampleSession);
 
  const logout = () => {
    setSession({ ...session, loginUser: null });
  };
  const login = (id: number, name: string) => {
    setSession({ ...session, loginUser: { id: id, name: name } });
  };
 
  const removeCartItem = (itemId: number) => {
    setSession({
      ...session,
      cart: session.cart.filter(({ id }) => id !== itemId),
    });
  };
 
  const saveCartItem = (id: number, name: string, price: number) => {
    if (id !== 0) {
      setSession({
        ...session,
        cart: [
          ...session.cart.map((item) =>
            item.id === id ? { id, name, price } : item
          ),
        ],
      });
    } else {
      id = Math.max(...session.cart.map((item) => item.id), 0) + 1;
      setSession({
        ...session,
        cart: [...session.cart, { id, name, price }],
      });
    }
  };
 
  return (
    <SessionContext.Provider
      value={{ session, login, logout, removeCartItem, saveCartItem }}
    >
      {children}
    </SessionContext.Provider>
  );
};
 
export const useSession = () => useContext(SessionContext);
 변경 후
src/hooks/session-context.tsx
import {
  createContext,
  PropsWithChildren,
  useCallback,
  useContext,
  useLayoutEffect,
  useReducer,
} from 'react';
import useFetch from './fetch-hook';
 
type LoginUser = { id: number; name: string } | null;
type Cart = { id: number; name: string; price: number }[];
type Session = {
  loginUser: LoginUser;
  cart: Cart;
};
 
const SKEY = 'MY-SESSION1';
 
const getStorage = () => {
  const loginUser = JSON.parse(sessionStorage.getItem(SKEY) ?? 'null');
  const cart = JSON.parse(localStorage.getItem(SKEY) ?? '[]');
 
  return { loginUser, cart };
};
 
const setStorage = (session: Session) => {
  // if (!session )
  const { loginUser, cart } = session;
  sessionStorage.setItem(SKEY, JSON.stringify(loginUser));
  localStorage.setItem(SKEY, JSON.stringify(cart));
};
 
type SessionContextProps = {
  session: Session;
  login: (id: number, name: string) => void;
  logout: () => void;
  saveCartItem: (id: number, name: string, price: number) => void;
  removeCartItem: (itemId: number) => void;
};
 
const defaultSession: SessionContextProps = {
  session: { loginUser: null, cart: [] },
  login: () => {},
  logout: () => {},
  saveCartItem: () => {},
  removeCartItem: () => {},
};
 
type Reducer =
  | { type: 'logout'; payload: null }
  | { type: 'login'; payload: LoginUser }
  | { type: 'removeCartItem'; payload: number }
  | { type: 'saveCartItem'; payload: Cart }
  | { type: 'Initialize'; payload: Session };
 
const reducer = (session: Session, { type, payload }: Reducer) => {
  let _session;
  switch (type) {
    case 'logout':
      _session = { ...session, loginUser: null };
      break;
    case 'login':
      _session = { ...session, loginUser: payload };
      break;
    case 'removeCartItem':
      _session = {
        ...session,
        cart: session.cart.filter((item) => item.id !== payload),
      };
      break;
    case 'saveCartItem':
      console.log('🚀  payload:', payload);
 
      _session = {
        ...session,
        cart: payload,
      };
      break;
    case 'Initialize':
      _session = payload;
      break;
    default:
      return session; // bailout(skip)
  }
  setStorage(_session);
  return _session;
};
 
const SessionContext = createContext<SessionContextProps>(defaultSession);
 
export const SessionProvider = ({ children }: PropsWithChildren) => {
  const storage = getStorage();
  const [session, dispatch] = useReducer(reducer, storage);
 
  const data = useFetch<Session>('/data/sample.json');
  useLayoutEffect(() => {
    if (!data) return;
    dispatch({ type: 'Initialize', payload: data });
  }, [data]);
 
  const logout = useCallback(() => {
    dispatch({ type: 'logout', payload: null });
  }, []);
 
  const login = useCallback((id: number, name: string) => {
    dispatch({ type: 'login', payload: { id, name } });
  }, []);
 
  const removeCartItem = useCallback((itemId: number) => {
    dispatch({ type: 'removeCartItem', payload: itemId });
  }, []);
 
  const saveCartItem = useCallback(
    (id: number, name: string, price: number) => {
      let payload;
      if (id !== 0) {
        payload = [
          ...session.cart.map((item) =>
            item.id === id ? { id, name, price } : item
          ),
        ];
      } else {
        id = Math.max(...session.cart.map((item) => item.id), 0) + 1;
        payload = [...session.cart, { id, name, price }];
      }
      dispatch({ type: 'saveCartItem', payload });
    },
    [session]
  );
 
  return (
    <SessionContext.Provider
      value={{
        session,
        login,
        logout,
        saveCartItem,
        removeCartItem,
      }}
    >
      {children}
    </SessionContext.Provider>
  );
};
 
export const useSession = () => useContext(SessionContext);