diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index da9fec22..ed1d7664 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -9,11 +9,18 @@ module.exports = {
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
- plugins: ['react-refresh'],
+ plugins: ['react-refresh', 'react-hooks'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true }
]
- }
+ },
+ 'react-hooks/rules-of-hooks': 'error',
+ 'react-hooks/exhaustive-deps': [
+ 'warn',
+ {
+ additionalHooks: 'useRecoilCallback'
+ }
+ ]
};
diff --git a/package-lock.json b/package-lock.json
index 26dc31fa..228a9216 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,13 +10,18 @@
"dependencies": {
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
+ "@tanstack/react-query": "^5.17.9",
+ "@tanstack/react-query-devtools": "^5.17.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-router-dom": "^6.21.1"
+ "react-error-boundary": "^4.0.12",
+ "react-router-dom": "^6.21.1",
+ "recoil": "^0.7.7"
},
"devDependencies": {
"@swc/core": "^1.3.102",
"@swc/jest": "^0.2.29",
+ "@tanstack/eslint-plugin-query": "^5.17.7",
"@types/jest": "^29.5.11",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
@@ -2472,6 +2477,193 @@
"integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==",
"dev": true
},
+ "node_modules/@tanstack/eslint-plugin-query": {
+ "version": "5.17.7",
+ "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.17.7.tgz",
+ "integrity": "sha512-RpKZXIuplRrUZLqqh+jTM1yJP8/Ck21FpaSB5uGyc9LY8LNwxC8AwgaRAXVOZzKVeQMunnt3HrK83HME+7jnGw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/utils": "^5.62.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "eslint": "^8.0.0"
+ }
+ },
+ "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/scope-manager": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
+ "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/visitor-keys": "5.62.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/types": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
+ "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
+ "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/visitor-keys": "5.62.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/utils": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
+ "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@types/json-schema": "^7.0.9",
+ "@types/semver": "^7.3.12",
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "eslint-scope": "^5.1.1",
+ "semver": "^7.3.7"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
+ "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@tanstack/eslint-plugin-query/node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@tanstack/eslint-plugin-query/node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/@tanstack/query-core": {
+ "version": "5.17.9",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.17.9.tgz",
+ "integrity": "sha512-8xcvpWIPaRMDNLMvG9ugcUJMgFK316ZsqkPPbsI+TMZsb10N9jk0B6XgPk4/kgWC2ziHyWR7n7wUhxmD0pChQw==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/query-devtools": {
+ "version": "5.17.7",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.17.7.tgz",
+ "integrity": "sha512-TfgvOqza5K7Sk6slxqkRIvXlEJoUoPSsGGwpuYSrpqgSwLSSvPPpZhq7hv7hcY5IvRoTNGoq6+MT01C/jILqoQ==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.17.9",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.17.9.tgz",
+ "integrity": "sha512-M5E9gwUq1Stby/pdlYjBlL24euIVuGbWKIFCbtnQxSdXI4PgzjTSdXdV3QE6fc+itF+TUvX/JPTKIwq8yuBXcg==",
+ "dependencies": {
+ "@tanstack/query-core": "5.17.9"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0"
+ }
+ },
+ "node_modules/@tanstack/react-query-devtools": {
+ "version": "5.17.9",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.17.9.tgz",
+ "integrity": "sha512-1viWP/jlO0LaeCdtTFqtF1k2RfM3KVpvwVffWv+PMNkS2u4s8YGUM17r3p82udbF9BY1mE7aHqQ3MM1errF5lQ==",
+ "dependencies": {
+ "@tanstack/query-devtools": "5.17.7"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "@tanstack/react-query": "^5.17.9",
+ "react": "^18.0.0"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -4125,6 +4317,11 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
+ "node_modules/hamt_plus": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz",
+ "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA=="
+ },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -6049,6 +6246,17 @@
"react": "^18.2.0"
}
},
+ "node_modules/react-error-boundary": {
+ "version": "4.0.12",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.12.tgz",
+ "integrity": "sha512-kJdxdEYlb7CPC1A0SeUY38cHpjuu6UkvzKiAmqmOFL21VRfMhOcWxTCBgLVCO0VEMh9JhFNcVaXlV4/BTpiwOA==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -6084,6 +6292,25 @@
"react-dom": ">=16.8"
}
},
+ "node_modules/recoil": {
+ "version": "0.7.7",
+ "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz",
+ "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==",
+ "dependencies": {
+ "hamt_plus": "1.0.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
@@ -6554,6 +6781,27 @@
}
}
},
+ "node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
+ },
+ "node_modules/tsutils": {
+ "version": "3.21.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+ "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^1.8.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+ }
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
diff --git a/package.json b/package.json
index bb78fda5..f6bce8c9 100644
--- a/package.json
+++ b/package.json
@@ -13,13 +13,18 @@
"dependencies": {
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
+ "@tanstack/react-query": "^5.17.9",
+ "@tanstack/react-query-devtools": "^5.17.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-router-dom": "^6.21.1"
+ "react-error-boundary": "^4.0.12",
+ "react-router-dom": "^6.21.1",
+ "recoil": "^0.7.7"
},
"devDependencies": {
"@swc/core": "^1.3.102",
"@swc/jest": "^0.2.29",
+ "@tanstack/eslint-plugin-query": "^5.17.7",
"@types/jest": "^29.5.11",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
diff --git a/src/App.tsx b/src/App.tsx
index 1e1181b4..5389b322 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,19 +1,59 @@
-import GlobalStyle from '@styles/GlobalStyle';
-import { ThemeProvider } from '@emotion/react';
-import theme from '@styles/theme';
import { BrowserRouter } from 'react-router-dom';
+import { ThemeProvider } from '@emotion/react';
+import {
+ QueryClient,
+ QueryClientProvider,
+ useQueryErrorResetBoundary
+} from '@tanstack/react-query';
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
+import { RecoilRoot } from 'recoil';
+import { ErrorBoundary } from 'react-error-boundary';
+import { Suspense } from 'react';
+
import { MainRouter } from './routes';
+import GlobalStyle from '@styles/GlobalStyle';
+import theme from '@styles/theme';
+import { ErrorApp } from '@components/ErrorFallback';
+import { LoadingApp } from '@components/Loading';
+
+const queryClient = new QueryClient();
+
+/* HACK: logError 논의 필요
+
+ const logError = (error: Error, info: { componentStack: string }) => {
+ // Do something with the error, e.g. log to an external API
+ };
+
+ */
const App = () => {
+ const { reset } = useQueryErrorResetBoundary();
+
return (
- <>
-
{error.message}
+
+