이번에 내가 일주일동안 만든 미니프로젝트인 CoinTracker에 대해 설명을 해보았다.
import styled from "styled-components";
import { Link } from "react-router-dom";
import { Helmet } from "react-helmet";
import { useEffect, useState } from "react";
import axios from "axios";
const Container = styled.div`
padding: 0px 2px;
max-width: 800px;
margin: 0 auto;
`;
const Head = styled.div`
display: flex;
justify-content: space-around;
border: 1px solid white;
border-radius: 15px;
background-color: ${(props) => props.theme.cardBgColor};
color: ${(props) => props.theme.textColor};
margin-bottom: 15px;
margin-top: 10px;
height: 30px;
align-items: center;
`;
const Header = styled.header`
height: 50px;
display: flex;
justify-content: center;
align-items: center;
`;
const CoinList = styled.ul``;
const Coin = styled.li`
background-color: ${(props) => props.theme.cardBgColor};
color: ${(props) => props.theme.textColor};
border-radius: 15px;
margin-bottom: 10px;
border: 1px solid white;
a {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
transition: color 0.2s ease-in;
}
span{
margin-right: 2rem;
}
&:hover {
a {
color: ${(props) => props.theme.accentColor};
}
}
`;
const Title = styled.h1`
font-size: 48px;
color: ${(props) => props.theme.accentColor};
`;
const Loader = styled.span`
text-align: center;
display: block;
`;
const Img = styled.img`
width: 35px;
height: 35px;
`;
interface ICoin {
id: string;
name: string;
symbol: string;
rank: number;
is_new: boolean;
is_active: boolean;
type: string;
}
interface ICoinsProps {
toggleDark: () => void;
}
function Coins({ toggleDark }: ICoinsProps) {
const [ticker, setTicker] : any= useState<any>([]);
useEffect(() => {
const getCoin = () => {
axios.get(`https://api.coinpaprika.com/v1/tickers?quotes=KRW`).then((res:any) => {
setTicker(res.data.slice(0,100));
});
};
getCoin();
}, []);
return (
<>
<button onClick={toggleDark} style={{ margin:20}}>Toggle Dark Mode</button>
<Container>
<Helmet>
<title>KeyPair</title>
</Helmet>
<Header>
<Title>KeyPair</Title>
</Header>
<Head>
<div className="name">이름</div>
<div className="price">현재가</div>
<div className="percent"> 변동률</div>
<div className="volum">거래량</div>
<div className="cap">총시가</div>
</Head>
<CoinList>
{ticker?.map((coin:any) => (
<Coin key={coin.id}>
<Link to={{
pathname: `/${coin.id}`,
state: { name: coin.name },
}}
>
<Img src={`https://coinicons-api.vercel.app/api/icon/${coin.symbol.toLowerCase()}`} style={{marginRight:20}}/>
<span>{coin.name}</span>
<span>{Number(coin.quotes.KRW.price.toFixed(1)).toLocaleString()}원<div style={{fontSize:5}}>({coin?.last_updated.slice(0,10)})</div><div style={{fontSize:5}}>last update</div></span>
<span>{coin.quotes.KRW.percent_change_24h}%</span>
<span>{Number((coin.quotes.KRW.volume_24h / 1000000000000).toFixed(1)).toLocaleString()}조</span>
<span>{Number((coin.quotes.KRW.market_cap / 1000000000000).toFixed(1)).toLocaleString()}조</span>
</Link>
</Coin>
))}
</CoinList>
</Container>
</>
);
}
export default Coins;
- react : 사용자 인터페이스를 구축하기 위한 선언적이고 효율적이며 유연한 JavaScript 라이브러리이다.
- react-helmet : 웹사이트 타이틀을 동적으로 변경할 수 있게 해주는 라이브러리.
- axios : 브라우저, node.js를 위한 promise Api를 활용하는 http 비동기 통신 라이브러리.
- style-component : js 안에 css를 작성하는 라이브러리.
- 각 js파일마다 고유한 css네임을 부여해주기 때문에, 각 react 컴포넌트에 완전히 격리된 스타일을 적용할 수 있다.
axios의 특징
- 운영 환경에 따라 브라우저의 XMLFttpRequest 객체 또는 Node.js의 HTTP API사용
- Promise(ES6)API사용
- 요청과 응답 데이터의 변형
- HTTP 요청 취소 및 요청과 응답을 JSON 형태로 자동변경
axios 사용법
yarn add axios
import axios from "axios";
- axios.get: 데이터 조회
- axios.post: 데이터 등록
- axios.put: 데이터 수정
- axios.delete: 데이터 제거
react-helmet 사용법
npm i react-helmet
import { Helmet } from "react-helmet"
- 헤더값을 프롭스로 전달하는 뿐만 아니라 자식 컴포넌트로 설정하는 방법도 있다.
react-helmet 필요한 이유
- 문서 타이틀을 변경할 때 : SPA는 화면을 이동할 때마다 페이지를 요청하는게 아니기 때문에 문서의 타이틀은 맨 처음 서버에서 받은 값을 사용한다.
- 소설 서비스에 포스팅할 때 : 공유할 링크의 og태그값을 가져와 각 플랫폼에 맞는 컨텐츠를 작성한다. og태그는 헤더 안에 위치하는 메타 태그로 작성한다.
- 검색엔진 최적화가 필요할 때 : 검색 결과애 날짜를 표시하기 위해서 몇가지 정보를 헤더에 담는데 이러한 메타 정보를 수집하여 검색 결과에 보여주는데 SEO 관점에서 보더라도 헤더값 관리가 필요하다.
import {Switch, Route, useLocation, useParams, useRouteMatch, Link} from "react-router-dom";
import styled from "styled-components";
import { useQuery } from "react-query";
import Chart from "./Chart";
import { fetchCoinInfo,fetchCoinTickers } from "../api";
import { Helmet } from "react-helmet";
import Info from "./Info";
const Title = styled.h1`
font-size: 48px;
color: black;
`;
const Loader = styled.span`
text-align: center;
display: block;
`;
const Container = styled.div`
padding: 0px 20px;
max-width: 480px;
margin: 0 auto;
`;
const Header = styled.header`
height: 15vh;
display: flex;
justify-content: center;
align-items: center;
`;
const Overview = styled.div`
display: flex;
justify-content: space-between;
background-color: black;
padding: 10px 20px;
border-radius: 10px;
margin-bottom: 10px;
`;
const OverviewItem = styled.div`
display: flex;
flex-direction: column;
align-items: center;
width: 33%;
color: white;
span:first-child {
font-size: 10px;
font-weight: 400;
text-transform: uppercase;
margin-bottom: 5px;
}
`;
const Description = styled.p`
margin: 20px 0px;
`;
const Tabs = styled.div`
display: grid;
grid-template-columns: repeat(2, 1fr);
margin: 25px 0px;
gap: 10px;
` ;
const Tab = styled.span<{ isActive: boolean }> `
text-align: center;
text-transform: uppercase;
font-size: 12px;
font-weight: 400;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 10px;
color: ${(props) =>
props.isActive ? props.theme.accentColor : props.theme.textColor};
a {
padding: 7px 0px;
display: block;
}
` ;
interface RouteParams {
coinId: string;
}
interface RouteState {
name: string;
}
interface InfoData {
id: string;
name: string;
symbol: string;
rank: number;
is_new: boolean;
is_active: boolean;
type: string;
description: string;
message: string;
open_source: boolean;
started_at: string;
development_status: string;
hardware_wallet: boolean;
proof_type: string;
org_structure: string;
hash_algorithm: string;
first_data_at: string;
last_data_at: string;
}
interface PriceData {
id: string;
name: string;
symbol: string;
rank: number;
circulating_supply: number;
total_supply: number;
max_supply: number;
beta_value: number;
first_data_at: string;
last_updated: string;
quotes: {
USD: {
ath_date: string;
ath_price: number;
market_cap: number;
market_cap_change_24h: number;
percent_change_1h: number;
percent_change_1y: number;
percent_change_6h: number;
percent_change_7d: number;
percent_change_12h: number;
percent_change_15m: number;
percent_change_24h: number;
percent_change_30d: number;
percent_change_30m: number;
percent_from_price_ath: number;
price: number;
volume_24h: number;
volume_24h_change_24h: number;
};
};
}
interface ICoinProps {
isDark: boolean;
}
function Coin({ isDark }: ICoinProps) {
const { coinId } = useParams<RouteParams>();
const { state } = useLocation<RouteState>();
const infoMatch = useRouteMatch("/:coinId/info");
const chartMatch = useRouteMatch("/:coinId/chart");
const { isLoading: infoLoading, data: infoData } = useQuery<InfoData>(
["info", coinId],
() => fetchCoinInfo(coinId)
);
const { isLoading: tickersLoading, data: tickersData } = useQuery<PriceData>(
["tickers", coinId],
() => fetchCoinTickers(coinId)
);
const loading = infoLoading || tickersLoading;
console.log(tickersData)
/* coinpaprika api
id : 코인아이디
name : 코인 종목
symbol : 기호
rank : 순위
circulating_supply : 현재까지 유통량
total_supply : 총 유통량
max_supply : 최대 발행량
last_update : 마지막 업데이트
quotes: {
KRW : { 원화 기준
price : 현재 시세
volume_24h : 지난 24시간 거래량
volume_24h_change_24h : 지난 24시간 거래 변동률
market_cap : 시총
market_cap_change_24h : 시총 가격 변동률
percent_change_15m : 마지막 업데이트 기준 변동률
percent_change_30m :
percent_change_1h :
percent_change_6h :
percent_change_12h :
percent_change_24h :
percent_change_7d :
percent_change_30d :
percent_change_1y :
ath_price : 사상 최고 가격
ath_date : 사상 최고 가격을 찍은 날짜
percent_from_price_ath:
}
}*/
return (
<>
<Link to={"/"}>
<button style={{margin:15}}>뒤로가기</button>
</Link>
<Container>
<Helmet>
<title>
{state?.name ? state.name : loading ? "Loading..." : infoData?.name}
</title>
</Helmet>
<Header>
<Title>{state?.name ? state.name : loading ? "Loading...": infoData?.name}</Title>
</Header>
{loading ? <Loader>Loading...</Loader> : (
<>
<Overview>
<OverviewItem>
<span>Rank:</span>
<span>{infoData?.rank}</span>
</OverviewItem>
<OverviewItem>
<span>Symbol:</span>
<span>${infoData?.symbol}</span>
</OverviewItem>
<OverviewItem>
<span>Price:</span>
<span>${tickersData?.quotes?.USD?.price?.toFixed(3)}</span>
</OverviewItem>
</Overview>
<Overview>
<OverviewItem>
<span>Total Suply:</span>
<span>{tickersData?.total_supply}</span>
</OverviewItem>
<OverviewItem>
<span>Max Supply:</span>
<span>{tickersData?.max_supply}</span>
</OverviewItem>
</Overview>
<Tabs>
<Tab isActive={chartMatch !== null}>
<Link to={`/${coinId}/chart`}>Chart</Link>
</Tab>
<Tab isActive={infoMatch !== null}>
<Link to={`/${coinId}/info`}>Information</Link>
</Tab>
</Tabs>
<Switch>
<Route path={`/:coinId/info`}>
<Info isDark={isDark} coinId={coinId} />
</Route>
<Route path={`/:coinId/chart`}>
<Chart isDark={isDark} coinId={coinId}/>
</Route>
</Switch>
</>
)}
</Container>
</>
);
}
export default Coin;
- react-query: 서버의 값을 클라이언트에 가져오거나,캐싱,값 업데이트,에러핸들링 등 비동기 과정을 더욱 편하게 하는데 사용
- react-router:사용자가 입력한 주소를 감지하는 역할을 하며, 여러 환경에서 동작할 수 있도록 여러 종류의 라우터 컴포넌트를 제공.
- get한 데이터를 업데이트하면 자동으로 다시 get.
- 데이터가 오래되었으면 다시 되었으면 다시 get(invalidateQueries)
- 같은 데이터를 여러번 요청하면 한번만 요청.
- 무한스크롤(인피니티스크롤)
- 비동기 과정을 선언적으로 관리할 수 있다.
- useParams : 동적으로 라우팅을 생성하기 위해 사용한다.
- useLocation : 사용자가 현재 머물러있는 페이지에 대한 정보를 알려주는 hooks.
- useRouteMatch: match 객체의 값에 접근할 수 있게 해주는 hooks.(router 버전이 업그레이드 되면서 useMatch로 바뀜)
- useQuery : react-query를 이용해 서버로부터 데이터를 가져올 때 사용.
import { useQuery } from "react-query";
import { fetchCoinHistory } from "../api";
import ApexChart from "react-apexcharts";
interface IHistorical {
time_open: string;
time_close: string;
open: number;
high: number;
low: number;
close: number;
volume: number;
market_cap: number;
}
interface ChartProps {
coinId: string;
isDark: boolean;
}
function Chart({ coinId,isDark }: ChartProps) {
const { isLoading, data } = useQuery<IHistorical[]>(["ohlcv",coinId], () =>
fetchCoinHistory(coinId)
// {
// refetchInterval: 10000,
// }
// 10초마다 refetch 시킨다
);
return(
<div>
{isLoading ? (
"Loading chart..."
) : (
<ApexChart
type="line"
series={[
{
name:"시초가",
data: data?.map((price: any) => (
parseFloat(price.open)
))??[],
},
{
name:"종가",
data: data?.map((price: any) => (
parseFloat(price.close)
))??[],
},
{
name: "고가",
data: data?.map((price: any) => (
parseFloat(price.high)
))??[],
}
]}
options={{
theme: {
mode: isDark ? "dark" : "light",
},
chart: {
height: 100,
width: 600,
toolbar: {
show: true,
},
background: "transparent",
},
grid: { show: false },
stroke: {
curve: "smooth",
width: 4,
},
yaxis: {
show: true,
},
xaxis: {
axisBorder: { show: true },
axisTicks: { show: true },
labels: { show: true,datetimeFormatter: {month: "mmm 'yy"} },
type: "datetime",
categories: data?.map((price:any) => new Date(price.time_close * 1000).toISOString())
/*toISOString은 timestamp를 isostring 형식의 문자열로 변환해주는 함수*/
/* 이 함수를 이용하면 하루 전 날짜가 찍히는데 이유는 우리나라 타임존이 아니라 UTC타임좀을 사용하기 떄문*/
},
fill: {
type: "gradient",
gradient: { gradientToColors: ["#0be881"], stops: [0, 100]},
},
colors: ["#0fbcf9","#8b0000","#00FF00"],
tooltip: {
y: {
formatter: (value) => `$${value.toFixed(2)}`,
}
}
}}
/>
)}
<div className="chart-info" style={{textAlign:"center"}}>
<h1> Chart Information</h1>
<div>1.last Update부터 21일치 전까지 기록</div>
<div>2.단위는 달러($)</div>
</div>
</div>
)
}
export default Chart;
- rect-apexcharts : 데이터를 시가화 해주는 차트 라이브러리.
몇몇 api 빼고는 타입만 설명이 되어 있어서 설명이 되어있는 api하고 내가 추측해서 작성해보았다
CoinPaprica APi
- CofinTicker api
[ { "id": "코인아이디", "name": "코인 종목", "symbol": "코인 닉네임", "rank": 1, "circulating_supply": 현재까지 유통량, "total_supply": 총 유통량, "max_supply": 최대 발행량, "beta_value": 0.735327, "first_data_at": "2010-11-14T07:20:41Z", "last_updated": "마지막 업데이트", "quotes": { "KRW": { "price": //현재시세, "volume_24h": 지난 24시간 거래량, "volume_24h_change_24h": 지난 24시간 거래 변동률, "market_cap": 시총, "market_cap_change_24h": 시총 가격 변동률, "percent_change_15m": //마지막 업데이트 기준 변동률 "percent_change_30m": "percent_change_1h": "percent_change_6h": "percent_change_12h": "percent_change_24h": "percent_change_7d": "percent_change_30d": "percent_change_1y": "ath_price": //사상 최고가격 "ath_date": // 사상 최고가격을 찍은 날짜 "percent_from_price_ath": }, } } ]
- CoinInfo.api
{ "id": "코인id", "name": "코인이름 "symbol": "코인 닉네임 "rank": 1, "is_new": false, //코인이 지난 5일 이내에 추가되어쓴지 여부를 나타내는 플래그 "is_active": true, //코인 활성상태인지 나타내는 플래그 "type": "coin", //암호화폐 "logo": "https://static.coinpaprika.com/coin/bnb-binance-coin/logo.png", "tags": [ //coinpaprika에서 이코인이 할당된 태그의 배열 { "id": "blockchain-service", "name": "Blockchain Service", "coin_counter": 160, "ico_counter": 80 } ], "team": [ //암호 화폐 창립 및 또는 개발팀 { "id": "vitalik-buterin", "name": "Vitalik Buterin", "position": "Author" } ], "description": "Bitcoin is a cryptocurrency and worldwide payment system. It is the first decentralized digital currency, as the system works without a central bank or single administrator.",//암호화폐애 대한 설명 "message": "string", //암호화폐 현황에 대한 중요한 메시지 "open_source": true, //암호화폐가 오픈소스 프로젝트인 경우 true설정 "hardware_wallet": true, //암호화페가 하드웨어 지갑에서 지원되는 경우 true 설정 "started_at": "2009-01-03T00:00:00Z", //암호화폐 출시일 "development_status": "Working product",//암호화폐 개발 현황 "proof_type": "Proof of work", //암호화폐 증명 유형 "org_structure": "Decentralized", //암호화폐 조직 구조 "hash_algorithm": "SHA256", //암호화폐에 사용하는 해시 알고리즘의 이름 "contracts": [ { "contract": "string", "platform": "string", "type": "string" } ], "links": { "explorer": [ "http://blockchain.com/explorer", "https://blockchair.com/bitcoin/blocks", "https://blockexplorer.com/", "https://live.blockcypher.com/btc/" ], "facebook": [ "https://www.facebook.com/bitcoins/" ], "reddit": [ "https://www.reddit.com/r/bitcoin" ], "source_code": [ "https://github.com/bitcoin/bitcoin" ], "website": [ "https://bitcoin.org/" ], "youtube": [ "https://www.youtube.com/watch?v=Um63OQz3bjo" ], "medium": null }, "links_extended": [ { "url": "http://blockchain.com/explorer", "type": "explorer" }, { "url": "https://www.reddit.com/r/bitcoin", "type": "reddit", "stats": { "subscribers": 1009135 } }, { "url": "https://github.com/bitcoin/bitcoin", "type": "source_code", "stats": { "contributors": 730, "stars": 36613 } }, { "url": "https://bitcoin.org/", "type": "website" } ], "whitepaper": { "link": "https://static.coinpaprika.com/storage/cdn/whitepapers/215.pdf", "thumbnail": "https://static.coinpaprika.com/storage/cdn/whitepapers/217.jpg" }, "first_data_at": "2018-10-03T11:48:19Z", //코인에 대해 사용 가능한 첫번쨰 시세 데이터의 날짜 "last_data_at": "2019-05-03T11:00:00" //코인에 대해 사용 가능한 마지막 시세 데이터의 날짜 }
- CoinHistory.api
- close : 종가
- high : 고가
- low : 저가
- marcket_cap: 시총
- open : 시초가
- time_close : 마지막 업데이트인 날짜 까지?
- time_open :
- volum : 거래량