HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
🌟
Programmers 최종 플젝 6조
/
[프론트] TWL
[프론트] TWL
/
useReducer로 context 관리하기
useReducer로 context 관리하기
useReducer로 context 관리하기

useReducer로 context 관리하기

생성일
Dec 10, 2021 03:23 PM
태그
React
작성자
해결 완료
해결 완료

🔥 문제

이번에 팀원들과 전역 콘텍스트 관리에 있어 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에서 타입에 대한 순환참조가 발생했다는 오류가 떴다.
notion image
 
그렇다면 우리는, 어떻게 관리할 것인지가 이제 중요해졌다.
고민 끝에, 나는 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에서 일부만 바꾸었으며, 이는 깃헙의 실제 결과물을 참고하시길 바랍니다!
 

👏🏻 참고자료

https://ko.reactjs.org/docs/hooks-reference.html#usereducer