🔥 문제
이번에 팀원들과 전역 콘텍스트 관리에 있어
useReducer
로 관리하기로 했다.이유는, 리덕스든, 리코일이든 결국 '불변성'을 어떻게 관리할 것이냐의 문제로 접근해야 하는데,
복잡한 로직으로 구현될 수록, 최적화가 되며 불변성을 체계적으로 관리가 가능한
useReducer
가 useState
보다는 확장성이 좋기 때문이다.따라서 이번에 제대로 배워보고자 한다.
⭐ 해결 방법
1. reducer
문법을 익혀보자.
먼저 빠르게 기존에 작성했던 코드를 통해 훑어보자.
리듀서는 처음엔 클로저로 인해 어려워보여도, 굉장히 깔끔하다.
먼저
Reducer
가 필요한데, 이 친구는 함수를 통해 불변성을 유지한 채로 state
를 관리한다.여기서 필요한
params
로 state
와 action
을 받는다.또한, 타입을 통해
dispatch
하는 식으로 우리는 action
을 불러올 수 있다.import useEventProvider from '@hooks/useEventProvider'; import React, { createContext, ReactNode, useMemo, useReducer } from 'react'; export interface Event { name: string; expiredAt: string | Date; marketName: string; marketDescription: string; eventDescription: string; isLike: boolean | null; isFavorite: boolean | null; pictures: []; isParticipated: boolean | null; } export type eventListType = Event[] | []; export interface InitialStateType { eventList: eventListType; event: Event; } const initialState: InitialStateType = { eventList: [], event: { name: '', expiredAt: '', marketName: '', marketDescription: '', eventDescription: '', isLike: null, isFavorite: null, pictures: [], isParticipated: null, }, }; export const GET_EVENTLIST = 'EVENT/GET_EVENTLIST'; const eventReducer = (state: InitialStateType, action: any) => { switch (action.type) { case GET_EVENTLIST: { const { eventList } = action; return { ...state, eventList, }; } default: return state; } };
2. 이후 콘텍스트와 Provider
을 구현하자.
useReducer
은 어려워보일 수 있지만, useState
와 거의 동일한 문법 구조를 갖고 있다.useState
에서도 initialState
를 받지 않는가? 단지 첫번째 인자로 "그래서 어떤 리듀서에서 적용할 건데"를 명시해줄 뿐이다.
따라서 우리는
Reducer
라는 특정 행동이 일어나는 방의 문을 찾았다. 여기에서 찾을 수 있는 상태는 왼쪽의
state(eventList, event)
이다.
그리고 이를 딸 열쇠는 오른쪽의 dispatch
이다.const EventContext = createContext<InitialStateType>(initialState); export const useEvent = () => useContext(EventContext); const EventListProvider: React.FC<ReactNode> = ({ children }) => { const [{ eventList, event }, dispatchEvent] = useReducer( eventReducer, // 어떤 리듀서를 쓸 건지를 말해주고, initialState // 여기서 초기화 상태를 규정해준다. ); const { dispatchEventList } = useEventProvider(dispatchEvent); const contextValue = useMemo( () => ({ eventList, event, dispatchEventList }), [event, eventList, dispatchEventList] ); return ( <EventContext.Provider value={contextValue}> {children} </EventContext.Provider> ); }; export default EventListProvider;
2. 구조를 체계화시키자.
지금까지는 나름 당황스러웠지만, 충분히 이해할 수 있을만한 로직이라 생각한다.
그렇다면 다음부터가 문제인데, 나는
hook
을 통해 깔끔하게 로직을 관리하려 했으나, 안타깝게도 eslint
에서 타입에 대한 순환참조가 발생했다는 오류가 떴다.
그렇다면 우리는, 어떻게 관리할 것인지가 이제 중요해졌다.
고민 끝에, 나는
Context
를 모듈로 관리하기로 결심했다. 그리고, 세부적으로 하위 모듈로 type
을 구성할 것이다.@contexts/Event/types.ts
export interface Event { name: string; expiredAt: string | Date; marketName: string; marketDescription: string; eventDescription: string; isLike: boolean | null; isFavorite: boolean | null; pictures: []; isParticipated: boolean | null; } export type eventListType = Event[] | []; export interface InitialStateType { eventList: eventListType; event: Event; } export const GET_EVENTLIST = 'EVENT/GET_EVENTLIST' as const;
@contexts/Event/actions.tsx
import getEventList from '@axios/event/getEventList'; import { GET_EVENTLIST } from '@contexts/Event/types'; import { Dispatch, useCallback } from 'react'; const useEventProvider = (dispatchEvent: Dispatch<any>) => { const dispatchEventList = useCallback(async () => { const eventListData = await getEventList(); dispatchEvent({ type: GET_EVENTLIST, payload: eventListData }); }, [dispatchEvent]); return { dispatchEventList, }; }; export default useEventProvider;
@contexts/Event/index.tsx
import useEventProvider from '@contexts/eventList/actions'; import React, { createContext, ReactNode, useContext, useMemo, useReducer, } from 'react'; import { Action, ContextType, GET_EVENTLIST, INITIALIZE_EVENTLIST, InitialStateType, } from '@contexts/eventList/types'; const initialState: InitialStateType = { eventList: [], event: { eventId: '', name: '', expiredAt: '', marketName: '', isLike: null, likeCount: null, reviewCount: null, maxParticipants: null, }, }; const eventReducer = (state: InitialStateType, action: Action) => { switch (action.type) { case GET_EVENTLIST: { const { payload: eventList } = action; return { ...state, eventList, }; } case INITIALIZE_EVENTLIST: { return { ...state, initialState, }; } default: return state; } }; const EventContext = createContext<ContextType>(initialState); export const useEvent = () => useContext(EventContext); const EventListProvider: React.FC<ReactNode> = ({ children }) => { const [{ eventList, event }, dispatchEvent] = useReducer( eventReducer, initialState ); const { dispatchEventList, initailizeEventList } = useEventProvider(dispatchEvent); const contextValue = useMemo( () => ({ eventList, event, dispatchEventList, initailizeEventList }), [event, eventList, dispatchEventList, initailizeEventList] ); return ( <EventContext.Provider value={contextValue}> {children} </EventContext.Provider> ); }; export default EventListProvider;
리듀서 분리
티모의 요청에 따라 리듀서를 따로 분리하였습니다.
index에서 일부만 바꾸었으며, 이는 깃헙의 실제 결과물을 참고하시길 바랍니다!