필요한 데이터를 다 받아 왔으니 Screen Paint 하기
Nested router는 route 안에 있는 또 다른 route 이다.
nested route는 tab을 이용할 때 많은 도움을 준다.
price 나 chart 탭을 선택하면 네이게이션 바 처럼 state나 URL을 활용해서 접속하도록 돕는다.
- Price 와 Chart라는 파일을 만든다
- Switch 와 Route를 한번 더 사용한다.
route 안에 또 다른 route를 넣은 것을 알 수 있다.
다음 시간에는 tab을 만드는 것을 알아보자 !
import { createGlobalStyle } from 'styled-components';
import Router from './Router';
export default function App() {
const GlobalStyle = createGlobalStyle`
@import url('https://fonts.googleapis.com/css2?family=Archivo+Narrow:wght@500&family=Bebas+Neue&family=Black+Han+Sans&family=Do+Hyeon&family=Source+Sans+Pro:wght@300;400&family=Ubuntu+Mono:ital@1&display=swap');
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, menu, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
main, menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, main, menu, nav, section {
display: block;
}
/* HTML5 hidden-attribute fix for newer browsers */
*[hidden] {
display: none;
}
body {
line-height: 1;
}
menu, ol, ul, li {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
*{
box-sizing: border-box;
}
body{
font-family: 'Source Sans Pro', sans-serif;
//현재 App은 Theme안에 있으므로 Theme의 props에 접근 가능한 상태이다.
//그 말은 즉슨 이렇게 쓸 수 있다는 뜻이다
background-color: ${(props) => props.theme.bgColor};
color : ${(props) => props.theme.textColor};
}
a{
text-decoration: none;
color:inherit;
// 링크가 클릭되었을 때 너무 못생겨져서 부모로부터 상속받게 하여
// 색깔을 유지시켰다
}
`;
return (
<>
<GlobalStyle />
{/* 이것이 reset이고 기본값을 제거하는 방법이다. */}
<Router />
</>
);
}
import { useEffect, useState } from 'react';
import { Switch, Route, useLocation, useParams } from 'react-router';
import styled from 'styled-components';
import Chart from './Chart';
import Price from './Price';
//useParams 는 URL에서 관심있어 하는 정보를 잡아낼 수 있게 해준다.
function Coin() {
const [loading, setLoading] = useState(true);
//state가 coins으로 이루어진 array라는 것을 알려주기 위해<interface>작성
//Coins 에서 보내는 state 를 사용하기 위해 useLocation 이라는 hook을 사용해서 받아와보자! react-router-dom이 보내주는
// location object에 접근하기만 하면 된다.
const { coinId } = useParams<RouteParams>();
const { state } = useLocation<RouteState>();
useEffect(() => {
(async () => {
const infoData = await (await fetch(`https://api.coinpaprika.com/v1/coins/${coinId}`)).json();
const priceData = await (
await fetch(`https://api.coinpaprika.com/v1/tickers/${coinId}`)
).json();
setInfo(infoData);
setPriceInfo(priceData);
setLoading(false);
//loading 상태를 false로 만드는 것 잊지 말아라
})();
}, [coinId]);
//[] 안에 coinId를 넣어주면 더 좋다
const [info, setInfo] = useState<InfoData>();
const [priceInfo, setPriceInfo] = useState<PriceData>();
return (
<Container>
<Header>
<Title>{state?.name ? state.name : loading ? 'Loading...' : info?.name}</Title>
</Header>
{loading ? (
<Loader>Loading...</Loader>
) : (
<>
<Overview>
<OverviewItem>
<span>Rank:</span>
<span>{info?.rank}</span>
</OverviewItem>
<OverviewItem>
<span>Symbol:</span>
<span>${info?.symbol}</span>
</OverviewItem>
<OverviewItem>
<span>Open Source:</span>
<span>{info?.open_source ? 'Yes' : 'No'}</span>
</OverviewItem>
</Overview>
<Description>{info?.description}</Description>
<Overview>
<OverviewItem>
<span>Total Suply:</span>
<span>{priceInfo?.total_supply}</span>
</OverviewItem>
<OverviewItem>
<span>Max Supply:</span>
<span>{priceInfo?.max_supply}</span>
</OverviewItem>
</Overview>
<Switch>
<Route path={`/${coinId}/price`}>
<Price />
</Route>
<Route path={`/${coinId}/chart`}>
<Chart />
</Route>
</Switch>
</>
)}
</Container>
);
}
interface RouteState {
name: string;
}
interface RouteParams {
coinId: 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;
};
};
}
const Container = styled.div`
padding: 0px 20px;
max-width: 480px;
margin: 0 auto;
`;
const Header = styled.header`
height: 20vh;
display: flex;
justify-content: center;
align-items: center;
`;
const Title = styled.h1`
font-size: 50px;
color: ${(props) => props.theme.accentColor};
`;
const Loader = styled.span`
display: block;
text-align: center;
`;
const Overview = styled.div`
display: flex;
justify-content: space-between;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px 20px;
border-radius: 10px;
`;
const OverviewItem = styled.div`
display: flex;
flex-direction: column;
align-items: center;
span:first-child {
font-size: 10px;
font-weight: 400;
text-transform: uppercase;
margin-bottom: 5px;
}
`;
const Description = styled.p`
margin: 20px 0px;
`;
export default Coin;
import styled from 'styled-components';
import { Link } from 'react-router-dom';
import { useEffect, useState } from 'react';
const Container = styled.div`
padding: 0px 20px;
max-width: 480px;
margin: 0 auto;
`;
const Header = styled.header`
height: 20vh;
display: flex;
justify-content: center;
align-items: center;
`;
const CoinsList = styled.ul``;
const Coin = styled.li`
background-color: white;
color: ${(props) => props.theme.bgColor};
margin-bottom: 10px;
padding: 20px;
border-radius: 15px;
a {
padding: 5px; // 좀 더 넓은 범위에서 transition 효과 적용 가능
transition: color 0.2s ease-in;
}
&:hover {
a {
color: ${(props) => props.theme.accentColor};
}
// 아래에서는 a가 아닌 Link라는 이름으로 사용했지만
// css에서는 anchor 를 선택해야 했다. 이건 모든 react router link들이
// 결국에는 anchor로 바뀔거기도 하고,
// react router dom이 우리 대신 설정을 도와줄 특별한 event listener들이 있기도 하다
}
`;
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: 25px;
height: 25px;
margin-right: 10px;
`;
interface CoinInterface {
id: string;
name: string;
symbol: string;
rank: number;
is_new: boolean;
is_active: boolean;
type: string;
}
function Coins() {
//state를 이용해 coins를 만들어 준다
const [coins, setCoins] = useState<CoinInterface[]>([]);
const [loading, setLoading] = useState(true);
//state가 coins으로 이루어진 array라는 것을 알려주기 위해<interface>작성
useEffect(() => {
(async () => {
const response = await fetch('https://api.coinpaprika.com/v1/coins');
const json = await response.json();
setCoins(json.slice(0, 100));
setLoading(false);
// loading 상태 수정하기
})();
}, []);
return (
<Container>
<Header>
<Title>코인</Title>
</Header>
{loading ? (
<Loader>"Loading..."</Loader>
) : (
//loading 이 참이면 Loading... 출력, 거짓이면 CoinsList 보여줌
<CoinsList>
{coins.map((coin) => (
<Coin key={coin.id}>
<Img
src={`https://cryptoicon-api.vercel.app/api/icon/${coin.symbol.toLowerCase()}`}
/>
<Link
to={{
pathname: `/${coin.id}`,
state: { name: coin.name },
//Link를 이용해 string 이외에 더 많은 데이터를 보낼 수 있다
}}
>
{coin.id}
</Link>
</Coin>
))}
</CoinsList>
)}
</Container>
);
}
export default Coins;
function Price() {
return <h1>Price</h1>;
}
export default Price;
function Chart() {
return <h1>Chart</h1>;
}
export default Chart;