y.developer
[TIL] Day 65 Recoil + React Query 비동기 상태 관리, 페이지 예외 처리 본문
2024.01.05 금
Recoil 중요 개념
Recoil은 React 어플리케이션의 상태 관리를 도와주는 라이브러리입니다. 이를 이해하기 위해 몇 가지 중요한 개념을 알아보겠습니다.
- atom (원자): Recoil에서의 기본적인 상태 단위입니다. atom은 전역 상태를 나타내며, 여러 컴포넌트 간에 공유됩니다. 예를 들어, 로그인 상태를 저장하는 atom을 만들 수 있습니다.
- selector (셀렉터): selector는 atom에서 파생된 데이터를 계산하고 반환하는 함수입니다. 이를 통해 더 복잡한 데이터 변환 및 가공을 할 수 있습니다. 예를 들어, 로그인 상태에 따라 다른 멤버 정보를 가져오는 selector를 만들 수 있습니다.
- useRecoilState와 useRecoilValue: useRecoilState는 atom의 현재 상태와 그 상태를 업데이트하는 함수를 반환합니다. useRecoilValue는 atom 또는 selector의 현재 값을 반환합니다. 이 두 훅을 사용하여 Recoil 상태를 컴포넌트에 연결할 수 있습니다.
- RecoilRoot: Recoil을 사용하는 어플리케이션을 감싸는 컴포넌트입니다. 이 컴포넌트는 Recoil 상태의 범위를 설정하고, Recoil을 사용하는 모든 하위 컴포넌트에서 상태를 공유할 수 있도록 합니다.
간단히 말하면, Recoil은 React 어플리케이션에서 전역 상태를 효과적으로 관리하기 위한 도구로 사용됩니다. atom으로 상태를 정의하고, selector로 복잡한 데이터 변환을 수행하며, useRecoilState와 useRecoilValue로 이를 컴포넌트에 적용합니다. RecoilRoot는 Recoil의 상태 범위를 설정합니다.
React Query 중요 개념
React Query는 React 어플리케이션에서 데이터를 관리하고 처리하기 위한 강력한 라이브러리입니다. React Query에 대한 몇 가지 중요한 개념을 알아보겠습니다.
- Query (쿼리): React Query의 핵심 개념 중 하나입니다. Query는 데이터를 가져오고 캐시하며 관리하는 객체입니다. 주로 데이터를 가져오는 비동기 작업에 사용됩니다.
- QueryKey (쿼리 키): Query를 식별하는 데 사용되는 문자열 또는 배열입니다. 유니크한 값을 가져야하며, 데이터를 가져오는 함수와 연결됩니다.
- useQuery 훅: React Query에서 가장 기본적으로 사용되는 훅 중 하나입니다. useQuery를 사용하여 데이터를 가져오고 관리하는 데 필요한 여러 기능들을 활용할 수 있습니다.
- Mutation (뮤테이션): 데이터를 변경하고 업데이트하는 데 사용되는 개념입니다. Mutation 객체는 데이터를 업데이트하고 이에 대한 비동기 작업을 처리하는 데 사용됩니다.
- QueryClient 및 QueryClientProvider: React Query의 핵심 클래스 및 프로바이더입니다. QueryClient는 쿼리 및 뮤테이션을 추적하고 캐시하는 데 사용되며, QueryClientProvider는 React 어플리케이션 전체에서 QueryClient를 사용할 수 있게 만듭니다.
- React Query Devtools: 개발 도구 확장 프로그램으로, React Query의 상태와 디버깅을 용이하게 해주는 도구입니다.
- Query Invalidation (쿼리 무효화): 데이터를 갱신하고 기존 캐시를 무효화하여 새로운 데이터를 다시 가져오는 과정입니다. 쿼리 무효화는 자동으로 처리되어 데이터의 일관성을 유지합니다.
- Query Retry (쿼리 재시도): 쿼리가 실패하면 자동으로 재시도하는 기능으로, 네트워크 문제 등에 대응할 수 있습니다.
React Query는 간편한 API와 강력한 성능을 통해 데이터 관리를 향상시키는데 사용됩니다. 이러한 기능들을 적절히 활용하면 복잡한 상태 관리와 비동기 데이터 처리를 효과적으로 다룰 수 있습니다.
예시 1 (Recoil)
이 예시에서는 Recoil을 사용하여 비동기적으로 데이터를 가져오고 관리하는 방법을 보여줍니다. fetchDataAsync 함수는 모의(mock)로 비동기 데이터를 가져오는 함수이며, Recoil atom과 selector를 사용하여 데이터를 관리하고 컴포넌트에서 해당 상태를 사용합니다.
import { atom, selector, useRecoilState, useRecoilValue, RecoilRoot } from 'recoil';
// 비동기적으로 데이터를 가져오는 함수를 모의(mock)로 대체합니다.
const fetchDataAsync = async () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ data: '비동기 데이터' });
}, 2000);
});
};
// Recoil atom 정의
const asyncDataState = atom({
key: 'asyncDataState',
default: {
data: null,
loading: false,
error: null,
},
});
// 비동기 데이터를 처리하는 selector 정의
const asyncDataSelector = selector({
key: 'asyncDataSelector',
get: async ({ get }) => {
const previousDataState = get(asyncDataState);
try {
// 데이터 로딩 상태를 업데이트
get(asyncDataState);
const response = await fetchDataAsync();
// 비동기 데이터를 성공적으로 가져왔을 때 상태 업데이트
return { data: response.data, loading: false, error: null };
} catch (error) {
// 에러 발생 시 상태 업데이트
return { data: null, loading: false, error: error.message };
}
},
});
// 컴포넌트에서 Recoil 사용
const AsyncDataComponent = () => {
const asyncData = useRecoilValue(asyncDataSelector);
if (asyncData.loading) {
return <p>로딩 중...</p>;
}
if (asyncData.error) {
return <p>에러 발생: {asyncData.error}</p>;
}
return <p>비동기 데이터: {asyncData.data}</p>;
};
const App = () => (
<RecoilRoot>
<AsyncDataComponent />
</RecoilRoot>
);
export default App;
예시 2 (Recoil)
멤버 로그인 여부를 확인하고 멤버 정보를 가져오는 예시
이 예시에서는 Recoil을 사용하여 멤버의 로그인 여부를 확인하고, 로그인된 경우에만 멤버 정보를 비동기적으로 가져오는 방법을 보여줍니다.
import { atom, selector, useRecoilState, useRecoilValue, RecoilRoot } from 'recoil';
// 멤버 정보를 비동기적으로 가져오는 함수를 모의(mock)로 대체합니다.
const fetchMemberInfoAsync = async () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ username: '사용자명', email: 'user@example.com' });
}, 2000);
});
};
// Recoil atom 정의
const loggedInState = atom({
key: 'loggedInState',
default: false,
});
const memberInfoState = atom({
key: 'memberInfoState',
default: {
username: '',
email: '',
loading: false,
error: null,
},
});
// 멤버 정보를 처리하는 selector 정의
const memberInfoSelector = selector({
key: 'memberInfoSelector',
get: async ({ get }) => {
const isLoggedIn = get(loggedInState);
if (!isLoggedIn) {
// 로그인되지 않은 경우
return { username: '', email: '', loading: false, error: '로그인이 필요합니다.' };
}
const previousMemberInfoState = get(memberInfoState);
try {
// 멤버 정보 로딩 상태를 업데이트
get(memberInfoState);
const response = await fetchMemberInfoAsync();
// 멤버 정보를 성공적으로 가져왔을 때 상태 업데이트
return { username: response.username, email: response.email, loading: false, error: null };
} catch (error) {
// 에러 발생 시 상태 업데이트
return { username: '', email: '', loading: false, error: error.message };
}
},
});
// 컴포넌트에서 Recoil 사용
const MemberInfoComponent = () => {
const memberInfo = useRecoilValue(memberInfoSelector);
if (memberInfo.loading) {
return <p>멤버 정보 로딩 중...</p>;
}
if (memberInfo.error) {
return <p>에러 발생: {memberInfo.error}</p>;
}
return (
<>
<p>사용자명: {memberInfo.username}</p>
<p>이메일: {memberInfo.email}</p>
</>
);
};
const App = () => (
<RecoilRoot>
<MemberInfoComponent />
</RecoilRoot>
);
export default App;
예시 3 (ReactQuery)
React Query를 사용하면 논리를 더 간결하게 표현할 수 있습니다.
React Query를 사용하면 상태 및 비동기 데이터 관리를 더욱 간소화하고, 특히 API 호출 및 상태 관리에 특화된 기능들을 활용할 수 있습니다.
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
// 멤버 정보를 비동기적으로 가져오는 함수를 모의(mock)로 대체합니다.
const fetchMemberInfoAsync = async () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ username: '사용자명', email: 'user@example.com' });
}, 2000);
});
};
// React Query를 초기화합니다.
const queryClient = new QueryClient();
// 컴포넌트에서 React Query 사용
const MemberInfoComponent = () => {
const { data, isLoading, isError } = useQuery('memberInfo', fetchMemberInfoAsync);
if (isLoading) {
return <p>멤버 정보 로딩 중...</p>;
}
if (isError) {
return <p>에러 발생: 멤버 정보를 가져오는 데 문제가 있습니다.</p>;
}
return (
<>
<p>사용자명: {data.username}</p>
<p>이메일: {data.email}</p>
</>
);
};
const App = () => (
<QueryClientProvider client={queryClient}>
<MemberInfoComponent />
<ReactQueryDevtools /> {/* 개발 도구를 사용할 경우 추가 */}
</QueryClientProvider>
);
export default App;
예시 4 (Recoil + ReactQuery)
Recoil와 React Query를 함께 사용하여 상태 관리를 최적화
이 코드에서는 React Query를 사용하여 데이터를 가져오고, Recoil을 사용하여 로그인 상태를 저장합니다. 로그인 상태에 따라 React Query의 useQuery 훅을 활용하여 멤버 정보를 비동기적으로 가져옵니다. Recoil은 전역 상태 관리를 담당하며, React Query는 데이터 요청 및 관리에 특화된 역할을 수행합니다.
import { useState } from 'react';
import { RecoilRoot, atom, useRecoilState } from 'recoil';
import { useQuery, QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
// 멤버 정보를 비동기적으로 가져오는 함수를 모의(mock)로 대체합니다.
const fetchMemberInfoAsync = async () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ username: '사용자명', email: 'user@example.com' });
}, 2000);
});
};
// React Query를 초기화합니다.
const queryClient = new QueryClient();
// Recoil atom 정의
const loggedInState = atom({
key: 'loggedInState',
default: false,
});
// 컴포넌트에서 React Query 및 Recoil 사용
const MemberInfoComponent = () => {
const [isLoggedIn, setIsLoggedIn] = useRecoilState(loggedInState);
const { data: memberInfo, isLoading, isError } = useQuery('memberInfo', fetchMemberInfoAsync, {
enabled: isLoggedIn,
});
if (!isLoggedIn) {
return <p>로그인이 필요합니다.</p>;
}
if (isLoading) {
return <p>멤버 정보 로딩 중...</p>;
}
if (isError) {
return <p>에러 발생: 멤버 정보를 가져오는 데 문제가 있습니다.</p>;
}
return (
<>
<p>사용자명: {memberInfo.username}</p>
<p>이메일: {memberInfo.email}</p>
</>
);
};
const App = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
return (
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<button onClick={() => setIsLoggedIn(!isLoggedIn)}>
{isLoggedIn ? '로그아웃' : '로그인'}
</button>
<MemberInfoComponent />
<ReactQueryDevtools />
</QueryClientProvider>
</RecoilRoot>
);
};
export default App;
예시 5 (Next.js + 페이지 예외 처리)
Next.js에서 로그인 상태에 따라 접근 가능한 페이지와 접근 불가능한 페이지를 예외 처리
이 코드에서는 ProtectedPage에서 useEffect를 사용하여 페이지가 마운트되었을 때 로그인 상태를 확인하고, 로그인되어 있지 않으면 로그인 페이지로 리다이렉트합니다. HomePage에서는 Recoil과 React Query를 사용하여 멤버 정보를 가져오는 예시를 보여줍니다. 이를 참고하여 로그인이 필요한 페이지와 로그인이 필요하지 않은 페이지를 구분하여 예외 처리할 수 있습니다.
// pages/index.js (예시로 사용자 정보를 보여주는 페이지)
import { useState } from 'react';
import { useQuery, QueryClient, QueryClientProvider } from 'react-query';
import { RecoilRoot, atom, useRecoilState } from 'recoil';
import { useRouter } from 'next/router';
const fetchMemberInfoAsync = async () => {
// 멤버 정보를 비동기적으로 가져오는 함수 (모의(mock)로 대체)
return new Promise((resolve) => {
setTimeout(() => {
resolve({ username: '사용자명', email: 'user@example.com' });
}, 2000);
});
};
const queryClient = new QueryClient();
const loggedInState = atom({
key: 'loggedInState',
default: false,
});
const MemberInfoComponent = () => {
const [isLoggedIn] = useRecoilState(loggedInState);
const { data: memberInfo, isLoading, isError } = useQuery('memberInfo', fetchMemberInfoAsync, {
enabled: isLoggedIn,
});
if (!isLoggedIn) {
return <p>로그인이 필요합니다.</p>;
}
if (isLoading) {
return <p>멤버 정보 로딩 중...</p>;
}
if (isError) {
return <p>에러 발생: 멤버 정보를 가져오는 데 문제가 있습니다.</p>;
}
return (
<>
<p>사용자명: {memberInfo.username}</p>
<p>이메일: {memberInfo.email}</p>
</>
);
};
const HomePage = () => (
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<MemberInfoComponent />
</QueryClientProvider>
</RecoilRoot>
);
export default HomePage;
// pages/protected.js (예시로 로그인이 필요한 페이지)
import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
import { loggedInState } from '../components/LoginLogic'; // 로그인 상태를 관리하는 Recoil atom을 임포트
import { useRouter } from 'next/router';
const ProtectedPage = () => {
const [isLoggedIn] = useRecoilState(loggedInState);
const router = useRouter();
useEffect(() => {
// 로그인이 되어있지 않으면 로그인 페이지로 이동
if (!isLoggedIn) {
router.push('/login');
}
}, [isLoggedIn, router]);
return (
<div>
<h1>보호된 페이지</h1>
{/* 로그인이 되어있는 경우에만 표시됨 */}
</div>
);
};
export default ProtectedPage;