노마드코더 ReactJS 마스터클래스 3

딩쓰·2023년 4월 4일
0
post-thumbnail

#5.0 ~ 5.4 2023.04.04 (화)

5 CRYPTO TRACKER (5.0 ~ 5.12)

섹션4는 React router 버전6에 관한 내용이고,이 강의에서는 버전5를 사용하기 때문에 먼저 버전5로 만든다음, 추후에 버전6로 리팩토링 할 예정임. 아직 많은 프로젝트들이 버전5 클래식 버전을 사용하고 있으니까 사용해도 아무 문제 없음.

5.0 코인 어플리케이션 세팅

이번 섹션에서는 지금까지 배운 TS,스타일 컴포넌트, 리액트의 Router Dom을 이용하며 평범하게 데이터를 fetching 한다음 react-query로 바꾸는 연습도 하면서 간단한 코인 트래킹 어플리케이션을 만들어 볼거임.

시작하기전 먼저 Set up을 해보자!

react-query 세팅

npm i react-router-dom@5.3.0 react-query
npm i --save-dev @types/react-router-dom

위의 명령어로 React-router와 React-query를 설치해줌.그리고 타입스크립트는 react-router-dom이 뭔지를 모르니 @types도 설치해주자.

페이지 라우팅은 다음과 같이 할것.
/ -> 전체 코인들
/:id -> /btc -> 코인 디테일 페이지
/btc/information -> 코인 디테일 페이지에서 정보 페이지로
/btc/chart -> 코인 디테일 페이지에서 차트 페이지로


src폴더 안에 routes폴더를 만들어주고 첫번째 스크린인 Coins.tsx파일과 디테일 페이지인 Coin.tsx파일을 만들어줌.

import { BrowserRouter, Switch, Route } from "react-router-dom";
import Coin from "./routes/Coin";
import Coins from "./routes/Coins";

function Router() {
  return (
    <BrowserRouter>
      <Switch>
        <Route path="/:coinId">
          <Coin />
        </Route>
        <Route path="/">
          <Coins />
        </Route>
      </Switch>
    </BrowserRouter>
  );
}

export default Router;

src폴더 안에 Router.tsx파일을 만들어서 위와 같이 react-router-dom을 세팅 해줌.


App.tsx에 위와 같이 Router를 렌더링 해줌.

  • coin.tsx파일에서 URL의 파라미터 부분을 잡아내기 위해서 useParams()를 사용했는데 타입스크립트는 useParams()를 빈 오브젝트라고 생각해서 오류를 내고 있음.

  • 타입스크립트에게 Routeparams라는 이름의 interface로 타입을 정의해주면 됨. 또는 const {coinId} = useParams<{coinId:string}>();로 대체 가능함.
  • useParams로 파라미터를 가져온걸 확인할 수 있음.

5.1 Styles


이번엔 CSS set up을 해보자. 아무것도 건들지 않은 CSS는 위와 같이 브라우저의 기본 스타일을 가지고 있는데 (h1이 기본 font-size를 가지거나 List가 padding을 가지는것 등등) 이걸 다 없앨 거임.

import { Reset } from 'styled-reset'를 설치해서 <Reset/>컴포넌트를 사용하는 방법도 있지만 Reset CSS를 이용해 전체 document에 적용해 볼거임.

그렇다면 스타일 컴포넌트에서 전체 document에 적용 시키려면 어떻게 해야 할까?

//App.tsx
import { createGlobalStyle } from "styled-components";
import Router from "./Router";

const GlobalStyle = createGlobalStyle`
 `;

function App() {
  return (
    <>
      <GlobalStyle></GlobalStyle>
      <Router></Router>
    </>
  );
}

export default App;
  • styled-components는 전체 document에 적용할 수 있는 createGlobalStyle이란 프로퍼티를 갖고 있음.
  • <GlobalStyle/>컴포넌트는 렌더링 될 때, 전역 스코프에 스타일들을 적용시켜줌.
  • <> </> : Fragment라고 불리며 일종의 유령 컴포넌트임. 2개의 컴포넌트를 리턴해야 할 때 사용함. 부모 없이 서로 붙어 있는 많은 것들을 리턴할 수 있음.
import { createGlobalStyle } from "styled-components";
import Router from "./Router";

const GlobalStyle = createGlobalStyle`
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, 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, 
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, menu, nav, section {
	display: block;
}
body {
	line-height: 1;
}
ol, ul {
	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;
}
 `;

function App() {
  return (
    <>
      <GlobalStyle></GlobalStyle>
      <Router></Router>
    </>
  );
}

export default App;
  • 위와 같이 Reset CSS 파일을 붙여 넣어주면 브라우저의 기본값이 다 없어짐.
  • 그 외 구글 폰트와 배경컬러, 폰트컬러 등도 다 설정해 줌.

5.2 Home part One

이번 강의에서는 어플리케이션에 들어갔을때 가장 먼저 보이는 Coins screen을 만들어 보자. 코인리스트를 만들거임.

//coins.tsx
import { Link } from "react-router-dom";
import styled from "styled-components";

const Container = styled.div`
  padding: 0px 20px;
`;

const Header = styled.header`
  height: 10vh;
  display: flex;
  justify-content: center;
  align-items: center;
`;

const CoinList = styled.ul``;

const Coin = styled.li`
  background-color: white;
  color: ${(props) => props.theme.bgColor};
  margin-bottom: 10px;
  border-radius: 15px;
  a {
    padding: 20px;
    transition: color 0.2s ease-in;
    display: block;
  }
  &:hover {
    a {
      color: ${(props) => props.theme.accentColor};
    }
  }
`;

const Title = styled.h1`
  font-size: 48px;
  color: ${(props) => props.theme.accentColor};
`;
const coins = [
  {
    id: "btc-bitcoin",
    name: "Bitcoin",
    symbol: "BTC",
    rank: 1,
    is_new: false,
    is_active: true,
    type: "coin",
  },
  {
    id: "eth-ethereum",
    name: "Ethereum",
    symbol: "ETH",
    rank: 2,
    is_new: false,
    is_active: true,
    type: "coin",
  },
  {
    id: "hex-hex",
    name: "HEX",
    symbol: "HEX",
    rank: 3,
    is_new: false,
    is_active: true,
    type: "token",
  },
];

function Coins() {
  return (
    <Container>
      <Header>
        <Title>코인</Title>
      </Header>
      <CoinList>
        {coins.map((coin) => (
          <Coin key={coin.id}>
            <Link to={`/${coin.id}`}>{coin.name} &rarr;</Link>
          </Coin>
        ))}
      </CoinList>
    </Container>
  );
}

export default Coins;
  • api를 들고 와서 배열로 만들어주고 map함수로 li태그를 매핑해 줌.
  • 일반 <a> 태그를 쓰면 새로고침되어 버리기 때문에 react-router-dom의 Link 컴포넌트를 사용해서 link를 걸어줌.
  • a링크의 글씨 밖까지 클릭되게 하기 위해 위와 같이 a태그에 display:block 해줌.
  • 누른 링크의 글자색이 보라색으로 되는것은 App.tsx로 가서a{color:inherit;}로 바꿔줌.

  • link를 눌러도 url만 바뀌고 렌더링이 바뀌지 않는 문제가 있었는데 index.tsx 에서 렌더 내부의 <React.StrictMode> 를 div로 바꿔 해결했다. 댓글을 보니 react-router v6는 문제가 없다고 하였고, v5의 문제인 것 같았다.

결과화면 ↓

5.3 Home part Two

이번 강의에서는 API로부터 데이터를 fetch(가져오기)해보자. 데이터가 5000개를 받아오는데 100개로 줄일 거임.

//Coins.tsx
const Container = styled.div`
  padding: 0px 20px;
  max-width: 480px;
  margin: 0 auto;
`;

먼저 화면을 크게 했을 때도, 모바일 화면처럼 화면을 가운데에 위치하기 위해서 max-width와 margin을 추가해 줌.

결과화면 ↓

//Coins.tsx
interface CoinInterface {
  id: string;
  name: string;
  symbol: string;
  rank: number;
  is_new: boolean;
  is_active: boolean;
  type: string;
}
// {
//   id: "hex-hex",
//   name: "HEX",
//   symbol: "HEX",
//   rank: 3,
//   is_new: false,
//   is_active: true,
//   type: "token",
// }, 원래 데이터의 모습을 바탕으로 interface를 만듦
  • 데이터를 가져오기 전에, 데이터의 interface를 만들어 줘야 함. 데이터를 API에서 가져올 때도 똑같이 타입스크립트에게 정의해 줘야 함.
//Coins.tsx
interface CoinInterface {
  id: string;
  name: string;
  symbol: string;
  rank: number;
  is_new: boolean;
  is_active: boolean;
  type: string;
}

function Coins() {
  const [coins, setCoins] = useState<CoinInterface[]>([]);
  const [loading, setLoading] = useState(true);
  
useEffect(() => {
  (() => console.log(1))();
}, []);
  
return (
    <Container>
      <Header>
        <Title>코인</Title>
      </Header>
      <CoinList>
        {coins.map((coin) => (
          <Coin key={coin.id}>
            <Link to={`/${coin.id}`}>{coin.name} &rarr;</Link>
          </Coin>
        ))}
      </CoinList>
    </Container>
  );
}

export default Coins;
  • useState()를 만들어서 state안에다가 초기값을 빈 배열로 적용해 주고, 타입스크립트에게 interface를 이용해 coins State는 coins로 이루어진 array라고 알려줌.
  • 이제 useEffect를 이용해 트릭을 사용할건데, 위와 같이 useEffect를 만들면 그 자리에서 바로 function을 execute(실행)할 수 있음.
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";

const Container = styled.div`
  padding: 0px 20px;
  max-width: 480px;
  margin: 0 auto;
`;

const Header = styled.header`
  height: 10vh;
  display: flex;
  justify-content: center;
  align-items: center;
`;

const CoinList = styled.ul``;

const Coin = styled.li`
  background-color: white;
  color: ${(props) => props.theme.bgColor};
  margin-bottom: 10px;
  border-radius: 15px;
  a {
    padding: 20px;
    transition: color 0.2s ease-in;
    display: block;
  }
  &: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;
`;

interface CoinInterface {
  id: string;
  name: string;
  symbol: string;
  rank: number;
  is_new: boolean;
  is_active: boolean;
  type: string;
}

function Coins() {
  const [coins, setCoins] = useState<CoinInterface[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    (async () => {
      const response = await fetch("https://api.coinpaprika.com/v1/coins");
      const json = await response.json();
      //console.log(json)
      setCoins(json.slice(0, 100));
      setLoading(false);
    })();
  }, []);

  console.log(coins);
  return (
    <Container>
      <Header>
        <Title>코인</Title>
      </Header>
      {loading ? (
        <Loader>"Loading..."</Loader>
      ) : (
        <CoinList>
          {coins.map((coin) => (
            <Coin key={coin.id}>
              <Link to={`/${coin.id}`}>{coin.name} &rarr;</Link>
            </Coin>
          ))}
        </CoinList>
      )}
    </Container>
  );
}

export default Coins;
  • 이제 위에는 async, 아래는 API의 response를 받기 위해 await을 쓰고, response로부터 json을 받아옴.

  • 콘솔로그로 json을 받았을때 5만개정도의 데이터를 받아오는걸 확인하고, 우리는 100개만 받아오고 싶기 때문에 slice(0,100)으로 잘라줌.
  • 위와같이 데이터를 받아옴.

  • 마지막으로 loading State를 만들어 true일땐 Loading 컴포넌트를 false일땐 코인리스트가 보이도록 만들어줌.
    -> 나중엔 react-query로 loading 컴포넌트를 만들어줄거임.

5.4 Route States

이제 api를 이용해 coins 스크린(홈페이지)의 코인아이콘 이미지들을 불러와보자.

  • <Img/> 스타일 컴포넌트를 만들어서 src에 url을 써주고, url맨 뒤에는 coin.id를 넣어줘서 코인들의 각각의 이미지를 불러오게 함.
  • Coins.tsx 페이지는 완성했으니 이제 coin.tsx 페이지를 만들어 볼거임.
  • Coins.tsx페이지에서 api를 불러 데이터를 받아오고 있고 개별 코인 페이지로 화면이동을 하면 parameter를 이용해 url로 개별 코인 정보(coin.id)를 넘기고 있는데, 이러면 개별 코인 페이지에서도 api를 불러오게 되어 이 과정에서 user들은 오직 loading만 보는 문제가 있음.
    💡 그래서 url로 정보를 넘기는 것이 아닌 State를 이용해 정보를 넘길거임!

  • react-router-dom인<Link/> 컴포넌트를 이용해 object형식의 state를 개별 코인 페이지로 넘겨줌.
  • 개별 코인 페이지는 useLocation()으로 state를 받아옴.
  • 홈 페이지가 아닌 개별 코인페이지 url로 바로 접속했을때, state를 못 받아오는 문제가 있음.
    ->{state?.name || "Loading..."}로 state의 name이 있으면 name을 보여주고 없으면 Loading string을 보이도록 해서 해결함.

결과화면↓

#5.5 ~ 5.7 2023.04.07 (금)

5.5 Coin Data

이번 강의에서는 개별 코인 상세페이지에서 보여줄 데이터를 불러 올거임.
사용할 URL은 아래와 같이 2가지임.
https://api.coinpaprika.com/v1/coins/btc-bitcoin
https://api.coinpaprika.com/v1/tickers/btc-bitcoin

첫번째 URL에서는 필요한 정보를 얻어올 거고, 두번째 URL에서는 코인의 정보와 가격들을 얻어올 거임.

  • URL에 coinId를 넣어야 하는데 우리는 이미 useParams()로 갖고 있음.
//coin.tsx
function Coin() {
  const [loading, setLoading] = useState(true);
  const { coinId } = useParams<Routeparams>();
  const { state } = useLocation<RouteState>();

  // console.log(state); {name: 'Bitcoin'}
  useEffect(() => {
    ()();
  }, []);
  
  return (
    <Container>
      <Header>
        <Title>{state?.name || "Loading..."}</Title>
      </Header>
      {loading ? <Loader>"Loading..."</Loader> : null}
    </Container>
  );
}

export default Coin;
  • 먼저 useEffect로 코드가 딱 한번만 실행되도록 세팅하고, 위와 같은 형태로 즉시 실행 될 function을 만듦.
 const response = await (
        await fetch(`https://api.coinpaprika.com/v1/coins/${coinId}`)
      ).json();
  • 위 한 줄의 solution이 responsejson이라는 2개의 변수를 받은거임.
    -> 코드를 한줄로 만들어서 캡슐화함.

  • infoData와 priceData로 데이터를 잘 받아 오는 것을 확인함.
  • useState를 2개 만들어 set수정함수에 각각 넣어줌.

  • useState의 state infopriceInfo는 타입스크립트가 빈 object라고 생각함.
    -> 그래서 타입스크립트에게 설명을 해줘야 함. 나중에 원한다면 자동으로 생성되게 할 수도 있고, 대부분의 상황에서는 API의 타입에 대한 정보를 자동생성 할 수 있음.

5.6 Data Types

이번 강의에서는 infopriceInfo를 빈 object라고 생각하는 타입스크립트에게 데이터를 설명해 줄 거임.


  • infopriceInfo의 interface를 만들건데 먼저 console.log()로 데이터를 받아오는것을 확인하고 info 데이터의 오른쪽 마우스를 눌러서 나오는 Store object as global variable을 클릭하면 object데이터가 temp1에 저장됨.
    -> infoData가 필요할때 temp1에 접근할 수 있음.
  • 똑같은 방법으로 priceInfotemp2에 저장해줌.
interface IInfoData {}

interface IPriceData {}
  • interface를 2개 만듦. 가끔 타입스크립트 코드 베이스 같은걸 보면, 사람들이 interface이름을 지을 때 앞에 대문자 I를 붙이는 걸 볼 수 있음. 나중에 코드를 읽을 때 이게 interface인지 알아볼 수 있게 하는 방법임.
    -> 이번 강의에서는 필요하지 않기 때문에 이 방법을 사용하지 않을것.

✔️ 댓글참고
VSCode 단축키
Ctrl(Command)+D: 같은 문자열 선택
Shift+Alt(Option)+i: 선택한 모든 문자열에 가장 우측 끝으로 포커싱
Ctrl(Command)+Shift+오른쪽 화살표: 현재 선택한 문자열을 기준으로 우측 끝까지 문자열 선택

  • 위와 같이 Object.keys()를 사용하여 temp1key값들을 다 모아서 스트링으로 만듦.


  • interface에 key값들을 붙여넣기 하고, 모든 쉼표들을 command + D 단축키로 선택해주고 지운다음 엔터를 치면 위와 같이 정렬됨.

  • 이번엔 value값들의 type이 필요함. Object.values()를 사용하여 temp1vlaue값들을 다 모아서 map()함수로 type으로 바꾼다음 스트링으로 만듦.

  • value값들을 붙여넣기 해서 쉼표를 위에했던 방법으로 정렬해주고 복사해줌.

  • interface의 key값들을 선택한다음 Shift+Option+i 으로 커서가 오른쪽으로 포커싱되면 value값들을 붙여넣기 해줌.

  • 붙여넣으면 위와 같이 interface가 정의 됐으며 temp2도 같은 방법으로 해줌.

  • 한 가지 문제점은 데이터의 타입이 array인것도 object로 정의 되게 때문에 이런건 따로 interface를 만들어서 정의해 줘야함.

  • 위와 같이 interface로 array를 일일히 정의해 주고 useState에 타입을 적어주면 완성! temp2도 똑같은 방법으로 하면됨.
//coin.tsx
import { useEffect, useState } from "react";
import { useLocation, useParams } from "react-router-dom";
import styled from "styled-components";

const Title = styled.h1`
  font-size: 48px;
  color: ${(props) => props.theme.accentColor};
`;

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: 10vh;
  display: flex;
  justify-content: center;
  align-items: center;
`;

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;
  logo: 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;
  links: object;
  links_extended: object;
  whitepaper: object;
  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;
    };
  };
}

function Coin() {
  const [loading, setLoading] = useState(true);
  const { coinId } = useParams<Routeparams>();
  const { state } = useLocation<RouteState>();
  const [info, setInfo] = useState<InfoData>();
  const [priceInfo, setPriceInfo] = useState<PriceData>();
  // console.log(state); {name: 'Bitcoin'}

  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);

      console.log(infoData);
      console.log(priceData);
    })();
  }, []);

  return (
    <Container>
      <Header>
        <Title>{state?.name || "Loading..."}</Title>
      </Header>
      {loading ? <Loader>"Loading..."</Loader> : null}
    </Container>
  );
}

export default Coin;

댓글 참고 ↓
JSON데이터를 타입스크립트 타입으로 빠르게 변환시켜주는 사이트
https://app.quicktype.io/?l=ts

5.7 Nested Routes part One (중첩라우터)

  • 필요한 데이터를 다 받아왔으니 이제 screen을 paint 해볼 거임.
//coin.tsx
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);

      console.log(infoData);
      console.log(priceData);
    })();
  }, []);

시작하기전, API로부터 데이터를 request한 후에 loading이 false가 되도록 setLoading(false);을 해주고,

  • useEffect의 의존성 배열에 마우스를 가져다 두면 에러가 아닌 경고가 하나 뜨는데, useEffect가 coinID의 의존성을 잃어버렸다고 말하고 있음.

  • coinId 변수를 추가해주면 경고가 사라짐.
  • component의 시작에서만 코드를 한 번만 실행하고 싶다면 no dependencies([])를 사용해야 하는걸 알고 있을거임.
    하지만 hooks는 최선의 성능을 위해서 []안에 dependency를 넣어야 함. 그래서 hook안에서 우리가 coinId라고 불리는 것을 사용하고 있다고 알려주는 거임.
  • 위의 경우에는 coinId가 변하면 동작할텐데, coinId는 url에 위치해서 절대 변하지 않으므로 넣어도 됨.

  • 스타일 컴포넌트로 화면을 만들어줌.
  • <Price/><Chart/>로 이루어진 2개의 탭도 만들거임.

  • 위의 Title에서 state는 홈페이지에서 coin을 클릭할 때만 생기는 state라서 있으면 보여줄거고 없으면 loading중일때 "Loading..."을 보여주고 loading중이 아니라면 API로부터 받아온 name을 보여주게함.
  • loading ? "Loading..." : info?.name부분은 홈페이지로 부터 온게 아닌 경우에 실행될거임.(path가 / 가 아닌 다른 url로 들어온 경우)
  • routes폴더안에 Price.tsxChart.tsx 파일을 만들어줌.
  • react-router인 Switch와 Route로 Nested router(중첩라우터)를 만들고 안에 <Price/>컴포넌트와 <Chart/>컴포넌트가 렌더링 되게 만듦.

⭐️ Nested router(중첩라우터)란 route안에 있는 또 다른 route임. 웹사이트에서 탭을 사용할때나 스크린 안에 많은 섹션이 나뉘어진 곳에서 유용하게 쓰임.

  • 위와 같이 타입스크립트는 자동완성이 있어서 빠르게 만들 수 있는 장점이 있음.
  • ?문법은 만약 priceInfo가 없거나 undefined이거나 존재하지 않으면 total_supply에 대해 요구하지 않는 거임.
    -> ?를 붙이지 않는다면 total_supply를 항상 요구하게 되어서 priceInfo가 undefined라면 undefined안에 total_supply가 없다는 에러가 날거임.


btc-bitcoin/price url로 들어가면 컴포넌트가 보임.

btc-bitcoin/chart url로 들어가면 컴포넌트가 보임.

#5.8 ~ 5.12 (토)

5.8 Nested Routes part Two

price와 chart를 스위치하는 탭을 만들어 보자.

//Coin.tsx
<Link to={`/${coinId}/chart`}>Chart</Link>
<Link to={`/${coinId}/price`}>Price</Link>
           
<Switch>
  <Route path={`/:coinId/price`}>
    <Price />
  </Route>
  <Route path={`/:coinId/chart`}>
    <Chart />
  </Route>
</Switch>
  • Link 컴포넌트를 만들어서 rerender 되지 않게 하고, 클릭하면 chart URL 또는 price URL로 가게 만들어서 탭을 만듦.
  • 유저가 탭을 클릭했을때 선택된 탭을 알려주고 싶으면 유저가 있는 곳의 URL에 대한 정보를 알아야 하는데 어떻게 정보를 불러올 수 있을까?
    -> useRouteMatch()를 이용해서 불러올 수 있음!

  • 먼저 Tabs와 Tab 스타일 컴포넌트를 만들어서 Link를 감싸줌.
  • useRouteMatch() 훅을 이용해 선택한 url에 접속했을때 정보를 오브젝트로 넘겨받을 수 있음. ()안에 들어간 url이 아닐때는 null을 넘겨줌.
  • Tab 컴포넌트에 isActive라고 불리는 prop을 가지게 하고 boolean의 형태로 만듦.

  • isActive를 priceMatch나 chartMatch에서 받아올건데 만약 priceMatch나 chartMatch가 null이 아니라면 (선택한 URL에 들어와 있다면 ) true가 될거고 color: ${(props) =>props.isActive ? props.theme.accentColor : props.theme.textColor}; 로 선택한 탭은 accentColor로 그렇지 않은 탭은 기본 textColor로 설정해 줌.

탭 결과화면 ↓

5.9 React query 1

react query 세팅하기

아래의 명령어로 설치해 줌. (react query 공식페이지 참고)

npm i react-query (17버전)
npm i @tanstack/react-query (18버전)

  • react 버전이 18이면 타입스크립트에서 react query를 못 불러와서 두번째 명령어로 설치해야함. (강의에서는 리액트 17버전이라 첫번재 명령어로 설치함)
//index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { QueryClient, QueryClientProvider } from "react-query";// import 해오기
import { ThemeProvider } from "styled-components";
import App from "./App";
import { theme } from "./theme";
 
const queryClient = new QueryClient();  //1

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <div>
    <QueryClientProvider client={queryClient}>  //2
      <ThemeProvider theme={theme}>
        <App />
      </ThemeProvider>
    </QueryClientProvider>
  </div>
);
  • src 폴더의 index.tsx 파일에서 위와 같이 세팅해 줌.
  • <ThemeProvider/>와 같은 맥락으로 <QueryClientProvider/> 안에 있는 모든 것은 queryClient에 접근할 수 있음.
    세팅 끝!

react query에 대해서 배워보자.

coin.tsx (코인 상세 페이지)

  • react query는 우리 스스로 실행하고 있던 로직들을 축약해 줄 수 있음.
    -> 위의 loadinginfo는 각각 로딩과 데이터를 위한 state임. 그리고 데이터가 준비되면 데이터를 state에 넣고 로딩은 false넣는 과정을 우리 스스로 했음. react query는 이 모든 과정을 자동으로 해줄 거임!
    -> 위의 usestate들과 useEffect들을 react query코드로 대체할 수 있음.

coins.tsx (홈페이지)

1. 먼저 데이터를 fetch하는 fetcher 함수가 필요한데, API와 관련된 것들은 component들과 멀리 떨어져 있는게 좋으니 src/api.ts 파일을 만들어서 fetcher 함수를 만들어줌.
위와 같이 fetcher 함수는 fetch promise를 꼭 return 해 줘야함. await / async 대신 promise를 사용함.

usequery


2. coins.tsx에 원래 있던 state와 useEffect를 주석처리하고 react query의 useQuery 훅으로 대체함.
useQuery는 2가지 argument가 필요한데, 첫 번째는 queryKey 이며 우리 query의 고유식별자임. 이름은 마음대로 지어도 무관함.() 두 번째 argument에는 fetcher 함수를 넣으면 됨.

  1. {}안에는 useQuery hook이 fetcher 함수를 불러오고 return 하는 것들이 들어감.
    fetcher 함수가 loading 중이라면 isLoading에서 boolean 값으로 알려주고, fetcher 함수가 끝나면 json값을 data에 넣음.


4. 원래 리턴되던 loading state를 useQuery에서 오는isLoading으로 바꾸고, coins state를 data로 바꿔줌. 하지만 타입스크립트는 data가 뭔지 몰라 coin에 오류를 나타냄.


5. 위와 같이 interface로 정의해주면 타입스크립트는 data가 CoinInterface array거나 undefined인걸 알게됨. 위와 같은 오류는 data?로 하면 없어짐.
data는 약 5만개의 모든 데이터를 가져오기 때문에 slice를 이용함.

  • 원래는 코인 상세 페이지에서 홈페이지로 돌아오면 로딩이 보였는데 react query를 사용한후 loading이 보이지 않음.
    -> react query가 데이터를 캐시에 저장해두고 캐시에서 가져온 데이터를 주기 때문임. 캐시를 직접 시행하는 건 꽤 어렵지만 react query에 저장한다면 쉬움!

5.10 React Query part Two

코인 상세 페이지(coin.tsx)를 react query로 리팩토링 해보자!

  • 홈페이지에서 코인 상세 페이지로 들어가면 Loading이 나오는데 다시 돌아갔다가 와도 Loading이 나오는 상태임.
    -> 코인 상세 페이지로 돌어갈 때마다 API에 접근하는 것을 의미함. 이걸 react query 코드로 바꿔볼거임.

react query를 시각화 하기 위해서 Devtools(Developer Tools)라는 것을 사용할 수 있는데, react query에 있는 devtools를 import 해오면 캐시에 있는 query를 볼 수 있음.

npm i -D @tanstack/react-query-devtools

강의에서는 Devtools를 사용하기 위해서 따로 설치하지 않았지만 전 단계에서 @tanstack/react-query 모듈을 설치했으므로 위의 명령어로 devtool을 따로 설치해줌.

이제 src/App.tsx 파일에 가서 Devtools를 세팅하자.

//App.tsx
import { ReactQueryDevtools } from "react-query/devtools"; //1
// import { ReactQueryDevtools } from "@tanstack/react-query-devtools";  -> 오류남
import { createGlobalStyle } from "styled-components";
import Router from "./Router";

const GlobalStyle = createGlobalStyle`
//코드생략
 `;

function App() {
  return (
    <>
      <GlobalStyle></GlobalStyle>
      <Router></Router>
      <ReactQueryDevtools initialIsOpen={true} /> {/* //2 */}
    </>
  );
}

export default App;
  • Devtools를 import 해 옴.
  • Router아래에서 render하고 initiallsOpen은 true로 둠.
    오류해결
    No QueryClient set, use QueryClientProvider to set one이라는 오류가 떴는데
    Coins.tsx
    import { useQuery } from "react-query";
    App.tsx
    import { ReactQueryDevtools } from "react-query/devtools";

@tanstack/붙어있는것을 위와 같이 바꾸니까 해결됨!

세팅이 완료되면 아래와 같이 Devtools가 나옴.

  • 앱이 가지고 있는 모든 query를 확인할 수 있고, 캐시에 가지고 있는 data들이 나옴.
  • 데이터를 다시 fetch하는 refetch기능과 쿼리를 reset하는 기능도 있음.

코인 상세 페이지(coin.tsx)로 가서

  • 위의 state들과 fetch들을 react query로 바꿔줄거임.

api.ts파일로 가서 fetcher함수 2개를 만들어 주자

  • 반복되는 url을 변수로 만듦
  • 위와같이 coinId를 사용하면 정의되지 않았다고 나오는데 fetchCoinInfo와 fetchCoinTickers의 argument로 coinId라는 이름의 string을 넘겨줌.

  • coin.tsx로와서 원래 있던 state들과 fetch들을 주석처리하고 useQuery를 만듦.
  • query키값에 고유한 값인 coinId를 넣고 fetcher함수들이 coinId가 필요로 한 상황이니 argument에 넣어줌.
  • 2가지의 query가 같은 query키값을 가지면 안되기 때문에
const {} = useQuery(["info",coinId], () => fetchCoinInfo(coinId));
  const {} = useQuery(["tickers",coinId], () => fetchCoinTickers(coinId));

각각 다른 이름으로 바꿔줌.

const {isLoading:infoLoading , data:infoData} = useQuery(["info",coinId], () => fetchCoinInfo(coinId));
  const {isLoading:tickersLoading, data:tickersData } = useQuery(["tickers",coinId], () => fetchCoinTickers(coinId));
  • isLoading과 data도 각각 다른 이름으로 만듦.


  • loading을 모르는 상황이 되었으니 const loading = infoLoading || tickersLoading;으로 정의해줌.

  • info -> infoData , priceInfo -> tickersData로 바꿔줬는데
    타입스크립트가 infoData와 tickersData가 뭔지 모르는 문제가 있음.

  • 위와 같이 interface로 정의해주면 끝!


이제 코인 상세 페이지 -> 홈페이지 -> 코인 상세 페이지로 다시 들어와도 loading이 뜨지 않는걸 볼 수 있음!

5.12 Price Chart

이제 코인 상세 페이지에서 차트를 클릭하면 제일 마지막 주의 시각화 차트를 보여주게 만들거임.

src/routes/Chart.tsx파일로 가서

비트코인 페이지에 들어가면 비트코인의 차트를 보여줘야 함. 유저가 보고 있는 암호화폐를 알아내는 2가지 방법이 있는데 하나는 위와같이 useParams()를 사용하는 것임.

하지만 코인 상세 페이지에선 chart를 렌더링하고 있고 URL로부터 이미 coinId값을 가지고 있기 때문에 코인 상세 페이지에서 props를 보내는 방법을 쓸 수 있음.

  • 위와 같이 Chart 컴포넌트에 coinId를 props로 보내고 chart는 coinId prop을 가지고 있지 않을테니
//Chart.tsx
interface ChartProps {
  coinId: string;
}

function Chart({ coinId }: ChartProps) {
  return <h1>Chart</h1>;
}

export default Chart;
  • 위와 같이 props들이 ChartProps라는 것을 알려주자.
  • 이제 coinId를 가지고 있으니 API에 요청을 보내서 모든 가격을 가져와 볼거임.

  • api.ts로 가서 fetcher 함수를 하나 만들어줌.

  • chart.tsx로 가서 useQuery()를 만들어줌. fetcher함수에는 coinId가 필요하므로 argument에 coinID를 넣어줌.

interface IHistorical {
  time_open: number;
  time_close: number;
  open: string;
  high: string;
  low: string;
  close: string;
  volume: string;
  market_cap: number;
}

interface ChartProps {
  coinId: string;
}

function Chart({ coinId }: ChartProps) {
  const { isLoading, data } = useQuery<IHistorical[]>(["ohlcv", coinId], () =>
    fetchCoinHistory(coinId)
  );
  • 타입스크립트에게 data는 unknown 상태이니 interface로 정의도 해줌.
  • 위의 data는 코인의 하루동안의 가격임. (가장 높은 가격, 낮은 가격, 시작가격, 종가)
  • 2주치를 받아올테니 array여야함.

결과화면 ↓

  • react query devtools로 내가 만든 query들과 data로 확인할 수 있음.
profile
Frontend Developer

0개의 댓글