From 84915067ce33da065c4bf6c16e322bcd49258914 Mon Sep 17 00:00:00 2001 From: hyunmin200 Date: Wed, 3 Apr 2024 08:17:46 +0900 Subject: [PATCH 1/4] My First React-Query --- src/api.ts | 5 +++++ src/index.css | 3 --- src/index.tsx | 12 ++++++++---- src/routes/Coins.tsx | 19 ++++++------------- 4 files changed, 19 insertions(+), 20 deletions(-) create mode 100644 src/api.ts delete mode 100644 src/index.css diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..b466755 --- /dev/null +++ b/src/api.ts @@ -0,0 +1,5 @@ +export async function fetchCoins() { + return await fetch("https://api.coinpaprika.com/v1/coins").then((response) => + response.json(), + ); +} diff --git a/src/index.css b/src/index.css deleted file mode 100644 index 42b2fc0..0000000 --- a/src/index.css +++ /dev/null @@ -1,3 +0,0 @@ -body { - margin: 0px; -} diff --git a/src/index.tsx b/src/index.tsx index cbf84da..57dd831 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,15 +1,19 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; -import "./index.css"; import { ThemeProvider } from "styled-components"; import { theme } from "./theme"; +import { QueryClient, QueryClientProvider } from "react-query"; + +const queryClient = new QueryClient(); const root = ReactDOM.createRoot( document.getElementById("root") as HTMLElement, ); root.render( - - - , + + + + + , ); diff --git a/src/routes/Coins.tsx b/src/routes/Coins.tsx index d4ab511..dfd9b91 100644 --- a/src/routes/Coins.tsx +++ b/src/routes/Coins.tsx @@ -1,6 +1,8 @@ import React, { useEffect, useState } from "react"; +import { useQueries, useQuery } from "react-query"; import { Link } from "react-router-dom"; import { styled } from "styled-components"; +import { fetchCoins } from "../api"; const Container = styled.div` padding: 0px 20px; @@ -56,7 +58,7 @@ const Img = styled.img` margin-right: 20px; `; -interface CoinInterface { +interface ICoin { id: string; name: string; symbol: string; @@ -67,26 +69,17 @@ interface CoinInterface { } function Coins() { - const [coins, setCoins] = useState([]); - const [loading, setLoading] = useState(true); - useEffect(() => { - (async () => { - const response = await fetch("https://api.coinpaprika.com/v1/coins"); - const json = await response.json(); - setCoins(json.slice(0, 100)); - setLoading(false); - })(); - }, []); + const { isLoading, data } = useQuery("allCoins", fetchCoins); return (
Coins
- {loading ? ( + {isLoading ? ( 😫loading😫 ) : ( - {coins.map((coin) => ( + {data?.slice(0, 100).map((coin) => ( Date: Thu, 4 Apr 2024 08:16:34 +0900 Subject: [PATCH 2/4] react-query to coin.tsx --- src/App.tsx | 2 ++ src/api.ts | 16 +++++++++++++-- src/routes/Coin.tsx | 49 ++++++++++++++++++++++----------------------- 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index c09ecec..2541f5f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import styled, { createGlobalStyle } from "styled-components"; import Router from "./router"; +import { ReactQueryDevtools } from "react-query/devtools"; const GlobalStyle = createGlobalStyle` /* http://meyerweb.com/eric/tools/css/reset/ @@ -76,6 +77,7 @@ function App() { <> + ); } diff --git a/src/api.ts b/src/api.ts index b466755..21d92bc 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,5 +1,17 @@ -export async function fetchCoins() { - return await fetch("https://api.coinpaprika.com/v1/coins").then((response) => +const BASE_URL = `https://api.coinpaprika.com/v1`; + +export function fetchCoins() { + return fetch(`${BASE_URL}/coins`).then((response) => response.json()); +} + +export function fetchCoinInfo(coinId: string) { + return fetch(`${BASE_URL}/coins/${coinId}`).then((response) => + response.json(), + ); +} + +export function fetchCoinTickers(coinId: string) { + return fetch(`${BASE_URL}/tickers/${coinId}`).then((response) => response.json(), ); } diff --git a/src/routes/Coin.tsx b/src/routes/Coin.tsx index c5931d8..ce77006 100644 --- a/src/routes/Coin.tsx +++ b/src/routes/Coin.tsx @@ -10,6 +10,8 @@ import { import { styled } from "styled-components"; import Price from "./Price"; import Chart from "./Chart"; +import { useQuery } from "react-query"; +import { fetchCoinInfo, fetchCoinTickers } from "../api"; const Container = styled.div` padding: 0px 20px; @@ -147,32 +149,29 @@ interface PriceData { } function Coin() { - const [loading, setLoading] = useState(true); const { coinId } = useParams(); const { state } = useLocation(); - const [info, setInfo] = useState(); - const [priceInfo, setPriceInfo] = useState(); const priceMatch = useRouteMatch("/:coinId/price"); const chartMatch = useRouteMatch("/:coinId/chart"); - useEffect(() => { - (async () => { - const infoData = await ( - await fetch(`https://api.coinpaprika.com/v1/coins/${coinId}`) - ).json(); - console.log(infoData); - setInfo(infoData); - const priceData = await ( - await fetch(`https://api.coinpaprika.com/v1/tickers/${coinId}`) - ).json(); - setPriceInfo(priceData); - setLoading(false); - })(); - }, []); + const { isLoading: infoLoading, data: infoData } = useQuery( + ["info", coinId], + () => fetchCoinInfo(coinId), + ); + const { isLoading: tickersLoading, data: tickerData } = useQuery( + ["tickers", coinId], + () => fetchCoinTickers(coinId), + ); + + const loading = infoLoading || tickersLoading; return (
- {state?.name ? state.name : loading ? "코인 로딩중..." : info?.name} + {state?.name + ? state.name + : loading + ? "코인 로딩중..." + : infoData?.name}
{loading ? ( @@ -182,26 +181,26 @@ function Coin() { Rank: - {info?.rank} + {infoData?.rank} Symbol: - ${info?.symbol} + ${infoData?.symbol} Open Source: - {info?.open_source ? "Yes" : "No"} + {infoData?.open_source ? "Yes" : "No"} - {info?.description} + {infoData?.description} - Total Suply: - {priceInfo?.total_supply} + Total Supply: + {tickerData?.total_supply} Max Supply: - {priceInfo?.max_supply} + {tickerData?.max_supply} From 91540b4620ffbc7f1b8a70f5ccbb3f04e99ed76f Mon Sep 17 00:00:00 2001 From: hyunmin200 Date: Thu, 4 Apr 2024 17:04:46 +0900 Subject: [PATCH 3/4] complete chart --- package-lock.json | 154 +++++++++++++++++++++++++++++++++++++++++++ package.json | 4 ++ src/api.ts | 6 ++ src/routes/Chart.tsx | 86 +++++++++++++++++++++++- src/routes/Coin.tsx | 49 +++++++++----- src/routes/Coins.tsx | 4 ++ 6 files changed, 285 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index a9c36f4..641ea31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,11 @@ "@types/react": "^18.2.73", "@types/react-dom": "^18.2.22", "@types/styled-components": "^5.1.34", + "apexcharts": "^3.48.0", "react": "^18.2.0", + "react-apexcharts": "^1.4.1", "react-dom": "^18.2.0", + "react-helmet": "^6.1.0", "react-query": "^3.39.3", "react-router-dom": "5.3", "react-scripts": "5.0.1", @@ -26,6 +29,7 @@ "web-vitals": "^2.1.4" }, "devDependencies": { + "@types/react-helmet": "^6.1.11", "@types/react-router-dom": "^5.3.3" } }, @@ -4266,6 +4270,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-helmet": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.11.tgz", + "integrity": "sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-router": { "version": "5.1.20", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", @@ -4758,6 +4771,11 @@ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, + "node_modules/@yr/monotone-cubic-spline": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", + "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==" + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -4982,6 +5000,20 @@ "node": ">= 8" } }, + "node_modules/apexcharts": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.48.0.tgz", + "integrity": "sha512-Lhpj1Ij6lKlrUke8gf+P+SE6uGUn+Pe1TnCJ+zqrY0YMvbqM3LMb1lY+eybbTczUyk0RmMZomlTa2NgX2EUs4Q==", + "dependencies": { + "@yr/monotone-cubic-spline": "^1.0.3", + "svg.draggable.js": "^2.2.2", + "svg.easing.js": "^2.0.0", + "svg.filter.js": "^2.0.2", + "svg.pathmorphing.js": "^0.1.3", + "svg.resize.js": "^1.4.3", + "svg.select.js": "^3.0.1" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -14873,6 +14905,18 @@ "node": ">=0.10.0" } }, + "node_modules/react-apexcharts": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.4.1.tgz", + "integrity": "sha512-G14nVaD64Bnbgy8tYxkjuXEUp/7h30Q0U33xc3AwtGFijJB9nHqOt1a6eG0WBn055RgRg+NwqbKGtqPxy15d0Q==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "apexcharts": "^3.41.0", + "react": ">=0.13" + } + }, "node_modules/react-app-polyfill": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz", @@ -15028,6 +15072,25 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + }, + "node_modules/react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "dependencies": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -15192,6 +15255,14 @@ } } }, + "node_modules/react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -16745,6 +16816,89 @@ "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" }, + "node_modules/svg.draggable.js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz", + "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", + "dependencies": { + "svg.js": "^2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.easing.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz", + "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==", + "dependencies": { + "svg.js": ">=2.3.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.filter.js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz", + "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz", + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==" + }, + "node_modules/svg.pathmorphing.js": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz", + "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", + "dependencies": { + "svg.js": "^2.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz", + "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", + "dependencies": { + "svg.js": "^2.6.5", + "svg.select.js": "^2.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js/node_modules/svg.select.js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz", + "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.select.js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz", + "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", + "dependencies": { + "svg.js": "^2.6.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/svgo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", diff --git a/package.json b/package.json index 79d217c..aa82f79 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,11 @@ "@types/react": "^18.2.73", "@types/react-dom": "^18.2.22", "@types/styled-components": "^5.1.34", + "apexcharts": "^3.48.0", "react": "^18.2.0", + "react-apexcharts": "^1.4.1", "react-dom": "^18.2.0", + "react-helmet": "^6.1.0", "react-query": "^3.39.3", "react-router-dom": "5.3", "react-scripts": "5.0.1", @@ -45,6 +48,7 @@ ] }, "devDependencies": { + "@types/react-helmet": "^6.1.11", "@types/react-router-dom": "^5.3.3" } } diff --git a/src/api.ts b/src/api.ts index 21d92bc..245d5b5 100644 --- a/src/api.ts +++ b/src/api.ts @@ -15,3 +15,9 @@ export function fetchCoinTickers(coinId: string) { response.json(), ); } + +export function fetchCoinHistory(coinId: string) { + return fetch( + `https://ohlcv-api.nomadcoders.workers.dev?coinId=${coinId}`, + ).then((response) => response.json()); +} diff --git a/src/routes/Chart.tsx b/src/routes/Chart.tsx index 6b357d1..51fcc80 100644 --- a/src/routes/Chart.tsx +++ b/src/routes/Chart.tsx @@ -1,5 +1,87 @@ import React from "react"; +import { useQuery } from "react-query"; +import { fetchCoinHistory } from "../api"; +import ReactApexChart from "react-apexcharts"; -export default function Chart() { - return
Chart
; +interface ChartProps { + coinId: string; +} + +interface IHistorical { + time_open: string; + time_close: string; + open: number; + high: number; + low: number; + close: number; + volume: number; + market_cap: number; +} + +export default function Chart({ coinId }: ChartProps) { + const { isLoading, data } = useQuery( + ["ohlcv", coinId], + () => fetchCoinHistory(coinId), + { + refetchInterval: 1000000, + }, + ); + const isError = !Array.isArray(data); + return ( +
+ {isLoading ? ( + "Loading Chart" + ) : isError ? ( +

값이 존재하지 않습니다..😭

+ ) : ( + Number(price.close)) ?? [], + }, + ]} + options={{ + theme: { + mode: "dark", + }, + chart: { + height: 400, + width: 500, + toolbar: { + show: false, + }, + background: "transparent", + }, + grid: { show: false }, + stroke: { + width: 5, + }, + yaxis: { show: false }, + xaxis: { + labels: { show: false }, + axisTicks: { show: false }, + axisBorder: { show: false }, + type: "datetime", + categories: + data?.map((price) => + new Date(Number(price.time_close) * 1000).toUTCString(), + ) ?? [], + }, + fill: { + type: "gradient", + gradient: { gradientToColors: ["#0be881"], stops: [0, 100] }, + }, + colors: ["#0fbcf9"], + tooltip: { + y: { + formatter: (value) => `$ ${value.toFixed(2)}`, + }, + }, + }} + /> + )} +
+ ); } diff --git a/src/routes/Coin.tsx b/src/routes/Coin.tsx index ce77006..a6ed2a0 100644 --- a/src/routes/Coin.tsx +++ b/src/routes/Coin.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from "react"; +import { Helmet } from "react-helmet"; import { Link, Route, @@ -43,9 +44,10 @@ const Loader = styled.h1` const Overview = styled.div` display: flex; - justify-content: space-between; + justify-content: space-around; background-color: rgba(0, 0, 0, 0.5); padding: 10px 20px; + margin: 15px 0px; border-radius: 10px; `; const OverviewItem = styled.div` @@ -61,6 +63,9 @@ const OverviewItem = styled.div` `; const Description = styled.p` margin: 20px 0px; + background-color: rgba(0, 0, 0, 0.5); + padding: 10px 20px; + border-radius: 10px; `; const Tabs = styled.div` @@ -70,7 +75,7 @@ const Tabs = styled.div` gap: 10px; `; -const Tab = styled.span<{ isActive: boolean }>` +const Tab = styled.span<{ $isActive: boolean }>` text-align: center; text-transform: uppercase; font-size: 12px; @@ -79,7 +84,7 @@ const Tab = styled.span<{ isActive: boolean }>` padding: 7px 0px; border-radius: 10px; color: ${(props) => - props.isActive ? props.theme.accentColor : props.theme.textColor}; + props.$isActive ? props.theme.accentColor : props.theme.textColor}; a { display: block; } @@ -160,11 +165,23 @@ function Coin() { const { isLoading: tickersLoading, data: tickerData } = useQuery( ["tickers", coinId], () => fetchCoinTickers(coinId), + { + refetchInterval: 10000000, + }, ); - const loading = infoLoading || tickersLoading; + const loading = infoLoading && tickersLoading; return ( + + + {state?.name + ? state.name + : loading + ? "코인 로딩중..." + : infoData?.name} + +
{state?.name @@ -180,35 +197,35 @@ function Coin() { <> <Overview> <OverviewItem> - <span>Rank:</span> - <span>{infoData?.rank}</span> + <span>심볼</span> + <span>${infoData?.symbol}</span> </OverviewItem> <OverviewItem> - <span>Symbol:</span> - <span>${infoData?.symbol}</span> + <span>순위</span> + <span>{infoData?.rank}</span> </OverviewItem> <OverviewItem> - <span>Open Source:</span> - <span>{infoData?.open_source ? "Yes" : "No"}</span> + <span>가격</span> + <span>{tickerData?.quotes.USD.price.toFixed(2)}</span> </OverviewItem> </Overview> - <Description>{infoData?.description}</Description> <Overview> <OverviewItem> - <span>Total Supply:</span> + <span>총량</span> <span>{tickerData?.total_supply}</span> </OverviewItem> <OverviewItem> - <span>Max Supply:</span> + <span>최대 발행량</span> <span>{tickerData?.max_supply}</span> </OverviewItem> </Overview> + <Description>{infoData?.description}</Description> <Tabs> - <Tab isActive={chartMatch !== null}> + <Tab $isActive={chartMatch !== null}> <Link to={`/${coinId}/chart`}>Chart</Link> </Tab> - <Tab isActive={priceMatch !== null}> + <Tab $isActive={priceMatch !== null}> <Link to={`/${coinId}/price`}>Price</Link> </Tab> </Tabs> @@ -218,7 +235,7 @@ function Coin() { <Price /> </Route> <Route path={`/:coinId/chart`}> - <Chart /> + <Chart coinId={coinId} /> </Route> </Switch> </> diff --git a/src/routes/Coins.tsx b/src/routes/Coins.tsx index dfd9b91..629d2e6 100644 --- a/src/routes/Coins.tsx +++ b/src/routes/Coins.tsx @@ -3,6 +3,7 @@ import { useQueries, useQuery } from "react-query"; import { Link } from "react-router-dom"; import { styled } from "styled-components"; import { fetchCoins } from "../api"; +import { Helmet } from "react-helmet"; const Container = styled.div` padding: 0px 20px; @@ -72,6 +73,9 @@ function Coins() { const { isLoading, data } = useQuery<ICoin[]>("allCoins", fetchCoins); return ( <Container> + <Helmet> + <title>Coins +
Coins
From c9d87bf26f09d48d5ac936803f8262dd280ce6ff Mon Sep 17 00:00:00 2001 From: hyunmin200 Date: Thu, 4 Apr 2024 17:07:49 +0900 Subject: [PATCH 4/4] document --- Document/Crypto-Tracker-Part01.md | 299 ++++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 Document/Crypto-Tracker-Part01.md diff --git a/Document/Crypto-Tracker-Part01.md b/Document/Crypto-Tracker-Part01.md new file mode 100644 index 0000000..39093bc --- /dev/null +++ b/Document/Crypto-Tracker-Part01.md @@ -0,0 +1,299 @@ +API를 사용해서 간단하게 비트코인을 보여주는 사이트를 만들어보면서 지금까지 배웠던 기술들을 총합해서 사용해보는 간단 프로젝트 + +- React.js +- Styled-Component +- TypeScript +- React-Router +- React-Query +- React-Helmet + +# 알게 된 점 + +- 현재 TypeScript를 사용하고 있기때문에 아래 구문을 하면 오류가 나게 될 것이다. + +```JS +import { BrowserRouter } from "react-router-dom"; +``` + +- 그럴때는 `npm i --save-dev @types/react-router-dom`이런식으로 types를 다운받아주면 된다. +- 아래 처럼 작성하게 된다면 URL에 변수값을 가진다는 것 + +```JS + + + +``` + +- url 파라미터를 가져오고 싶을 때는 useParams()라는 ReactHook을 사용해준다. +- `const { coinId } = useParams();`이런식으로 작성을 하면 오류가 나는데, 이럴때는 TypeScript가 useParams가 비어있다고 인식해서 그렇다. +- 해결 방법은 두 가지가 존재한다. interface를 사용하든가? 그냥 타입지정을 해주든가. + 이 경우에서는 값이 한 가지만 존재하기에 나는 그냥 타입지정을 해주었다. + `const { coinId } = useParams<{ coinId: string }>();` +- 우리가 여러 개의 element를 사용해야 할 때에는 fragment를 사용할 수 있다. + +```JS +<> + + + +``` + +- createGlobalStyle (전역 스타일을 처리함) + 전역 스타일을 처리하는 특수 Styled Component를 생성하는 helper 함수이다. + +```JS +const GlobalStyle = createGlobalStyle` //css코드 `; +``` + +- a href를 사용하면 페이지의 새로고침이 일어난다. +- 그래서 새로고침을 안하기 위해서는 react-router-dom에 있는 Link를 사용한다. +- Link는 결국 a로 바뀌기 때문에 css에서는 a로 쓰면 된다. +- 아래 코드처럼 사용하면 함수를 바로 실행시킬 수 있다. (즉시 실행 함수) + +```JS +(() => console.log("1"))() +``` + +- API가져올 때 아래처럼 fetch를 사용해서 가져왔는데.. + +```JS +useEffect(() => { +    (async () => { +      const response = await fetch("https://api.coinpaprika.com/v1/coins"); +      const json = await response.json(); +      setCoins(json.slice(0, 100)); +      setLoading(false); +    })(); +  }, []); +``` + +- axios를 사용하면 기본이 json이기에 좀 더 편하게 가져올 수 있다. + +```JS +const getCoins = async() =>{ + const res = await axios("https://api.coinpaprika.com/v1/coins"); + setCoins(res.data.slice(0, 100)); + setLoading(false); +}; +``` + +- Link를 사용할 state를 사용해준다면 데이터를 넘길 수 있다. + +```JS + +``` + +- state값을 가져오기 위해서는 react-router-dom의 locationObj에 접근하면 된다. + +```JS +const location = useLocation(); +``` + +```JS +const { state } = useLocation(); +``` + +- 이런식으로 state를 가져와서 사용할 수 있다. +- 이렇게 가져와주면 웹이 좀 빠른것처럼 만들 수 있다. +- 하지만 이런식으로 하면 home에서 클릭할 때 name을 넘겨주는 것이기 때문에 링크를 복붙하면 에러가 나게된다. +- 그럴 때는 아래처럼 해주면 된다. + +```JS +{state?.name || "이름 로딩중"} +``` + +- 이런식으로 대처해줄 수 있다! +- API불러올 때 한 줄로 만들고 싶다? 라고 하면 아래 처럼 써보자 + +```JS +const response = await ( +        await fetch(`https://api.coinpaprika.com/v1/coins/${coinId}`) +      ).json(); +``` + +- 별로 안이쁘지만 시간 절약은 된다. (react-query배우면 지린다고 함) + +## nested route + +```JS + + + + + + + + +``` + +이런식으로 Route안에 Route를 추가해서 탭마다 하나만 보이게 만들 수도 있다. + +## Props를 사용할 때 $를 붙이는 이유 + +React18 이후 일관성을 높이고, 사용자 혼동을 방지하기 위해 prop의 이름은 소문자나 앞에 $가 있어야만 사용자 지정 속성으로 인식한다. $가 추가된 이유는 일부 라이브러리 또는 구성 요소는 대문자를 사용하는 prop을 요구하기 때문이다. + +### useRouteMatch + +- 특정한 url에 있는지 여부를 알려주는 hook이다. + +```JS +const priceMatch = useRouteMatch("/:coinId/price") +``` + +- 이런식으로 작성해서 콘솔에 출력을 해보면 만약에 url이 맞다면 object하나를 넘겨주고 아니라면 null이 뜬다. + +## ReactQuery + +```JS +const [coins, setCoins] = useState([]); +const [loading, setLoading] = useState(true); +  useEffect(() => { +    (async () => { +      const response = await fetch("https://api.coinpaprika.com/v1/coins"); +      const json = await response.json(); +      setCoins(json.slice(0, 100)); +      setLoading(false); +    })(); +}, []); +``` + +만약에 이 코드를 React-Query를 사용하게 되면 어떻게 될까? + +일단 fetcher라는 함수를 만들어줘야 한다. +fetcher는 api.ts라는 파일을 하나 만들어서 관리하게 된다. + +> fetcher함수는 fetch를 하는 함수라고 보면 된다. + +```JS +const response = await fetch("https://api.coinpaprika.com/v1/coins"); +const json = await response.json(); +``` + +이 부분을 때와서 + +```JS +export async function fetchCoins() { +  return await fetch("https://api.coinpaprika.com/v1/coins").then((response) => +    response.json(), +  ); +} +``` + +만들어주고 + +그 다음부터 useQuery라는 hook을 사용하면 되는데 useQuery는 2가지 인자가 필요하다. + +- queryKey + - query의 고유 식별자 +- fetcher 함수 + 그리고 useQuery가 return하는 것을 받아오면 된다. + +잠깐 정리해보면 + +> useQuery가 fetcher함수를 불러주고 isLoading값에 로딩중인지 알려주고 data에 fetcher에서 return한 json을 넣어준다! + +그래서 그 긴거를.. + +```JS +const { isLoading, data } = useQuery("allCoins", fetchCoins); +``` + +단 한 줄로 해결할 수 있다.. + +심지어 React-Query를 사용하면 가져온 데이터를 캐싱해두기 때문이다. +그래서 코인을 누르고 뒤로가기를 눌러도 loading이 뜨지 않는다! + +심지어 많은 state들도 사라지기에 사용할 때 너무 편하다! + +또한 캐싱된 데이터를 보기 위해서 + +```JS + +``` + +를 사용하여 시각화해 확인할 수도 있다. + +ReactQuery는 Key를 Array로 받기 때문에 key을 줄 때 중복이 된다면, + +```JS +const {} = useQuery(["info", coinId], () => fetchCoinInfo(coinId)); +const {} = useQuery(["tickers", coinId], () => fetchCoinTickers(coinId)); +``` + +이런식으로 Array로 Key를 만들어주면 고유한 값을 가지게 할 수 있다. + +이제 우리가 useQuery에서 하나씩 뽑아 먹을 때, 여러 개의 쿼리를 날리면 프로퍼티 이름이 겹칠 것이다. +그렇때는 아래처럼 이름을 지정해서 사용해주면 된다. + +```JS +const { isLoading: infoLoading, data: infoData } = useQuery( + ["info", coinId], + () => fetchCoinInfo(coinId), +); + +const { isLoading: tickersLoading, data: tickerData } = useQuery( + ["tickers", coinId], + () => fetchCoinTickers(coinId), +); +``` + +또한 이놈들도 infoData의 type과 tickerData의 type을 모르기에 타입 지정을 해줘야한다. + +```JS +const { isLoading: infoLoading, data: infoData } = useQuery( + ["info", coinId], + () => fetchCoinInfo(coinId), +); + +const { isLoading: tickersLoading, data: tickerData } = useQuery( + ["tickers", coinId], + () => fetchCoinTickers(coinId), +); +``` + +![[Pasted image 20240404080950.png]] +그러면 이런식으로 캐시에 들어가게 된다. + +```JS +const [loading, setLoading] = useState(true); +const [info, setInfo] = useState(); +const [priceInfo, setPriceInfo] = useState(); +useEffect(() => { + (async () => { + const infoData = await ( + await fetch(`https://api.coinpaprika.com/v1/coins/${coinId}`) + ).json(); + console.log(infoData); + setInfo(infoData); + const priceData = await ( + await fetch(`https://api.coinpaprika.com/v1/tickers/${coinId}`) + ).json(); + setPriceInfo(priceData); + setLoading(false); + })(); +}, []); +``` + +그래서 결론을 보면 위에서 아래로 바뀐 것을 볼 수 있다! + +```JS +const { isLoading: infoLoading, data: infoData } = useQuery( + ["info", coinId], + () => fetchCoinInfo(coinId), +); +const { isLoading: tickersLoading, data: tickerData } = useQuery( + ["tickers", coinId], + () => fetchCoinTickers(coinId), +); + +const loading = infoLoading || tickersLoading; +``` + +## React-Helmet + +React-Helmet이라는 것을 사용해서 head를 넣을 수 있다.