이번에 내가 일주일동안 만든 미니프로젝트인 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 : 거래량