목차

    react-query에서 fetch 주기적으로 하기

    useQuery에서 refetchInterval를 설정해준다

    react-query Auto Refetching Example

     

    React Query Auto Refetching Example | TanStack Query Docs

    An example showing how to implement Auto Refetching in React Query

    tanstack.com

    const { isLoading: infoLoading, data: infoData } = useQuery<InfoData>(
      ["info", coinId],
      () => fetchCoinInfo(coinId),
      { refetchInterval: 5000 }
    );

     

    react-helmet

    각 페이지의 head를 변경할 수 있게 해준다

    react-helmet npm

     

    react-helmet

    A document head manager for React. Latest version: 6.1.0, last published: 3 years ago. Start using react-helmet in your project by running `npm i react-helmet`. There are 4256 other projects in the npm registry using react-helmet.

    www.npmjs.com

    설치
    npm i react-helmet --save --legacy-peer-deps
    npm i --save-dev @types/react-helmet
    적용
    import React from "react";
    import {Helmet} from "react-helmet";
     
    class Application extends React.Component {
      render () {
        return (
            <div className="application">
                <Helmet>
                    <meta charSet="utf-8" />
                    <title>My Title</title>
                    <link rel="canonical" href="http://mysite.com/example" />
                </Helmet>
                ...
            </div>
        );
      }
    };
    최종코드
    import {
      useLocation,
      useParams,
      Switch,
      Route,
      Link,
      useRouteMatch,
    } from "react-router-dom";
    import styled from "styled-components";
    import { useState, useEffect } from "react";
    import Price from "./Price";
    import Chart from "./Chart";
    import { useQuery } from "react-query";
    import { fetchCoinInfo, fetchCoinTickers } from "../api";
    import { Helmet } from "react-helmet";
    
    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;
      margin-bottom: 20px;
    `;
    
    const Title = styled.h1`
      font-size: 48px;
      color: ${(props) => props.theme.accentColor};
    `;
    
    const Loader = styled.span`
      text-align: center;
      display: block;
    `;
    
    const Overview = styled.div`
      display: flex;
      justify-content: space-between;
      background-color: ${(props) => props.theme.textColor};
      color: ${(props) => props.theme.bgColor};
      padding: 10px 20px;
      border-radius: 10px;
    `;
    
    const OverviewItem = styled.div`
      display: flex;
      flex-direction: column;
      align-items: center;
      span:first-child {
        font-size: 10px;
        text-transform: uppercase;
        margin-bottom: 5px;
      }
    `;
    
    const Description = styled.div`
      margin: 20px 20px;
      padding: 10px;
      border-left: 6px solid ${(props) => props.theme.accentColor};
    `;
    
    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;
      background-color: ${(props) =>
        props.$isActive ? props.theme.accentColor : props.theme.textColor};
      color: ${(props) => props.theme.bgColor};
      padding: 10px 0px;
      border-radius: 10px;
      a {
        display: block;
      }
    `;
    
    const BackLink = styled.div`
      position: absolute;
      top: 5vh;
      left: 5vh;
      color: ${(props) => props.theme.textColor};
      &:hover {
        a {
          color: ${(props) => props.theme.accentColor};
        }
      }
    `;
    
    interface RouteParams {
      coinId: string;
    }
    
    interface RouteState {
      name: string;
    }
    
    // interface ITag {
    //   id: string;
    //   name: string;
    //   coin_counter: number;
    //   ico_counter: number;
    // }
    
    interface InfoData {
      id: string;
      name: string;
      symbol: string;
      rank: number;
      is_new: boolean;
      is_active: boolean;
      type: string;
      logo: string;
      // tags: ITag[];
      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;
        };
      };
    }
    
    function Coin() {
      const { coinId } = useParams<RouteParams>();
      const { state } = useLocation<RouteState>();
      const priceMatch = useRouteMatch("/:coinId/price");
      const chartMatch = useRouteMatch("/:coinId/chart");
      const { isLoading: infoLoading, data: infoData } = useQuery<InfoData>(
        ["info", coinId],
        () => fetchCoinInfo(coinId),
        { refetchInterval: 500000 }
      );
      const { isLoading: tickerLoading, data: tickerData } = useQuery<PriceData>(
        ["ticker", coinId],
        () => fetchCoinTickers(coinId)
      );
      // const [loading, setLoading] = useState(true);
      // const [info, setInfo] = useState<InfoData>();
      // const [priceInfo, setPriceInfo] = useState<PriceData>();
      // console.log(priceMatch);
      // useEffect(() => {
      //   (async () => {
      //     const infoData = await (
      //       await fetch(`https://api.coinpaprika.com/v1/coins/${coinId}`)
      //     ).json();
      //     setInfo(infoData);
      //     const priceData = await (
      //       await fetch(`https://api.coinpaprika.com/v1/tickers/${coinId}`)
      //     ).json();
      //     console.log(priceData);
      //     setPriceInfo(priceData);
      //     setLoading(false);
      //   })();
      // }, []);
    
      const loading = infoLoading || tickerLoading;
      return (
        <Container>
          <Helmet>
            <title>
              {state?.name ? state.name : loading ? "Loading" : infoData?.name}
            </title>
            <link
              rel="icon"
              type="image/png"
              href={`https://coinicons-api.vercel.app/api/icon/${tickerData?.symbol.toLowerCase()}`}
              sizes="16x16"
            />
          </Helmet>
          <Header>
            <BackLink>
              <Link to={{ pathname: "/" }}>&larr; Back</Link>
            </BackLink>
            <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>{tickerData?.quotes.USD.price.toFixed(4)}</span>
                </OverviewItem>
              </Overview>
              <Description>{infoData?.description}</Description>
              <Overview>
                <OverviewItem>
                  <span>total supply</span>
                  <span>{tickerData?.total_supply}</span>
                </OverviewItem>
                <OverviewItem>
                  <span>max supply</span>
                  <span>{tickerData?.max_supply}</span>
                </OverviewItem>
              </Overview>
    
              <Tabs>
                <Tab $isActive={priceMatch !== null}>
                  <Link to={`/${coinId}/price`}>price</Link>
                </Tab>
                <Tab $isActive={chartMatch !== null}>
                  <Link to={`/${coinId}/chart`}>chart</Link>
                </Tab>
              </Tabs>
    
              <Switch>
                <Route path={`/${coinId}/price`}>
                  <Price />
                </Route>
                <Route path={`/:coinId/chart`}>
                  <Chart coinId={coinId} />
                </Route>
              </Switch>
            </>
          )}
        </Container>
      );
    }
    
    export default Coin;
    오픈소스 라이브러리인 apexcharts로 차트를 간단하게 구현할 수 있다!

    참고 - 코인이름 값 가져오기

    /{코인이름}에서 /{코인이름}/chart 주소로 이동할 때 {코인이름} 값 받아오는 방법

    1. useParams - 주소에 있는 값 가져오기
    // Chart.tsx
    import { useParams } from "react-router-dom";
    
    function Chart() {
      const params = useParams();
      console.log(params);
      return <h1>Chart</h1>;
    }
    
    export default Chart;
    2. props로 받아서 사용하기
    // Chart.tsx
    interface ChartProps {
      coinId: string;
    }
    
    function Chart({ coinId }: ChartProps) {
      console.log(coinId);
      return <h1>Chart</h1>;
    }
    
    export default Chart;

     

    Chart 그리기

    ApexChart.js DEMOs

     

    JavaScript Chart Examples & Samples Demo – ApexCharts.js

    Get a glimpse of ApexCharts by exploring all the samples created using the library. You can download these JavaScript Chart Examples and use them freely.

    apexcharts.com

    설치
    npm install --save react-apexcharts apexcharts
    차트적용
    // Chart.tsx
    import ApexChart from "react-apexcharts";
    
    interface ChartProps {
      coinId: string;
    }
    
    function Chart({ coinId }: ChartProps) {
      return (
        <div>
          <ApexChart
            type="line"
            series={[
              { name: "potato", data: [1, 2, 3, 4, 5, 6] },
              { name: "tomato", data: [12, 5, 3, 23, 16, 17] },
            ]}
            options={{ chart: { height: 500, width: 500 } }}
          />
        </div>
      );
    }
    
    export default Chart;
    데이터로 차트 그리기
    // Chart.tsx
    import { useQuery } from "react-query";
    import { fetchCoinHistory } from "../api";
    import ApexChart from "react-apexcharts";
    
    interface ChartProps {
      coinId: string;
    }
    
    interface IPriceHis {
      time_open: number;
      time_close: number;
      open: string;
      high: string;
      low: string;
      close: string;
      volume: string;
      market_cap: number;
    }
    
    function Chart({ coinId }: ChartProps) {
      const { isLoading, data } = useQuery<IPriceHis[]>(["priceHis", coinId], () =>
        fetchCoinHistory(coinId)
      );
      return (
        <div>
          {isLoading ? (
            "Loading Chart"
          ) : (
            <ApexChart
              type="line"
              series={[
                {
                  name: `${coinId}`,
                  data: data?.map((price) => parseFloat(price.close)) ?? [],
                },
              ]}
              options={{
                chart: {
                  height: 500,
                  width: 500,
                  toolbar: { show: false },
                  background: "transparent",
                },
                grid: {
                  show: true,
                  borderColor: "#90A4AE",
                },
                stroke: { curve: "smooth", width: 2, colors: ["#ff4757"] },
                xaxis: { floating: true },
                yaxis: {
                  labels: {
                    formatter(val, opts) {
                      return val.toFixed(4);
                    },
                  },
                },
              }}
            />
          )}
        </div>
      );
    }
    
    export default Chart;

    react-query

    react-query는 useEffect, fetch문을 쉽게 대체할 수 있도록 해준다!

    이점

    1. useState, useEffect, fetch문으로 된 데이터의 fetch 과정을 단순화해준다
    2. 데이터를 cache에 저장하여 페이지를 되돌아올 경우 fetch를 안하고 저장된 데이터를 보여준다

    react-query  Docs

     

    Overview | TanStack Query Docs

    TanStack Query (FKA React Query) is often described as the missing data-fetching library for web applications, but in more technical terms, it makes fetching, caching, synchronizing and updating server state in your web applications a breeze. Motivation

    tanstack.com

    react의 버전에 따라서 설치가 다르다 아래는 react 18 기준
    npm i @tanstack/react-query
    index.tsx에 provider 적용 (사용하기 위한 설치 과정 같은 것)
    // index.tsx
    import React from "react";
    import ReactDOM from "react-dom/client";
    import App from "./App";
    import { ThemeProvider } from "styled-components";
    import { lightTheme } from "./theme";
    import { QueryClient, QueryClientProvider } from "react-query";
    
    const queryClient = new QueryClient();
    
    const root = ReactDOM.createRoot(
      document.getElementById("root") as HTMLElement
    );
    root.render(
      <QueryClientProvider client={queryClient}>
        <ThemeProvider theme={lightTheme}>
          <App />
        </ThemeProvider>
      </QueryClientProvider>
    );
    fetcher 함수 만들기
    기존 fetch한 내용을 분리하여 ts파일에서 함수로 만들어준다
    // api.ts
    export async function fetchCoins() {
      return fetch("https://api.coinpaprika.com/v1/coins").then((response) =>
        response.json()
      );
    }
    // export async function fetchCoins() {
    //   const response = await fetch("https://api.coinpaprika.com/v1/coins");
    //   const json = await response.json();
    //   return json;
    // }
    기존 구문을 대체하고 useQuery로 대체한다
    로딩의 boolean 여부는 isLoading에, fetch된 데이터 정보는 data에 저장됨
    import { useQuery } from "react-query";
    import { fetchCoins } from "../api";
    
    function Coins() {
      const { isLoading, data } = useQuery<ICoin[]>("allCoins", fetchCoins);
      // const [coins, setCoins] = useState<ICoin[]>([]);
      // const [loading, setLoading] = useState(true);
      // useEffect(() => {
      //   (async () => {
      //     const response = await fetch("https://api.coinpaprika.com/v1/coins");
      //     const json = await response.json();
      //     setCoins(json.slice(0, 100));
      //     setLoading(false);
      //   })();
      // }, []);
      return (
        ...
      )
    }
    coin에도 적용
    // Coin.tsx
    import {
      useLocation,
      useParams,
      Switch,
      Route,
      Link,
      useRouteMatch,
    } from "react-router-dom";
    import styled from "styled-components";
    import { useState, useEffect } from "react";
    import Price from "./Price";
    import Chart from "./Chart";
    import { useQuery } from "react-query";
    import { fetchCoinInfo, fetchCoinTickers } from "../api";
    
    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;
      margin-bottom: 20px;
    `;
    
    const Title = styled.h1`
      font-size: 48px;
      color: ${(props) => props.theme.accentColor};
    `;
    
    const Loader = styled.span`
      text-align: center;
      display: block;
    `;
    
    const Overview = styled.div`
      display: flex;
      justify-content: space-between;
      background-color: ${(props) => props.theme.textColor};
      color: ${(props) => props.theme.bgColor};
      padding: 10px 20px;
      border-radius: 10px;
    `;
    
    const OverviewItem = styled.div`
      display: flex;
      flex-direction: column;
      align-items: center;
      span:first-child {
        font-size: 10px;
        text-transform: uppercase;
        margin-bottom: 5px;
      }
    `;
    
    const Description = styled.div`
      margin: 20px 0px;
      padding: 10px;
    `;
    
    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;
      background-color: ${(props) =>
        props.$isActive ? props.theme.accentColor : props.theme.textColor};
      color: ${(props) => props.theme.bgColor};
      padding: 10px 0px;
      border-radius: 10px;
      a {
        display: block;
      }
    `;
    
    interface RouteParams {
      coinId: string;
    }
    
    interface RouteState {
      name: string;
    }
    
    // interface ITag {
    //   id: string;
    //   name: string;
    //   coin_counter: number;
    //   ico_counter: number;
    // }
    
    interface InfoData {
      id: string;
      name: string;
      symbol: string;
      rank: number;
      is_new: boolean;
      is_active: boolean;
      type: string;
      logo: string;
      // tags: ITag[];
      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;
        };
      };
    }
    
    function Coin() {
      const { coinId } = useParams<RouteParams>();
      const { state } = useLocation<RouteState>();
      const priceMatch = useRouteMatch("/:coinId/price");
      const chartMatch = useRouteMatch("/:coinId/chart");
      const { isLoading: infoLoading, data: infoData } = useQuery<InfoData>(
        ["info", coinId],
        () => fetchCoinInfo(coinId)
      );
      const { isLoading: tickerLoading, data: tickerData } = useQuery<PriceData>(
        ["ticker", coinId],
        () => fetchCoinTickers(coinId)
      );
      // const [loading, setLoading] = useState(true);
      // const [info, setInfo] = useState<InfoData>();
      // const [priceInfo, setPriceInfo] = useState<PriceData>();
      // console.log(priceMatch);
      // useEffect(() => {
      //   (async () => {
      //     const infoData = await (
      //       await fetch(`https://api.coinpaprika.com/v1/coins/${coinId}`)
      //     ).json();
      //     setInfo(infoData);
      //     const priceData = await (
      //       await fetch(`https://api.coinpaprika.com/v1/tickers/${coinId}`)
      //     ).json();
      //     console.log(priceData);
      //     setPriceInfo(priceData);
      //     setLoading(false);
      //   })();
      // }, []);
    
      const loading = infoLoading || tickerLoading;
      return (
        <Container>
          <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>open source</span>
                  <span>{infoData?.open_source ? "Yes" : "No"}</span>
                </OverviewItem>
              </Overview>
              <Description>{infoData?.description}</Description>
              <Overview>
                <OverviewItem>
                  <span>total supply</span>
                  <span>{tickerData?.total_supply}</span>
                </OverviewItem>
                <OverviewItem>
                  <span>max supply</span>
                  <span>{tickerData?.max_supply}</span>
                </OverviewItem>
              </Overview>
    
              <Tabs>
                <Tab $isActive={priceMatch !== null}>
                  <Link to={`/${coinId}/price`}>price</Link>
                </Tab>
                <Tab $isActive={chartMatch !== null}>
                  <Link to={`/${coinId}/chart`}>chart</Link>
                </Tab>
              </Tabs>
    
              <Switch>
                <Route path={`/${coinId}/price`}>
                  <Price />
                </Route>
                <Route path={`/:coinId/chart`}>
                  <Chart />
                </Route>
              </Switch>
            </>
          )}
        </Container>
      );
    }
    
    export default Coin;

     

    react-query-devtools

    cache에 저장되어 있는 데이터를 보여줌

    npm i @tanstack/react-query-devtools

    react-query-devtools Docs

     

    Devtools | TanStack Query Docs

    Wave your hands in the air and shout hooray because React Query comes with dedicated devtools! 🥳 When you begin your React Query journey, you'll want these devtools by your side. They help visualize all of the inner workings of React Query and will like

    tanstack.com

    아래 방법으로 적용한다
    // App.tsx
    import { createGlobalStyle } from "styled-components";
    import Router from "./Router";
    import { ReactQueryDevtools } from "react-query/devtools";
    function App() {
      return (
        <>
          <GlobalStyle />
          <Router />
          <ReactQueryDevtools initialIsOpen={true} />
        </>
      );
    }
    
    export default App;

    ✅react-router-dom@5.3 기준으로 작성되었음

    Nested Router

    return 문 안의 router로 특정 주소가 입력될 경우에 rendering될 수 있도록 할 수 있다!
    function Coin() {
      return (
        <Container>
          {loading ? (
            <Loader>Loading...</Loader>
          ) : (
            <>
              <Overview />
              <Switch>
                <Route path="/btc-bitcoin/price">
                  <Price />
                </Route>
                <Route path="/btc-bitcoin/chart">
                  <Chart />
                </Route>
              </Switch>
            </>
          )}
        </Container>
      );
    }

    useRouteMatch

    특정한 url 안에 있는지 알려줌
    페이지 내에서 nested router로 탭을 구성할 때 유용!
    const priceMatch = useRouteMatch("/:coinId/price");
    console.log(priceMatch);
    // 해당 url에 들어갈 경우 object 반환, 아닐 경우 null 반환
    최종 코드
    import {
      useLocation,
      useParams,
      Switch,
      Route,
      Link,
      useRouteMatch,
    } from "react-router-dom";
    import styled from "styled-components";
    import { useState, useEffect } from "react";
    import Price from "./Price";
    import Chart from "./Chart";
    import { useQuery } from "react-query";
    import { fetchCoinInfo, fetchCoinTickers } from "../api";
    import { Helmet } from "react-helmet";
    
    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;
      margin-bottom: 20px;
    `;
    
    const Title = styled.h1`
      font-size: 48px;
      color: ${(props) => props.theme.accentColor};
    `;
    
    const Loader = styled.span`
      text-align: center;
      display: block;
    `;
    
    const Overview = styled.div`
      display: flex;
      justify-content: space-between;
      background-color: ${(props) => props.theme.textColor};
      color: ${(props) => props.theme.bgColor};
      padding: 10px 20px;
      border-radius: 10px;
    `;
    
    const OverviewItem = styled.div`
      display: flex;
      flex-direction: column;
      align-items: center;
      span:first-child {
        font-size: 10px;
        text-transform: uppercase;
        margin-bottom: 5px;
      }
    `;
    
    const Description = styled.div`
      margin: 20px 20px;
      padding: 10px;
      border-left: 6px solid ${(props) => props.theme.accentColor};
    `;
    
    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;
      background-color: ${(props) =>
        props.$isActive ? props.theme.accentColor : props.theme.textColor};
      color: ${(props) => props.theme.bgColor};
      padding: 10px 0px;
      border-radius: 10px;
      a {
        display: block;
      }
    `;
    
    const BackLink = styled.div`
      position: absolute;
      top: 5vh;
      left: 5vh;
      color: ${(props) => props.theme.textColor};
      &:hover {
        a {
          color: ${(props) => props.theme.accentColor};
        }
      }
    `;
    
    interface RouteParams {
      coinId: string;
    }
    
    interface RouteState {
      name: string;
    }
    
    // interface ITag {
    //   id: string;
    //   name: string;
    //   coin_counter: number;
    //   ico_counter: number;
    // }
    
    interface InfoData {
      id: string;
      name: string;
      symbol: string;
      rank: number;
      is_new: boolean;
      is_active: boolean;
      type: string;
      logo: string;
      // tags: ITag[];
      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;
        };
      };
    }
    
    function Coin() {
      const { coinId } = useParams<RouteParams>();
      const { state } = useLocation<RouteState>();
      const priceMatch = useRouteMatch("/:coinId/price");
      const chartMatch = useRouteMatch("/:coinId/chart");
      const { isLoading: infoLoading, data: infoData } = useQuery<InfoData>(
        ["info", coinId],
        () => fetchCoinInfo(coinId),
        { refetchInterval: 500000 }
      );
      const { isLoading: tickerLoading, data: tickerData } = useQuery<PriceData>(
        ["ticker", coinId],
        () => fetchCoinTickers(coinId)
      );
      // const [loading, setLoading] = useState(true);
      // const [info, setInfo] = useState<InfoData>();
      // const [priceInfo, setPriceInfo] = useState<PriceData>();
      // console.log(priceMatch);
      // useEffect(() => {
      //   (async () => {
      //     const infoData = await (
      //       await fetch(`https://api.coinpaprika.com/v1/coins/${coinId}`)
      //     ).json();
      //     setInfo(infoData);
      //     const priceData = await (
      //       await fetch(`https://api.coinpaprika.com/v1/tickers/${coinId}`)
      //     ).json();
      //     console.log(priceData);
      //     setPriceInfo(priceData);
      //     setLoading(false);
      //   })();
      // }, []);
    
      const loading = infoLoading || tickerLoading;
      return (
        <Container>
          <Helmet>
            <title>
              {state?.name ? state.name : loading ? "Loading" : infoData?.name}
            </title>
            <link
              rel="icon"
              type="image/png"
              href={`https://coinicons-api.vercel.app/api/icon/${tickerData?.symbol.toLowerCase()}`}
              sizes="16x16"
            />
          </Helmet>
          <Header>
            <BackLink>
              <Link to={{ pathname: "/" }}>&larr; Back</Link>
            </BackLink>
            <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>{tickerData?.quotes.USD.price.toFixed(4)}</span>
                </OverviewItem>
              </Overview>
              <Description>{infoData?.description}</Description>
              <Overview>
                <OverviewItem>
                  <span>total supply</span>
                  <span>{tickerData?.total_supply}</span>
                </OverviewItem>
                <OverviewItem>
                  <span>max supply</span>
                  <span>{tickerData?.max_supply}</span>
                </OverviewItem>
              </Overview>
    
              <Tabs>
                <Tab $isActive={priceMatch !== null}>
                  <Link to={`/${coinId}/price`}>price</Link>
                </Tab>
                <Tab $isActive={chartMatch !== null}>
                  <Link to={`/${coinId}/chart`}>chart</Link>
                </Tab>
              </Tabs>
    
              <Switch>
                <Route path={`/${coinId}/price`}>
                  <Price />
                </Route>
                <Route path={`/:coinId/chart`}>
                  <Chart coinId={coinId} />
                </Route>
              </Switch>
            </>
          )}
        </Container>
      );
    }
    
    export default Coin;
    TypeScript를 사용할 때는 데이터를 가지고 올 때, 어떤 형태의 (interface) 데이터인지 알려줘야 한다!
    coin의 interface 지정
    interface CoinInterface {
      id: string;
      name: string;
      symbol: string;
      rank: number;
      is_new: boolean;
      is_active: boolean;
      type: string;
    }
    useState에서 coin의 interface 지정
    function Coins() {
      const [coins, setCoins] = useState<CoinInterface[]>([])
      return ...
    }
    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();
        setCoins(json.slice(0, 100));
        setLoading(false);
      })();
    }, []);
    최종 코드
    // Coins.tsx
    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: 10vh;
      display: flex;
      justify-content: center;
      align-items: center;
      margin-bottom: 20px;
    `;
    
    const CoinsList = styled.ul``;
    
    const Coin = styled.li`
      background-color: ${(props) => props.theme.textColor};
      color: ${(props) => props.theme.bgColor};
      border-radius: 15px;
      margin-bottom: 10px;
      a {
        padding: 20px;
        transition: color 0.15s 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();
          setCoins(json.slice(0, 100));
          setLoading(false);
        })();
      }, []);
      return (
        <Container>
          <Header>
            <Title>Coin</Title>
          </Header>
          {loading ? (
            <Loader>Loading...</Loader>
          ) : (
            <CoinsList>
              {coins.map((coin) => (
                <Coin key={coin.id}>
                  <Link to={`/${coin.id}`}>{coin.name} &rarr;</Link>
                </Coin>
              ))}
            </CoinsList>
          )}
        </Container>
      );
    }
    
    export default Coins;

    useLocation

    이전 페이지에 있던 데이터를 가져올 수 있다!

    React Router Dom v5 Link Docs

    Link에서 가져올 데이터를 state에 적어준다
    <Link
      to={{
        pathname: `/${coin.id}`,
        state: { name: coin.name },
      }}
    >
      {coin.name} &rarr;
    </Link>
    옮겨진 페이지에서 state안에 object 형태로 있는 것을 확인할 수 있다!
    // Coin.tsx
    const location = useLocation();
    console.log(location);
    // {pathname: '/btc-bitcoin', search: '', hash: '', state: {…}, key: 'qpimth'}
    // hash: ""
    // key: "qpimth"
    // pathname: "/btc-bitcoin"
    // search: ""
    // state: {name: 'Bitcoin'}
    // [[Prototype]]: Object
    해당 주소를 직접 입력하여 이전 페이지의 데이터가 없는 상태로 페이지가  render 될 경우에 location 정보는 없다.
    따라서 해당 경우도 고려해야한다
    interface RouteState {
      name: string;
    }
    
    function Coin() {
      const { state } = useLocation<RouteState>();
    
      return <Title>{state?.name || "Loading"}</Title>;
    }
    최종 코드
    // Coin.tsx
    import { useLocation, useParams } from "react-router-dom";
    import styled from "styled-components";
    import { useState, useEffect } from "react";
    
    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;
      margin-bottom: 20px;
    `;
    
    const Title = styled.h1`
      font-size: 48px;
      color: ${(props) => props.theme.accentColor};
    `;
    
    const Loader = styled.span`
      text-align: center;
      display: block;
    `;
    
    interface RouteParams {
      coinId: string;
    }
    
    interface RouteState {
      name: string;
    }
    
    // interface ITag {
    //   id: string;
    //   name: string;
    //   coin_counter: number;
    //   ico_counter: number;
    // }
    
    interface InfoData {
      id: string;
      name: string;
      symbol: string;
      rank: number;
      is_new: boolean;
      is_active: boolean;
      type: string;
      logo: string;
      // tags: ITag[];
      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;
        };
      };
    }
    
    function Coin() {
      const { coinId } = useParams<RouteParams>();
      const [loading, setLoading] = useState(true);
      const { state } = useLocation<RouteState>();
      const [info, setInfo] = useState<InfoData>();
      const [priceInfo, setPriceInfo] = useState<PriceData>();
      useEffect(() => {
        (async () => {
          const infoData = await (
            await fetch(`https://api.coinpaprika.com/v1/coins/${coinId}`)
          ).json();
          console.log(infoData);
          const priceData = await (
            await fetch(`https://api.coinpaprika.com/v1/tickers/${coinId}`)
          ).json();
          console.log(priceData);
          setLoading(false);
        })();
      }, []);
      return (
        <Container>
          <Header>
            <Title>{state?.name || "Loading"}</Title>
          </Header>
          {loading ? <Loader>Loading...</Loader> : null}
        </Container>
      );
    }
    
    export default Coin;

    styled-components의 DefaultTheme를 사용하여 테마를 사용할 수 있다

    • 미리 정해둔 색과 수치 사용으로 통일성 제공
    • theme를 변경하여 라이트 / 다크 모드 전환 등을 쉽게 변경 가능
    1. styled.d.ts 파일 생성
    // styled.d.ts
    import "styled-components";
    
    declare module "styled-components" {
      export interface DefaultTheme {
        bgColor: string;
        textColor: string;
        accentColor: string;
      }
    }
    2. theme.ts 파일 생성
    // theme.ts
    import { DefaultTheme } from "styled-components/dist/types";
    
    export const lightTheme: DefaultTheme = {
      bgColor: "#dfe4ea",
      textColor: "#2f3542",
      accentColor: "#ff4757",
    };
    
    export const darkTheme: DefaultTheme = {
      bgColor: "black",
      textColor: "white",
      accentColor: "black",
    };
    3. index.tsx 에 ThemeProvider 적용
    // index.tsx
    import React from "react";
    import ReactDOM from "react-dom/client";
    import App from "./App";
    import { ThemeProvider } from "styled-components";
    import { lightTheme } from "./theme";
    
    const root = ReactDOM.createRoot(
      document.getElementById("root") as HTMLElement
    );
    root.render(
      <ThemeProvider theme={lightTheme}>
        <App />
      </ThemeProvider>
    );
    4. theme props 적용1
    // App.tsx
    const GlobalStyle = createGlobalStyle`
      /* theme */
      body {
        background-color:${(props) => props.theme.bgColor};
        color: ${(props) => props.theme.textColor}
      }
    5. theme props 적용2
    // Coins.tsx
    import styled from "styled-components";
    import { Link } from "react-router-dom";
    
    const Container = styled.div`
      padding: 0px 20px;
    `;
    
    const Header = styled.header`
      height: 10vh;
      display: flex;
      justify-content: center;
      align-items: center;
    `;
    
    const CoinsList = styled.ul``;
    
    const Coin = styled.li`
      background-color: ${(props) => props.theme.textColor};
      color: ${(props) => props.theme.bgColor};
      border-radius: 15px;
      margin-bottom: 10px;
      a {
        padding: 20px;
        transition: color 0.15s 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",
      },
    ];
    
    function Coins() {
      return (
        <Container>
          <Header>
            <Title>Coin</Title>
          </Header>
          <CoinsList>
            {coins.map((coin) => (
              <Coin key={coin.id}>
                <Link to={`/${coin.id}`}>{coin.name} &rarr;</Link>
              </Coin>
            ))}
          </CoinsList>
        </Container>
      );
    }
    
    export default Coins;

    CSS reset 방법

    1. css 직접 적용하기 
    2. styled-reset 적용하기
    3. createGlobalStyle로 전체 문서에 스타일 적용하기
    ✅<></>로 리턴할 때, 가상의 묶음을 형성하여 쓸데없이 <div>를 생성하지 않을 수 있음
    import { createGlobalStyle } from "styled-components";
    import Router from "./Router";
    
    const GlobalStyle = createGlobalStyle`
      body {
        color:red;
      }
    `;
    
    function App() {
      return (
        <>
          <GlobalStyle />
          <Router />
        </>
      );
    }
    
    export default App;

    reset css를 적용한 코드

    // App.tsx
    import { createGlobalStyle } from "styled-components";
    import Router from "./Router";
    
    const GlobalStyle = createGlobalStyle`
      /* font */
      @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;600&display=swap');
      /* box-sizing border-box */
      * {
        box-sizing: border-box
      }
      /* http://meyerweb.com/eric/tools/css/reset/ 
        v2.0 | 20110126
        License: none (public domain)
      */
    
      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;
      }
      /* font */
      body {
        font-family: 'Noto Sans KR', sans-serif;
      }
    
      /* anchor(link) without underline */
      a {
        text-decoration: none;
      }
    
    
    `;
    
    function App() {
      return (
        <>
          <GlobalStyle />
          <Router />
        </>
      );
    }
    
    export default App;

    TypeScript

     

    JavaScript With Syntax For Types.

    TypeScript extends JavaScript by adding types to the language. TypeScript speeds up your development experience by catching errors and providing fixes before you even run your code.

    www.typescriptlang.org

    • JS를 기반으로 한 프로그래밍 언어
    • Strongly-typed 언어: 프로그래밍 언어가 작동하기 전에 데이터의 type을 먼저 확인
    JS는 type을 신경쓰지 않는다
    const plus = (a, b) => a + b;
    plus(2, 2);
    // 4
    plus(2, "hi");
    // '2hi'
    const user = {
      firstName: "Angela",
      lastName: "Davis",
      role: "Professor",
    };
    console.log(user.name);
    // undefined

     

    데이터의 타입을 제어할 수 있을까?

     

     

    const plus = (a: number, b: number) => a + b;

    설치

    처음부터 TypeScript로 프로젝트 시작하기

    create-react-app 관련문서

     

    Adding TypeScript | Create React App

    Note: this feature is available with react-scripts@2.1.0 and higher.

    create-react-app.dev

    npx create-react-app my-app --template typescript

    기존 프로젝트에 TypeScript 적용하기

    1. 아래 설치

    npm install --save typescript @types/node @types/react @types/react-dom @types/jest

    2. 확장자 변경

    • .js -> .tsx (리액트에서 사용할 경우)로 변경
    •  .js -> .ts 리액트가 아닐 경우

    3. tsconfig.json 파일 생성 및 수정

    npx tsc --init
    tsconfig.json 파일 수정
    // tsconfig.json
    {
      "compilerOptions": {
        "jsx": "react-jsx",
    ...

    4. index.tsx 파일 수정

    아래 오류 발생 시 
    TS2345: Argument of type 'HTMLElement | null' is not assignable to parameter of type 'Element | DocumentFragment'.
    Type 'null' is not assignable to type 'Element | DocumentFragment'.
    // index.tsx
    const root = ReactDOM.createRoot(
      document.getElementById("root") as HTMLElement
    );

    위 과정까지 한 경우 실행이 되는 것을 확인함

    5. @types/styled-components 설치

    npm i --save-dev @types/styled-components
    호환이 되지 않아 설치가 안될 경우, --legacy-peer-deps를 입력하면 무시하고 설치할 수 있다
    npm i --save-dev @types/styled-components --save --legacy-peer-deps

    @types?

    DefinitelyTyped Repository

     

    GitHub - DefinitelyTyped/DefinitelyTyped: The repository for high quality TypeScript type definitions.

    The repository for high quality TypeScript type definitions. - GitHub - DefinitelyTyped/DefinitelyTyped: The repository for high quality TypeScript type definitions.

    github.com

    유명한 npm package들을 TypeScript가 이해할 수 있도록 TypeScript Definition이 있는 repo

    🏋️‍♂️용어: how to TYPE

    component를 type한다 = component에 type을 추가한다 = TS한테 뭐가 뭔지 설명한다


    PropTypes vs TypeScript

    • PropTypes는 prop이 거기 있는지 없는지 확인해주지만, 코드를 실행한 "후"에 확인 가능
    • TypeScript는 코드 실행 "전"에 오류를 확인해줌

    interface

    interface는 object shape을 TS에게 설명해준다

    playerObj의 object shape을 설명한 간단한 예시
    interface PlayerShape {
      name: string;
      age: number;
    }
    
    const sayHello = (playerObj: PlayerShape) =>
      `Hello ${playerObj.name}! You are ${playerObj.age}`;
    
    sayHello({ name: "홍엽", age: 10 });
    Circle안에 들어가는 object shape을 CircleProps로 type하고,
    Circle이 리턴하는 Container의 object shape을 ContainerProps로 type 함
    // Circle.tsx
    import styled from "styled-components";
    
    interface ContainerProps {
      bgColor: string;
    }
    
    const Container = styled.div<ContainerProps>`
      width: 200px;
      height: 200px;
      border-radius: 100px;
      background-color: ${(props) => props.bgColor};
    `;
    
    interface CircleProps {
      bgColor: string;
    }
    
    function Circle({ bgColor }: CircleProps) {
      return <Container bgColor={bgColor} />;
    }
    
    export default Circle;
    // App.tsx
    import styled from "styled-components";
    import Circle from "./Circle";
    
    function App() {
      return (
        <div>
          <Circle bgColor="teal" />
          <Circle bgColor="tomato" />
        </div>
      );
    }
    
    export default App;

    optional한 값을 주고 싶은 경우 ?를 사용

    interface CircleProps {
      bgColor: string;
      borderColor?: string;
    }

    default한 값을 주고 싶을 경우 ??를 사용

    아래의 경우에 borderColor가 없을 경우 borderColor 값은 bgColor 값 입력
    function Circle({ bgColor, borderColor }: CircleProps) {
      return <Container bgColor={bgColor} borderColor={borderColor ?? bgColor} />;

    최종 예제 코드

    // Circle.tsx
    import styled from "styled-components";
    
    interface ContainerProps {
      bgColor: string;
      borderColor: string;
    }
    
    const Container = styled.div<ContainerProps>`
      width: 200px;
      height: 200px;
      border-radius: 100px;
      background-color: ${(props) => props.bgColor};
      border: 5px solid ${(props) => props.borderColor};
    `;
    
    interface CircleProps {
      bgColor: string;
      borderColor?: string;
      text?: string;
    }
    
    function Circle({ bgColor, borderColor, text = "default txt" }: CircleProps) {
      return (
        <Container bgColor={bgColor} borderColor={borderColor ?? bgColor}>
          {text}
        </Container>
      );
    }
    
    export default Circle;
    // App.js
    import styled from "styled-components";
    import Circle from "./Circle";
    
    function App() {
      return (
        <div>
          <Circle bgColor="teal" borderColor="coral" text="new text" />
          <Circle bgColor="tomato" />
        </div>
      );
    }
    
    export default App;

    useState

    • TypeScript는 초기값으로 type을 예측
    • 필요한 경우 옆에 타입을 명시함으로 여러 type을 사용할 수 있음
    const [counter, setCounter] = useState<number | string>(1);
    setCounter("hi");

    Form Event

    • 이벤트 타입의 명시 방법
    • 어떤 타입을 사용할지는 구글링해서 익숙해져야 함
    • event의 target을 currentTarget으로 사용하는 것을 확인할 수 있었음
    import { useState } from "react";
    import styled from "styled-components";
    
    function App() {
      const [value, setValue] = useState("");
      const onChange = (event: React.FormEvent<HTMLInputElement>) => {
        const {
          currentTarget: { value },
        } = event;
        setValue(value);
      };
      const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
        event.preventDefault();
        console.log(`hello ${value}`);
      };
      return (
        <div>
          <form onSubmit={onSubmit}>
            <input
              value={value}
              onChange={onChange}
              type="text"
              placeholder="username"
            />
            <button>Log in</button>
          </form>
        </div>
      );
    }
    
    export default App;

    theme 적용 방법

    npm i --save-dev @types/styled-components 설치 이후

    1. styled component 확장

    // styled.d.ts
    import "styled-components";
    
    // and extend them!
    declare module "styled-components" {
      export interface DefaultTheme {
        textColor: string;
        bgColor: string;
      }
    }

    2. theme.ts 생성

    // theme.ts
    import { DefaultTheme } from "styled-components/dist/types";
    
    export const lightTheme: DefaultTheme = {
      bgColor: "white",
      textColor: "black",
    };
    
    export const darkTheme: DefaultTheme = {
      bgColor: "black",
      textColor: "white",
    };

    3. index.tsx에서 theme 적용

    // index.tsx
    import React from "react";
    import ReactDOM from "react-dom/client";
    import App from "./App";
    import { ThemeProvider } from "styled-components";
    import { lightTheme } from "./theme";
    
    const root = ReactDOM.createRoot(
      document.getElementById("root") as HTMLElement
    );
    root.render(
      <ThemeProvider theme={lightTheme}>
        <App />
      </ThemeProvider>
    );

    4. App 내에서 theme 값 사용

    // App.tsx
    import { useState } from "react";
    import styled from "styled-components";
    
    const Container = styled.div`
      background-color: ${(props) => props.theme.bgColor};
    `;
    
    const H1 = styled.h1`
      color: ${(props) => props.theme.textColor};
    `;
    
    function App() {
      return (
        <div>
          <Container>
            <H1>test</H1>
          </Container>
        </div>
      );
    }
    
    export default App;

    + Recent posts