(3) 상태 관리 도구
1) 클라이언트 측: UI 상태 관리
- 상태
- UI 컨트롤 상태: 사용자의 입력, 버튼 활성 여부, 드롭다운 메뉴의 선택 등
- UI 레이아웃 상태: 테마 설정, 사이드바 열림/닫힘, 모달 창의 보임/숨김 등
- 클라이언트 측 캐싱: 사용자 인증 정보, API 호출 결과 등
- 애플리케이션 로직 상태: 애플리케이션 내부에서 발생하는 이벤트나 사용자 작업에 따른 상태 변화 등
- 도구 종류
redux
- 구조: 단일 스토어와 단방향 데이터 흐름
- Actions: 상태 변경을 요청하기 위해 스토어에 전달하는 데이터 패킷. 이벤트 종류(type)와 추가 데이터(payload)로 구성
- Reducers: 액션의 타입에 따라 이전 상태를 기반으로 한 상태 생성하는 순수 함수
- Store: 전역 상태 보관소
- 장점: 전역 상태 관리, 상태 변경 로직 분리, immer로 상태 불변성 유지, 미들웨어 지원
- 단점: 보일러플레이트 코드가 많고 학습의 진입 장벽이 높음
createStore()
: 스토어 생성store.dispatch()
: set statestore.getState()
: get statestore.subscribe()
: 리스너 등록
- 구조: 단일 스토어와 단방향 데이터 흐름
src/
|-- actions/ # 액션 타입, 액션 생성자 함수 정의
| |-- userActions.js
| |-- counterActions.js
|-- reducers/ # 초기 상태, 리듀서 함수 정의
| |-- userReducer.js
| |-- counterReducer.js
| |-- index.js # 도메인별 리듀서 합쳐서 루트 리듀서 생성
|-- store/ # 스토어, 미들웨어
|-- index.js
// actions/counterActions.js
// 액션 타입
const ACTION_TYPE = {
INCREMENT: 'INCREMENT',
DECREMENT: 'DECREMENT',
};
// 액션 생성자 함수
const increment = amount => ({
type: ACTION_TYPE.INCREMENT,
payload: amount,
});
const decrement = amount => ({
type: ACTION_TYPE.DECREMENT,
payload: amount,
});
// reducers/counterReducer.js
// 초기 상태
const initialState = {
count: 0,
};
// 리듀서 함수
function counterReducer(state = initialState, action) {
switch (action.type) {
case ACTION_TYPE.INCREMENT:
return { count: state.count + action.payload };
case ACTION_TYPE.DECREMENT:
return { count: state.count - action.payload };
default:
return state;
}
}
// reducers/index.js
// 루트 리듀서 생성
const rootReducer = combineReducers({
user: userReducer,
counter: counterReducer,
});
// store/index.js
// 스토어 생성
const store = createStore(
rootReducer, // 리듀서 등록
applyMiddleware(thunk) // 미들웨어 등록
);
// component.js
// 상태 변경 리스너 등록
const listener = () => console.log('state changed!')
const unsubscribe = store.subscribe(listener)
);
// 액션 디스패치
store.getState(); // 현재 상태 조회,
/* { user: {}, counter: { count: 0 } } */
store.dispatch(increment(1));
store.getState();
/* { user: {}, counter: { count: 1 } } */
// 작업 후 구독 해제
unsubscribe();
@reduxjs/toolkit
: redux 코드 작성을 간소화configureStore({reducer, middleware, devTools})
: createStore + combineReducer, 미들웨어 설정 등createSlice()
: 액션 + 리듀서 정의- slice: 도메인 객체 조각(상태+메서드)
src/
|-- features/ # 기능별 슬라이스 정의
| |-- userSlice.js
| |-- counterSlice.js
|-- app/
| |-- store.js # 스토어 생성, 미들웨어 등록
|-- views/
|-- HomePage.js
// features/counterSlice.js
interface counterState {
count: number;
}
const initialState = {
count: 0,
};
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state, action: PayloadAction<counterState>) => {
state.count += action.payload.count;
},
decrement: (state, action: PayloadAction<counterState>) => {
state.count -= action.payload.count;
},
},
});
// counterSlice = {actions, reducer}
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
// app/store.js
const store = configureStore({
reducer: {
counter: counterReducer,
user: userReducer,
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(counterSlice.middleware),
devTools: () => {},
});
export default store;
// 이후 store 사용은 redux와 같음
- @reduxjs/toolkit +
react-redux
: react 컴포넌트 내 state 구독으로 리렌더링 유발, 렌더링 최적화useSelector()
: store.getState() 대체useDispatch()
: store.dispatch(action) 대체
// store 설정까지 rtk로 진행
// app/store.js
export default store;
export type RootState = ReturnType<typeof store.getState>;
// App.tsx
const App: React.FC = () => {
return (
<Provider store={store}>
<CounterComponent />
</Provider>
);
};
// CounterComponent.tsx
const CounterComponent: React.FC = () => {
const { count } = useSelector((state: RootState) => state.counter);
const dispatch = useDispatch();
return (
<>
<h1>Count: {count}</h1>
<button onClick={() => dispatch(increment(1))}>+</button>
<button onClick={() => dispatch(decrement(1))}>-</button>
</>
);
};
- MobX: 반응형 프로그래밍 패턴 사용
- Recoil: 원자(Atoms)와 선택자(Selectors) 사용
- Zustand: 가벼운 상태 관리 솔루션
2) 서버 측: 비동기적 상태 데이터 패칭, 캐싱, 동기화
- 상태
- 영구 데이터: 장기 저장 데이터. 데이터베이스, 파일 시스템, 클라우드 스토리지 등에 보관됨. 사용자 정보, 게시글 등
- 세션 데이터: 임시 저장 데이터. 로그인 세션 동안만 메모리에 유지됨. 로그인 여부, 장바구니 내용 등
- 캐싱 데이터: 일시 저장 데이터. 서버의 응답 속도 향상을 위해 자주 사용하는 데이터를 메모리에 보관. 특정 시간 동안만 유효하고 주기적으로 갱신됨
- 도구 종류
@tanstack/react-query
- 기능: 데이터 패칭, 캐싱, 동기화
- 데이터 패칭: HTTP 엔드포인트로 쿼리를 보내 데이터 패칭하고, 패칭 과정의 상태(로딩, 성공, 에러) 처리 간편화
쿼리(query): 데이터 요청, 요청과 조건을 URL이나 HTTP 요청의 본문으로 전송하면 서버는 해당하는 데이터로 응답함. 조건으로 데이터의 종류, 양, 형식 등 설정이 가능
- 캐싱: 패칭된 데이터를 자동으로 캐싱하고 백그라운드 리프레시로 주기적인 동기화. 동일한 요청을 줄여 재요청이 없으므로 앱 사용이 빠르고 효율적임
- 동기화: 스테일 데이터를 주기적으로 업데이트하여 서버와 동기화. 쿼리 키로 데이터를 관리하여 여러 컴포넌트를 효율적으로 관리함
- 데이터 상태: fresh🥩 & stale🥔
- 무한 스크롤, 페이지네이션 같은 고급 기능들도 지원
- 데이터 패칭: HTTP 엔드포인트로 쿼리를 보내 데이터 패칭하고, 패칭 과정의 상태(로딩, 성공, 에러) 처리 간편화
queryClient
: 비동기 상태 스토어useQuery(queryKey, queryFn, staleTime, gcTime, enabled)
: 비동기적인 데이터 페칭(read)isPending
: 초기 데이터 요청이 시작되기 전(쿼리 비활성화), 진행 중(활성화) 데이터 대기 상태isFetching
: 초기, 추가 데이터 대기 상태isLoading
: (isPending && isFetching)초기 데이터 로딩(활성화) 상태, 쿼리 비활성화시엔 로딩 상태가 아님(false). 검색창에 검색시 트리거 되는 등, 특정 작업을 통한 쿼리 활성화시 사용isError
: 데이터 요청 실패
// app.tsx export const queryClient = new QueryClient(); function App() { return ( <QueryClientProvider client={queryClient}> <RouterProvider router={router} /> </QueryClientProvider> ); } // src/components/Component.tsx const { data, error, isPending, isLoading, isError } = useQuery({ queryKey: ['events', { searchTerm }], // 배열키 사용 queryFn: ({ signal, queryKey }) => fetchEvent({ signal, ...queryKey[1] }), // HTTP 요청 함수, signal로 요청 취소 staleTime: 1000, // 새 요청을 전송하는 시기 gcTime: 5 * 60 * 1000, // 데이터를 보관하는 기간 enabled: searchTerm !== undefined, }); // apis.ts interface Event { id: number; name: string; } export const fetchEvent = async ({ signal, searchTerm }): Promise<Event> => { const response = await fetch(`/events/${searchTerm}`, { signal }); // 쿼리 중단시 요청 취소 signal 전달 // ... return response.json(); };
useMutation(mutationFn, onSuccess, onMutate, onError, onSuccess, onSettled)
: 비동기적인 데이터 전송(write)
const { mutate, error, isPending, isError } = useMutation({ mutationFn: createNewEvent, onSuccess: () => { // 뮤테이션 성공시 실행 queryClient.invalidateQueries({ // 쿼리 무효화(=> 리패치) queryKey: ['events'], // 해당 쿼리를 포함한 쿼리를 대상 // exact: false, // 정확히 일치하는 쿼리로 제한하는 옵션 refetchType: 'none', // 리패치 비활성화 }); navigate('/events'); }, // 낙관적 업데이트: 데이터를 서버에 업데이트하기 전에 클라이언트 상태를 미리 업데이트함. 업데이트 실패시 클라이언트는 이전 상태로 롤백. 서버 데이터 업데이트 전에 사용자가 기다림 없이 즉각적인 피드백을 받을 수 있다는 장점 onMutate: async ({ id, event }) => { // mutate 호출 후 서버 응답 필요 없이 즉시 실행, 낙관적 업데이트시 비동기적으로 작동해야 함 const queryKey = ['events', { id }]; await queryClient.cancelQueries({ queryKey }); const previousEvent = queryClient.getQueryData(queryKey); queryClient.setQueryData(queryKey, event); return { previousEvent }; // 롤백을 위한 이전 데이터(context) }, onError: (error, data, context) => { // 뮤테이션 실패시 롤백 queryClient.setQueryData(['events', { id }], context.previousEvent); }, onSuccess: () => {}, // 뮤테이션 성공시 작업 onSettled: () => { // 뮤테이션 완료시(성패 무관 결과 확정시) queryClient.invalidateQueries(['events', { id }]); // 쿼리 무효화-데이터 리패칭을 통해 서버 데이터와 동기화 }, }); function handleSubmit(formData) { mutate({ event: formData }); // 함수를 실행해 요청 보냄, 이 인자는 mutationFn: createNewEvent로 전달됨 }
- react-router-dom + tanstack-query: 라우트시 초기 데이터(loader) + 데이터 업데이트(caching)
- useQuery ➜ loader + useQuery
- useMutation ➜ action
- 중복 HTTP 요청 방지:
staleTime: 10000
으로 loader에서 가져온 데이터 감지하도록 함
- 기능: 데이터 패칭, 캐싱, 동기화
SWR
: 캐시된 데이터를 먼저 반환하고 데이터를 패칭하여 새로고침하는 방법 사용Apollo Client
: GraphQL 사용 애플리케이션 맞춤 도구Relay
: React와 GraphQL 사용 애플리케이션을 위한 프레임워크
'STORAGE > React' 카테고리의 다른 글
3. 내장 함수를 통한 성능 최적화 (0) | 2024.04.13 |
---|---|
2-1. 상태 관리 개괄 (0) | 2024.04.11 |
1. React와 컴포넌트 (0) | 2024.04.03 |
댓글