react - CoinTracker

장산·2022년 9월 23일
0

React

목록 보기
10/12

이번에 내가 일주일동안 만든 미니프로젝트인 CoinTracker에 대해 설명을 해보았다.

coins.tsx

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를 작성하는 라이브러리.

style-component 특징

  • 각 js파일마다 고유한 css네임을 부여해주기 때문에, 각 react 컴포넌트에 완전히 격리된 스타일을 적용할 수 있다.

axios

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

react-helmet 사용법

npm i react-helmet
import { Helmet } from "react-helmet"

  • 헤더값을 프롭스로 전달하는 뿐만 아니라 자식 컴포넌트로 설정하는 방법도 있다.

react-helmet 필요한 이유

  • 문서 타이틀을 변경할 때 : SPA는 화면을 이동할 때마다 페이지를 요청하는게 아니기 때문에 문서의 타이틀은 맨 처음 서버에서 받은 값을 사용한다.
  • 소설 서비스에 포스팅할 때 : 공유할 링크의 og태그값을 가져와 각 플랫폼에 맞는 컨텐츠를 작성한다. og태그는 헤더 안에 위치하는 메타 태그로 작성한다.
  • 검색엔진 최적화가 필요할 때 : 검색 결과애 날짜를 표시하기 위해서 몇가지 정보를 헤더에 담는데 이러한 메타 정보를 수집하여 검색 결과에 보여주는데 SEO 관점에서 보더라도 헤더값 관리가 필요하다.

사용한 React hooks

  • useEffect: react에게 컴포넌트가 렌더링 이후에 어떤일을 수행해야 할지 알려주는 hooks. 즉 컴포넌트의 사이드 이펙트 관리를 위한 hook.
  • useState: 상태관리를 위한 hooks.

coin.tsx

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:사용자가 입력한 주소를 감지하는 역할을 하며, 여러 환경에서 동작할 수 있도록 여러 종류의 라우터 컴포넌트를 제공.

react-query의 특징

  • get한 데이터를 업데이트하면 자동으로 다시 get.
  • 데이터가 오래되었으면 다시 되었으면 다시 get(invalidateQueries)
  • 같은 데이터를 여러번 요청하면 한번만 요청.
  • 무한스크롤(인피니티스크롤)
  • 비동기 과정을 선언적으로 관리할 수 있다.

사용한 react hooks

  • useParams : 동적으로 라우팅을 생성하기 위해 사용한다.
  • useLocation : 사용자가 현재 머물러있는 페이지에 대한 정보를 알려주는 hooks.
  • useRouteMatch: match 객체의 값에 접근할 수 있게 해주는 hooks.(router 버전이 업그레이드 되면서 useMatch로 바뀜)
  • useQuery : react-query를 이용해 서버로부터 데이터를 가져올 때 사용.

Chart.tsx

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 : 데이터를 시가화 해주는 차트 라이브러리.

Coinpaprika api

몇몇 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 : 거래량
profile
신입 개발자

0개의 댓글