오늘도 코딩하나

[React] CRYPTO TRACKER #1 - useLocation, Nested Router, React-Query 본문

React/노마드코더

[React] CRYPTO TRACKER #1 - useLocation, Nested Router, React-Query

오늘도 코딩하나 2025. 2. 18. 19:29
강의 내용: 강의 링크
#5.0 ~ #5.11

 

페이지간 데이터 전달 기능

 

  ➰ useLocation

  • 현재 URL 정보를 가져오는 hook
  • pathname, search, hash, state, key 등의 정보 제공
<Link to={`/${coin.id}`} state={{ name: coin.name }}>
    <Img
      src={`https://cryptocurrencyliveprices.com/img/${coin.id}.png`}
    />
    {coin.name} &rarr;
</Link>

const { state } = useLocation();

<Title>
  {state?.name ? state.name : loading ? "Loading..." : infoData?.name}
</Title>
  • useLocation().state를 사용하면 URL에 보이지 않는 방식으로 데이터를 전달할 수 있음
    ⇒ 비하인드더씬
  • /${coin.id} 페이지로 이동하면서 state 값을 함께 전달
  • useLocation().state에서 전달된 값을 받아서 사용

 

중첩 라우트

 

  ➰ nested router

  • 부모 경로 안에 자식 경로를 중첩해서 정의하는 방식
  • route안에 있는 또 다른 route
    (tab or 스크린 안에 많은 섹션이 나뉘어진 곳)
  • 페이지 내부에서 페이지 이동없이 또 다른 페이지에 방문할 수 있게 해줌

- Router.tsx

<Route path="/:coinId" element={<Coin />}>
  <Route path="price" element={<Price />} />
  <Route path="chart" element={<Chart />} />
</Route>

 

- Coin.tsx

const priceMatch = useMatch("/:coinId/price");
const chartMatch = useMatch("/:coinId/chart");
  
<Tabs>
    <Tab isActive={chartMatch !== null}>
      <Link to={`/${coinId}/chart`}>Chart</Link>
    </Tab>
    <Tab isActive={priceMatch !== null}>
      <Link to={`/${coinId}/price`}>Price</Link>
    </Tab>
</Tabs>

<Outlet />
  • useMatch : 현재 URL이 특정 패턴과 일치하는지 확인하는 Hook ( 특정 경로에 있는지 여부 판단 )

 

React-Query

 

  ➰ react-query

  • 서버 상태 관리 라이브러리
  • fetching, caching, synchronizing, updating을 도와주는 라이브러리

   ▶ 기존 방식: useState + useEffect

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);
    })();
  }, []);
  • setLoading(false)  : 로딩 상태를 직접 관리해야함
  • 데이터 캐싱 및 리페칭 기능 없음
    매번 컴포넌트가 마운트될 때마다 API 요청이 발생함(Loading...)
  • 코드가 길어지고 가독성이 떨어짐

    ▶ react-query 방식

import { useQuery } from "@tanstack/react-query";
const { isLoading, data } = useQuery<ICoin[]>({
    queryKey: ["allCoins"],
    queryFn: fetchCoins,
  });
  • 로딩 상태를 자동 관리
    별도의 useState 필요 없이 isLoading 값이 자동으로 업데이트됨
  • 코드가 간결하고 가독성이 좋음
  • 데이터 캐싱 및 리페칭 지원
    동일한 요청에 대해 자동 캐싱, 브라우저 포커스 변경 시 자동 리페칭 가능
    → 데이터를 저장했다가 필요한 시점에 자동으로 갱신하는 것!
  • API 요청을 최소화
    같은 요청을 반복해서 보내지 않도록 캐시에서 데이터를 제공
    → 불필요한 로딩이 발생하지 X
  • fetcher함수 : API를 fetch 하고 json을 return하는 함수

- api.ts

const BASE_URL = `https://api.coinpaprika.com/v1`;
export function fetchCoins() {
  // json data의 Promise를 return해야함
  return fetch(`${BASE_URL}/coins`).then((response) => response.json());
}

export function fetchCoinInfo(coinId: string) {
  return fetch(`${BASE_URL}/coins/${coinId}`).then((response) =>
    response.json()
  );
}

export function fetchCoinTickers(coinId: string) {
  return fetch(`${BASE_URL}/tickers/${coinId}`).then((response) =>
    response.json()
  );
}

 

- Coin.tsx

  const { coinId } = useParams();
  
  const { isLoading: infoLoading, data: infoData } = useQuery<IInfoData>({
    queryKey: ["info", "coinId"],
    queryFn: () => fetchCoinInfo(coinId),
  });
  const { isLoading: tickersLoading, data: tickersData } = useQuery<PriceData>({
    queryKey: ["tickers", "coinId"],
    queryFn: () => fetchCoinTickers(coinId),
  });
  • queryKey는 캐싱을 위한 고유 식별자 역할을 한다.
  • 같은 coinId를 사용해도 "info"는 코인 기본 정보, "tickers"는 가격 정보를 의미하도록 데이터 유형을 구분한다.

❓ ❓ 근데 오류가 발생했다. ❓ ❓

Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
Type 'undefined' is not assignable to type 'string'.
  • TypeScript의 "Type Incompatibility Error" (타입 불일치 오류) 
  const { coinId } = useParams();
  
  const { isLoading: infoLoading, data: infoData } = useQuery<IInfoData>({
    queryKey: ["info", "coinId"],
    queryFn: () => fetchCoinInfo(coinId as string),
    enabled: !!coinId,
  });
  const { isLoading: tickersLoading, data: tickersData } = useQuery<PriceData>({
    queryKey: ["tickers", "coinId"],
    queryFn: () => fetchCoinTickers(coinId as string),
    enabled: !!coinId,
  });
  • coinIdstring|undefined로 되어있어 타입 오류가 발생했다.
    여러가지 방법이 있는데 나는 as string, enabled를 적용시켜줬다.
  • as string
    → TypeScript의 타입 에러 방지
    → coinId를 강제로 string 취급, 하지만 coinId가 실제로 undefined일 경우 런타임 에러 발생
        (fetchCoinInfo(undefined) → API 요청 실패)
  • enabled: !!coinId
    → 실제 실행 방지
    → coinId가 undefined일 때 queryFn 자체가 실행되지 않음
    * !! (Boolean 변환 연산자) : Falsy한 값은 false로, Truthy한 값은  true로 변환되며, 한 번 변환된 값은 바뀌지 않는다.

 

🔎 TypeScript 타입 체크

   1. 변수 선언 시

const { coinId } = useParams();  // coinId: string | undefined
  • TypeScript는 여기서는 coinIdstring | undefined 라는 타입 정보만 할당
  • 실제 값이 string인지 undefined인지까지 체크하지 않음
    ⇒ 이 단계에서는 에러 발생 ❌

   2. 함수 실행 시

queryFn: () => fetchCoinInfo(coinId),  // ❌ TypeScript 오류 발생
  • fetchCoinInfo는 string을 받아야 하는데, coinId는 string | undefined
  • TypeScript는 coinId가 undefined일 가능성을 감지하고 에러 발생!

  📌 TypeScript는 변수를 선언할 때는 "이 변수가 가질 수 있는 모든 타입"을 기록해 두고,
        이 변수를 사용할 때 그 타입이 안전한지 확인하는 방식


 

🔎 coinId type error 해결방법

       강의 댓글창에서 사람들이 다양한 방법으로 type error를 해결했고, 그 방법을 정리해봤다.

 

   1. as string 👉 (가장 추천)

fetchCoinTickers(coinId as string);
  • coindId가 undefined일 경우 런타임 오류 발생
  • TypeScript의 타입 체크를 무시하기 때문에 안전하지 않음
    👉 단독으로 쓰는 것은 위험하지만, enabled: !!coinId와 함께 쓰면 안전해짐!

   2. string | undefined 

function fetchCoinTickers(coinId: string | undefined) {...}
  • 함수 내부에서 undefined를 체크할 수 있어 안전함
  • 하지만 매번 if(!coinId) return; 같은 체크 로직을 작성해야함
    없으면, undefined가 그대로 전달되어 잘못된 API 요청이 될 수 있음
  • coinId를 받기 전에 아예 undefined가 안들어오도록 타입을 보장하는게 더 깔끔한 해결책임

   3. ${coinId} 👉 (비추천)

fetchCoinTickers(`${coinId}`);
  • undefined일 경우 "undefined"(문자열)로 변환되므로 TypeScript 오류 없음
  • API 요청이 fetchCoinTickers("undefined")처럼 잘못된 값으로 가게 됨