HTTP 프로토콜

서버 쪽에 저장된 웹 표준 데이터(HTML, JavaScript, CSS)를 브라우저(클라이언트)로 받아오는 것이 HTTP 프로토콜

  • Hyper Transfer Protocol 
  • www에서 쓰이는 핵심 프로토콜로 문서의 전송을 위해 쓰이며, 오늘 날 거의 모든 웹 애플리케이션에서 사용 (음성, 화상 등 여러 종류의 데이터를 MIME로 정의하여 전송 가능)
  • Request / Response 동작에 기반하여 서비스 제공

 

 HTTP 1.0

  • 연결 수립, 동작, 연결 해체 단순함이 특징 (하나의 URL은 하나의 TCP 연결)
  • HTML 문서를 전송 받은 뒤, 연결을 끊고 다시 연결하여 데이터를 전송
  • 단순 동작 (연결 수립, 동작, 연결 해체)이 반복되어 통신 부하

HTTP 1.0은 데이터를 하나 받을 때마다 다시 연결

HTTP 1.1

한 번 연결할 경우, 필요한 데이터를 다 받은 다음 연결 종료


HTTP 요청 프로토콜

요청하는 방식을 정의하고 클라이언트 정보를 담고 있는 요청 프로토콜 구조

Request Line

  • 요청 타입
    • GET: 데이터를 서버에 보낼 때, URI(주소창)에 포함하여 보냄
    • POST: 데이터를 서버에 보낼 때, body에 포함하여 보냄

  • URI(Uniform Resource Identifier): 주소 전체; 인터넷 상에서 특정 자원(파일)을 나타내는 유일한 주소
    • scheme: 프로토콜의 형식로 이해하자
    • IP주소 [:포트]
      • 보통 도메인 주소 사용 (컴퓨터가 도메인 주소를 IP 주소로 바꿔줌) 
      • 포트 번호는 http 80번 / https 443번으로 자동으로 입력
    • /폴더이름/파일이름: 서버 쪽의 파일(프로그램)의 경로로 이해하자
    • ?query: 앞의 파일(프로그램)에 입력해주는 값


HTTP 응답 프로토콜

 

사용자가 볼 웹 페이지를 담고 있는 응답 프로토콜 구조

  • 200: client  요청 성공
  • 403: client가 권한이 없는 페이지 요청
  • 404: client가 서버에 없는 페이지를 요청
  • 500: server가 멈춘 경우
  • 504: 최대 session 수가 초과

HTTP 헤더 포맷

요청 헤더

클라이언트 정보를 담고 있는 요청 헤더

  • Content-Length: 메세지 바디 길이를 나타냄
  • Content-Type: 메세지 바디에 들어있는 컨텐츠 종류 (HTML 문서는 text/html )
  • Cookie: 서버로부터 받은 쿠키를 다시 서버에 보내주는 역할
  • Host: 요청된 URL에 나타난 호스트명을 상세하게 표시 (HTTP 1.1에서 필수)
  • User-Agent: client program에 대한 식별 가능 정보 제공

 응답 헤더

서버 정보를 담고 있는 응답 헤더

  • Server: 사용하고 있는 웹 서버의 소프트웨어에 대한 정보를 포함
  • Set-Cookie: 쿠키를 생성하고 브라우저에 보낼 때 사용, 해당 쿠키 값을 브라우저가 서버에게 다시 보낼 때 사용

 

 

 


 

실습

Burp Suite로 http 응답 intercept 해보기

Burp Suite는 요청과 응답을 중간에 가로채서 수정할 수 있게 해준다!
Burp Suite community edtion 설치

Burp Suite 설치 사이트

 

Professional / Community 2023.9.3

This release upgrades Burp's built-in browser and fixes a bug when scanning GraphQL APIs. Browser upgrade We have upgraded Burp's built-in browser to 116.0.5845.110 for Mac and Linux and 116.0.5845.11

portswigger.net

프로젝트 생성 신경 쓰지 않고 다음으로 진행 후
Proxy 탭의 Proxy settings 이동

Proxy settings에서 Intercept requests와 Intercept responses 체크

chrome extension: Proxy SwitchyOmega 설치

chrome store: Proxy SwitchyOmega

 

Proxy SwitchyOmega

Manage and switch between multiple proxies quickly & easily.

chrome.google.com

아래와 Option에서 설정해준다.
✅참고: 127.0.0.x 는 자기자신을 뜻하는 주소

Proxy SwitchOmega Proxy 설정 / Burp Suite intercept on 설정

⛔주의: Proxy SwitchOmega Proxy와 Burp Suite intercept 설정의 주소와 포트가 일치해야 함 

http 응답을 intercept하고 싶은 사이트에 접속해본다

Proxy SwitchOmega Proxy에서 Forward 버튼을 눌러서 조작하고 싶은 response가 보일 때까지 Forward를 누른다

response 페이지에서 우클릭과 관련된 rightClickOpenYn 변수를 true로 변경한다

받은 페이지에서 우클릭이 되는 것을 확인할 수 있다


참고자료

https://www.youtube.com/watch?v=TwsQX1AnWJU&list=PL0d8NnikouEWcF1jJueLdjRIC4HsUlULi&index=28 

위 강의를 보고 정리한 글입니다.

 

목차

    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;

    + Recent posts