-
Notifications
You must be signed in to change notification settings - Fork 1
상태의 분리
사용자가 인터페이스와 상호작용하면서 변경되는 **데이터**
이전에 프론트엔드의 관심사 분리와 상태 관리의 중요성에 대해 다룬 적이 있습니다. 프론트엔드 디자인 변천사
여기서 상태 관리 또한 관심사의 분리가 가능해집니다. 바로 클라이언트 상태
와 서버 상태
로 말이죠.
기존에는 상태를 따로 나누지 않고 redux만으로 상태 관리를 하는 경우가 많았습니다. 다만 클라이언트 상태와 서버 상태는 본질적 차이가 있어서 단일 도구로 관리하기 어렵고, 이에 따른 관심사 분리가 필요해졌습니다.
[클라이언트 상태]
클라이언트 내부에서 소유하고 있고, 온전히 제어가능한 상태들
- 클라이언트에서 항상 제어가 가능하기에 항상 동기적인 상태
[서버 상태]
클라이언트 외부에서 소유하고 있고, 클라이언트에서는 일종의 캐시인 상태들
- Fetching이나 Updating에 비동기 API가 필요함
- 다른 사람들과 공유되는 것으로 사용자가 모르는 사이에 변경될 수 있음
- 신경 쓰지 않는다면 잠재적으로 “out of date”가 될 가능성을 지님
결국 둘의 차이는 소유 여부라고 볼 수 있습니다. 클라이언트가 소유하고 있는지 그렇지 않은지에 따라 나뉜다는 것입니다. 이는 결국 상태의 특성으로 드러납니다.
클라이언트 상태는 클라이언트가 완전히 제어할 수 있기 때문에 동기적입니다. 상태 변화는 즉시 반영되고, 동기적으로 처리할 수 있습니다.
서버 상태는 클라이언트의 제어를 벗어난 외부에서 가져오는 데이터이기 때문에 비동기적입니다. 이에 서버에서 데이터를 받아오거나, 데이터가 신선한지 확인하는 등의 추가적인 처리가 필요합니다.
관심사 분리가 이루어지기 전에는 Redux를 사용하여 클라이언트 상태와 서버 상태를 모두 관리하는 경우가 많았습니다. 이때 store에서 전역 상태를 저장하고 관리하는 동시에, 비동기 통신 로직도 함께 처리되었죠.
아래 redux store 설정 예시 코드를 보면 클라이언트 상태와 서버 상태가 같은 reducer에 정의되어 있고, 비동기 요청이 상태 변경 로직과 섞여 있어서 코드가 매우 복잡하다는 것을 확인할 수 있습니다.
// 액션 타입 정의
const FETCH_USERS_REQUEST = 'FETCH_USERS_REQUEST';
const FETCH_USERS_SUCCESS = 'FETCH_USERS_SUCCESS';
const FETCH_USERS_FAILURE = 'FETCH_USERS_FAILURE';
// 액션 생성자 정의
const fetchUsersRequest = () => ({ type: FETCH_USERS_REQUEST });
const fetchUsersSuccess = (users) => ({ type: FETCH_USERS_SUCCESS, payload: users });
const fetchUsersFailure = (error) => ({ type: FETCH_USERS_FAILURE, payload: error });
// 비동기 액션
const fetchUsers = () => async (dispatch) => {
dispatch(fetchUsersRequest()); // 로딩 시작
try {
const response = await fetch('https://api.example.com/users'); // 서버 데이터 요청
const data = await response.json();
dispatch(fetchUsersSuccess(data)); // 데이터 성공적으로 받아왔을 때
} catch (error) {
dispatch(fetchUsersFailure(error.message)); // 오류 발생 시
}
};
// 리듀서 정의
const reducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_USERS_REQUEST:
return { ...state, loading: true, error: null };
case FETCH_USERS_SUCCESS:
return { ...state, loading: false, users: action.payload };
case FETCH_USERS_FAILURE:
return { ...state, loading: false, error: action.payload };
default:
return state;
}
};
이런 문제를 해결하기 위해 서버 상태 관리를 위한 라이브러리들이 출현했습니다. 대표적인 라이브러리는 바로 React-Query입니다.
React Query는 서버 상태를 관리하기 위한 최고의 라이브러리 중 하나라고 소개하고 있으며, 실제로 복잡한 서버 상태 관리를 위한 유용한 기능들을 제공합니다.
[React-Query]
"전역 상태"를 건드리지 않고도 애플리케이션에서 데이터를 Fetch, Update, Cache할 수 있습니다.
-
전역 데이터 관리
: 캐싱과 Key 기반 관리로 불필요한 네트워크 요청을 줄이고, 전역에서 안전하게 데이터 접근이 가능합니다. -
데이터 신선도 유지 및 자동 동기화
: 자동 갱신과 캐시 수명 관리(staleTime, cacheTime)로 최신 데이터를 유지하며, 포커스 변경 시나 네트워크 재연결 시 자동으로 동기화합니다. -
상태 업데이트 및 낙관적 업데이트
: 서버와 클라이언트 상태를 자동으로 동기화하고, 예상되는 결과를 먼저 반영하는 낙관적 업데이트로 반응성을 높입니다. -
에러 핸들링 및 재시도
: 요청 실패 시 자동 재시도를 통해 안정성을 유지합니다. -
개발 편의성
: Devtools로 API 요청 및 캐시 상태를 실시간 모니터링하여 디버깅을 쉽게 합니다.
서버 상태와 클라이언트 상태가 분리되면서, 클라이언트 상태 관리를 간소화하려는 필요성이 커졌습니다. 이에 따라 Recoil, Zustand와 같은 라이브러리들이 등장했습니다.
이 라이브러리들은 Redux와 달리 리듀서나 액션 정의 없이 상태와 상태 변경 함수를 함께 설정할 수 있어, 더 간단하게 사용할 수 있고 보일러플레이트가 적습니다.
-
Recoil
: 상태를 atom 단위로 나눠 관리하여, 구독 중인 컴포넌트만 리렌더링합니다. -
Zustand
: 컴포넌트별로 구독한 상태 일부만 리렌더링하여 성능을 최적화합니다.
