요근래 몇 없는 면접이지만, 두 라이브러리를 써왔어서 그런지 늘 받던 질문입니다. 정확히는 Redux를 써보다가 React Query를 쓰게된 이유가 무엇인가요? 였는데, 다시한번 두 라이브러리의 차이점을 고찰해보려고 합니다
이전에 제가 처음으로 React Query를 사용했을때 블로그 글이 있어서 링크 남깁니다
https://kyung-a.tistory.com/40
React Query란?
서버 상태를 패칭, 캐싱, 동기화, 업데이트 하는 작업을 쉽게 해주는 라이브러리
저는 React Query가 Tanstack (오픈 소스 소프트웨어) Query로 되기 전에 사용했습니다
둘 차이점은 무엇이지? 했더니 원래부터 같은 회사의 라이브러리였던 것 같고 단지 v4 버전이 나오면서 이름을 바꾼 것 같습니다
제가 React Query를 처음 사용해본 프로젝트는 사내 어드민 프로젝트였습니다
해당 프로젝트에서 axios를 이용해 API를 호출하는 로직, 그리고 그 데이터를 fetching하는 로직을 React Query로 사용했습니다
사용 방법
const useProjectQuery = () => {
const fetchProject = useCallback(async () => {
const resp = await axios.get(API);
return resp.data;
}, []);
const { data, isSuccess, isLoading, refetch } = useQuery(
["fetchProject"],
fetchProject,
{
refetchOnWindowFocus: false,
retry: false,
},
);
return {
data,
isSuccess,
isLoading,
refetch,
};
};
export default useProjectQuery;
보통 이런식으로 공통 fetching hook을 만들어서 import해서 사용해왔습니다
useQuery에 다양한 옵션들을 사용하면 더 정교한 캐싱 및 리프레시 전략을 짤 수 있습니다.
아래에 언급될 Redux 로직과는 확연되게 간단한 로직입니다...ㅋㅋㅋㅋㅋㅋㅋㅋㅋ
Redux란?
여러 컴포넌트가 공유하는 상태를 중앙화하여 관라하기 위한 라이브러리
Redux는 3가지 원칙을 따른다고 합니다.
1. 애플리케이션의 모든 상태는 하나의 Store 안에 하나의 객체 트리 구조로 저장된다
2. 상태를 변화시키는 유일한 방법은 무슨일이 벌어지는 지를 묘사하는 Action 객체를 전달하는 방법뿐이다 (상태는 읽기 전용이다)
3. Action에 의해 상태 트리가 어떻게 변화하는지를 지정하기 위해 순수 reducer를 작성해야한다 (변화는 순수 함수로 작성 되어야한다)
여기서 핵심 개념을 말하자면
1. 스토어 (Store) : 애플리케이션의 상태를 저장하는 객체, 모든 상태를 관리합니다
2. 액션 (Action) : 상태를 변경하기위한 [의도]라는 개념을 나타내는 객체, 상태 변경에 필요한 데이터를 포함 할 수 있음
3. 리듀서 (Reducer) : 액션이 디스패치 되었을때 상태를 실제로 변경하는 함수
✅ 디스패치란? : 액션을 리덕스 스토어로 보내는 역할, 디스패치된(리덕스 스토어에 보내진) 액션은 리듀서에 전달되고, 리듀서는 그 액션에 따라 상태를 업데이트
거의 3년전에 Redux를 인강을 통해 처음 사용해보았습니다
그때 느낀점은 굉장히 어렵다란 느낌이 있었는데요 (실제로도 러닝커브가 높은 라이브러리)
왜 어려운가? 봤더니 Redux를 알기전에 Flux라는 패턴을 먼저 알아야하더군요
Flux 패턴
디자인 패턴중의 하나입니다
Aation → Dispatch → Store → View
그리고 View에서 Action이 발생하면 다시 Dispatcher → Store → View로 흐른다
(단방향)
Flux 패턴이라는 개념이 왜 생겨난건지 보니까,
[사용자 주소 변경]이라는 이벤트를 트리거 했을때, 다른 코드는 이 이벤트를 수신하고 그 다음 다른 이벤트를 트리거, 이 이벤트는 다시 다른 이벤트를 트리거 등등... 갑자기 20개의 이벤트가 하나의 큰 동기식 호출로 이어지고, 중간과정에서 무슨 일이 있는지, 처음에 어떤 이벤트 트리거가 발동됐는지 알기 어렵다라는 문제점 때문에 Flux라는 패턴이 나왔다고 합니다. 위에 내용을 보니 MVC 패턴이 떠오르네요
Redux는 이러한 Flux 패턴을 기반으로 만들어졌다고 합니다. 그러나 정작 React에서 쓰는 Redux의 패턴은 MVC 패턴과 유사하고, Redux 공식 문서에는 Flux와 달리 Redux에는 디스패처 라는 개념이 존재하지 않는다고 합니다 (dispatcher 없이 바로 reducer로 전달되기 때문, 그리고 단일 스토어이기 때문)
사용 방법
저는 회사에서 유저 서비스를 Redux를 사용했습니다. 비동기 처리 로직은 별도로 Redux-Thunk, Redux-saga를 이용하지 않고 직접 구현했습니다. 디렉토리는 아래와 같았습니다
stores
|
|─ todo
|─ ─ |─ actions
|─ ─ |─ api
|─ ─ |─ reducer
|─ ─ └─ thunks
└─ index
stores에서는 리듀서 결합, 커스텀 리듀서 정의, 미들웨어 바인딩, 스토어 생성, wrapper 타입 정의 로직이 작성되어 있고
하위 디렉토리에는 라우터별 action, api, reducer, thunks 파일이 위치해 있습니다
// action
const fetchProfile = createAsyncAction(
'auth/FETCH_PROFILE',
'auth/FETCH_PROFILE_SUCCESS',
'auth/FETCH_PROFILE_FAIL',
)<unknown, IProfile, AxiosError>()
// reducer
const isLoading = createReducer<boolean, IAuthAction>(false as boolean)
.handleAction(
[
fetchProfile.request
],
() => true,
)
.handleAction(
[
fetchProfile.success,
fetchProfile.failure
],
() => false,
)
const profile = createReducer<IProfile | null, IAuthAction>(null)
.handleAction(fetchProfile.success, (state, action) => action.payload)
.handleAction(clearProfile, () => null)
// thunks
export const fetchProfileThunk = createAsyncThunk(fetchProfile, requestProfile)
// api
export const requestProfile = async () => {
const resp = await axios.get<IProfileResponse>(API)
return convertCaseList(resp.data, ConvertType.CAMEL) as IProfile
}
// 컴포넌트 내에서
useEffect(() => {
dispatch(fetchProfileThunk())
}, [dispatch])
profile을 fetch하는 전반적인 로직입니다
- 액션 생성 : createAsyncAction을 이용해서 비동기 액션을 생성합니다. 성공 시 IProfile 타입의 데이터를 반환합니다
- 리듀서 : 여러 비동기 액션을 처리합니다. 현재 비동기 작업이 진행 중인지 여부를 나타낼 수 있습니다. fetchProfile.success 액션이 발생하면 서버에서 받은 데이터를 상태에 저장합니다
- Thunk (비동기 액션) : fetchProfile.request이 디스패치되어 로딩 시작, requestProfile API를 호출하고 성공시 fetchProfile.success, 실패시 fetchProfile.faulure의 액션이 디스패치됩니다. 성공하면 상태가 업데이트 됩니다
로직의 흐름을 설명하자면
- 클라이언트에서 fetchProfileThunk 호출
- fetchProfile.request 액션을 디스패치하여 isLoading을 true로 바꿈
- requestProfile API 요청 함수 실행, 성공하면 fetchProfile.success 액션이 디스패치
- profile 리듀서가 상태에 데이터를 저장하고 isLoading은 false로 변경
여기서 fetching 함수는 각 컴포넌트마다 useEffect를 통해서 호출해야합니다
그리고 또 하나 비동기 작업을 위해 직접 유틸리티 함수를 짰더라고요. 이건 제가 직접 짠건 아니지만 왜 그런걸까? 하는 의문이 있었습니다. 이유를 찾아보니 알 수 있었습니다
function createAsyncThunk<
A extends IAnyAsyncActionCreator,
F extends (...params: any[]) => Promise<any>
>(asyncActionCreator: A, promiseCreator: F) {
type Params = Parameters<F>
return function thunk(...params: Params) {
return async (dispatch: Dispatch) => {
const { request, success, failure } = asyncActionCreator
dispatch(request(undefined))
try {
const result = await promiseCreator(...params)
dispatch(success(result))
} catch (e) {
dispatch(failure(e))
}
}
}
}
1. 반복되는 패턴을 추상화 (코드 중복 방지)
2. 일관성 유지
3. 재사용성
4. 요구사항에 맞춤화 (특정한 에러처리 또는 API 응답 형식에 맞춘 데이터 반환 등....)
이렇게 공통된 유틸리티 함수를 사용하지 않으면 매번 액션 생성자 로직을 작성해줘야 하더라고요. 그 수고를 덜어낸 것 같습니다 (이런게... 추상화군....) 물론 redux toolkit의 createAsyncThunk를 사용해도 될 것 같습니다 해당 프로젝트는 redux toolkit을 쓰지 않았기에 직접 만들어준 것 같습니다
스토어가 업데이트 되지 않는 문제
제가 가끔가다 겪었던 이슈입니다 해당 페이지에서 업데이트 되었고, 스토어에서 액션도 잘 발생되고 업데이트가 되었는데 다른 페이지로 이동 또는 새로고침하면 값이 초기화 되어있던 문제입니다. 사실 그 당시 useRef를 이용해서 해결했으나, 지금 곰곰히 생각해보니 아마 SSR 상태 문제 또는 불변성 문제지 않았을까 싶습니다
서버 측에서는 업데이트가 되어있지 않은데, 클라이언트만 업데이트 되었거나 기존 상태를 유지하면서 업데이트를 하는게 아닌, 상태를 직접 수정하거나 그랬지 않았을까... 싶습니다 아무래도 ref를 썼으니 이랬을 확률도 높고, SSR 같은 경우에는 곳곳에 getServerSideProps를 사용했던 흔적도 있고, redux-persist 라이브러리를 썼었던 것도 있어서 합리적 의심을 해봅니다
한가지 더 의심이 되는 부분은 페이지 안에서 컴포넌트가 여러번 랜더링 되는 현상이 발생하다보니 스토어가 업데이트 되기전에 랜더링이 발생하여 불일치 되는 경우도 있었을 것 같습니다. 이럴땐 철저하게 isLoading 상태를 관리하거나, 조건부 렌더링을 하거나... 해야할 것 같습니다 이 부분은 페이지 안에 의존성이 많아질수록 어려운데 정 모르겠음 console.log를 찍어서 일일히 업데이트 되는 순간을 포착하는게 도움이 됩니다 (너무 노가다인가?) 그렇게 되면 어느순간 값이 덮어씌어졌다 업데이트 되는지 포착할 수 있더군요... 가끔은 아날로그도 도움이 됩니다 하핫^,^
그래서, Redux와 React Query의 차이?
애초에 둘은 비교 대상이 아닌 것 같습니다
그러나, 제가 상태 관리라는 큰 틀에서 사용해본 라이브러리가 Redux, React Query이기 때문에 질문 대상이 되는 것 같습니다
React Query
- 서버 상태 라이브러리
- 서버와 클라이언트 사이에서 일어나는 비동기적인 작업을 효율적이게 해줌
- 몇가지 옵션을 통해 자동으로 데이터를 리패칭
- 동일한 데이터 요청할 경우 캐싱된 정보를 이용 -> 이로인해 업데이트 시 지연이 거의 없음
- 비교 대상 : SWR 등등
Redux
- 클라이언트 상태 라이브러리
- API 통신 및 비동기 상태 관리를 위한 라이브러리가 아님 -> 별도의 로직을 구축해나가야 함
- 서버 측 상태가 아닌 로컬 상태의 관리가 많을 경우 용이
- 서버 데이터가 바뀐다고 해서 리랜더링, 리패칭, 스토어 업데이트가 자동으로 일어나지 않음
- 비교 대상 : context API 등등
지금 정리하면서 드는 생각이 두 개는 개념이 다른 라이브러리니까 같이 쓸 수도 있는거 아니야? 라는 생각이 들었습니다. 그럼 대체 될 수 있는 부분이 어딜까? 바로 thunks 부분입니다
// actions.js
export const updateUserRequest = () => ({ type: 'UPDATE_USER_REQUEST' });
export const updateUserSuccess = (user) => ({ type: 'UPDATE_USER_SUCCESS', payload: user });
export const updateUserFailure = (error) => ({ type: 'UPDATE_USER_FAILURE', payload: error });
const updateUser = async (userData) => {
const response = await axios.post('/api/updateUser', userData);
return response.data;
};
const useUpdateUser = () => {
const dispatch = useDispatch();
const mutation = useMutation(updateUser, {
onSuccess: (data) => {
dispatch(updateUserSuccess(data)); // 성공 시 Redux 상태 업데이트
},
onError: (error) => {
dispatch(updateUserFailure(error)); // 실패 시 Redux 상태 업데이트
},
});
return mutation;
};
export default useUpdateUser;
이런식으로 쓸 수 있지 않을까 싶습니다
그러나 확실히 관리 포인트가 늘어나는 기분은 드네요 워낙 리액트에서 상태라는 개념이 어려운데 그거에 관련된 라이브러리를 2개를 쓰자니... 마냥 쉬운 선택은 아닌 것 같습니다
React Query, Redux는 어떨때 쓰면 좋을까?
백오피스 프로젝트에 React Query를 써보는거 추천드립니다
백오피스 프로젝트는 대부분 읽기가 더 많고 생각됩니다. 사용자가 클라이언트에서 유저 서비스 만큼이나 많은 액션을 일으키지도 않고, 가장 최신의 데이터 현황을 빠르게 보는게 중요하다고 생각합니다. 그렇기에 오래된 데이터를 빠르게 감지하고 업데이트 해주는 React Query가 더 적합하다는 생각이 듭니다. 또한 관리자분들은 어드민을 켜놓고 내려두는 경우가 더 많다고 생각 드는데요 ㅎㅎ 그럴때 refetchOnWindowFocus 옵션을 써주면 딱이지 않나 싶습니다
반면, 유저 서비스에서는 어떠한 비즈니스냐에 따라 많이 다를 것 같습니다. 개인적으로 서비스 규모도 따져야 한다고 생각하고... 커스터마이징이 많이 필요하다면 Redux가 좋은 선택일 수 있습니다. 뭐가 되었든 클라이언트 상태도, 비동기도 관리할 수 있으니까요. React Query를 써보고 느낀점은 데이터 패칭에 따른 클라이언트단에서의 다양한 상태를 업데이트 해야한다면 분명 부족한데라는 느낌이 들겁니다 (Recoil를 써야하나? 싶은...)
결론은 복잡하지만 확장성 있는 로직을 짜야한다면 Redux, 클라이언트에서 복잡한 로직이 적은 편이고 서버의 데이터가 더 중요하다면 React Query를 추천드리는게 제 개인적인 생각입니다
'Frontend' 카테고리의 다른 글
useCallback의 오남용 (2) | 2024.09.07 |
---|---|
이벤트 리스너(event listener)를 제거 해야하는 이유 (1) | 2024.08.30 |
React Query 사용기 (1) | 2022.07.31 |
React Hook Form | 내가 실무에서 쓰는 법 (0) | 2022.07.28 |
React | Hook 정리 + 18 버전 새로운 Hook (0) | 2022.07.28 |
댓글