[TIL-0514] Axios로 API함수 만들기

jiny·2025년 5월 27일

캡스톤2

목록 보기
14/22

🌟 위키피디아 요약 정보를 받아오는 API 함수

지역 정보는 위키피디아 API를 사용하기로 결정하였다.
위키백과에 지역 이름(예: '치앙마이')을 검색했을 때 제일 위쪽에 나오는 설명을 가져오는 함수를 만들어서 지역 모달에 뿌려주기로 하였다.

Axios를 이용해 위키피디아 요약 정보를 받아오는 API 함수를 구현하면서 잘 몰랐던 내용들을 정리하기로 하였다.


✍️ 구현 예시

위키피디아 요약 정보를 가져오는 API 함수는 다음과 같이 작성하였다.

// information.api.ts
import axios from "axios";
import { PageData, WikipediaApiResponse } from "../types/pageData.type";

const WIKIPEDIA_URL = "https://ko.wikipedia.org";

export const getPlaceInformation = async (placeName: string) => {
  const url = `${WIKIPEDIA_URL}/w/api.php`;
  const params = {
    action: "query", // 문서 정보 조회
    prop: "extracts", // 문서의 요약(본문)을 가져옴
    format: "json", // JSON 형식
    origin: "*", // CORS 우회
    exintro: "true", // 첫 섹션만 가져오기
    titles: placeName, // 검색할 문서의 제목
  };
  
  try {
    const response = await axios.get(url, { params });
    const wikipediaData: WikipediaApiResponse = response.data;
    const data: PageData = Object.values(wikipediaData.query.pages)[0];
    const textData = data.extract;
    if (!textData) {
      console.warn(`[getPlaceInformation] ${placeName} 문서는 있지만 본문 없음`);
      return "설명 없음";
    }
    const result = `<div class="flex flex-col gap-y-[10px] leading-[1.6]">${textData}</div>`;
    return result;
  } catch (error) {
    if (axios.isAxiosError(error)) {
      if (error.response?.status === 404) {
          console.warn(`[getPlaceInformation] ${placeName}`에 대한 위키피디아 문서 없음);
      return "설명 없음",
      } else {
        console.error("[getPlaceInformation] Axios 에러: ", error.message);
      }
    } else {
      console.error("[getPlaceInformation] 일반 에러: ", error);
    }
    throw error;
  }
};

그리고 CityInfo라는 컴포넌트에서 이 함수를 호출하여 사용하고 있다.

// CityInfo.tsx
import { useQuery } from "@tanstack/react-query";
import { getPlaceInformation } from "../../apis/information.api";
import { WIKIPEDIA_SEARCH_WORD } from "../../constants/wikipedia";
import DOMPurify from "dompurify";

interface CityInfoProps {
  cardName: string;
}

const CityInfo = ({ cardName }: CityInfoProps) => {
  const city = cardName.split(" ")[1];
  const { data, error, isError } = useQuery<string, Error>({
    queryKey: ["cityInfo", city],
    queryFn: () => getPlaceInformation(WIKIPEDIA_SEARCH_WORD[city]), // 👈 API 함수
    staleTime: 60 * 60 * 1000, // 1시간 동안 fresh 상태로 유지
    gcTime: 2 * 60 * 60 * 1000, // 2시간 동안 캐시 유지 (garbage collection 대상 제외)
    refetchOnWindowFocus: false, // 윈도우 포커스 시 자동 refetch 비활성화
    refetchInterval: 10 * 60 * 1000, // 10분마다 자동 refetch (배경 refetch 포함)
  });
  
  // 받아온 HTML 문자열을 XSS 방지를 위해 정화
  const cleanHTML = DOMPurify.sanitize(data!);
  
  return (
    <div className="text-[14px] overflow-y-auto scrollbar-custom pr-[10px]">
      {/* 정제된 HTML을 dangerouslySetInnerHTML을 통해 렌더링 */}
      <div dangerouslySetInnerHTML={{ __html: cleanHTML }}></div>
    </div>
  );
};

🌟 try...catch문에 throw error를 안 쓰면?

  • throw error의 역할
    catch (error) {
      // 에러 처리 코드
      throw error; // 에러를 다시 바깥으로 전달(전파)
    }
    • throw error;현재 함수 바깥으로 에러를 전달
    • 즉, getPlaceInformation()을 호출한 쪽에서도 try...catch로 이 에러를 감지할 수 있음
  • throw error를 안 쓰면 어떻게 되나?
    catch (error) {
      // 에러를 콘솔에만 출력하고 끝
      console.error("에러 발생", error);
      // throw error;
    }
    • 에러가 더 이상 바깥으로 전파되지 않음
    • 호출한 쪽에서는 에러가 발생하지 않은 것처럼 보임
    • 예상치 못한 결과(예: undefined 반환, 이후 코드가 정상 실행된다고 착각)가 생길 수 있음
  • 예시 비교

    • getPlaceInformation()에서 throw error가 있는 경우

      • 코드

        export const getPlaceInformation = async (placeName: string) => {
          try {
            // 에러가 발생할 가능성이 있는 코드 (정상 동작 시도)
          } catch (error) {
            // 에러가 실제로 발생했을 때 실행할 코드 (에러 처리)
            throw error; // 무조건 던짐
          }
        };
      • 결과

        항목
        dataundefined
        errorError 객체 (예: AxiosError)
        isErrortrue
      • 설명

        • 에러가 throw 되어서 useQuery는 실패로 인식함
        • data는 설정되지 않음
        • error.message에 에러 메시지 있음
    • getPlaceInformation()에서 throw error가 없는 경우 (모든 에러 fallback 처리)

      • 코드

        export const getPlaceInformation = async (placeName: string) => {
          try {
            // 에러가 발생할 가능성이 있는 코드 (정상 동작 시도)
          } catch (error) {
            // 에러가 실제로 발생했을 때 실행할 코드 (에러 처리)
            return "설명 없음"; // fallback 처리
          }
        };
      • 결과

        항목
        data"설명 없음"
        errorundefined
        isErrorfalse
      • 설명

        • 에러가 발생했지만 throw 하지 않고 fallback 값을 리턴
        • useQuery 입장에서는 성공으로 간주
        • 에러 표시나 예외 흐름이 동작하지 않음
  • 결론

    상황throw error 필요 여부
    호출한 쪽에서 에러를 처리하고 싶다필요함
    함수 내부에서 에러를 완전히 처리했다 (예: fallback 리턴)없어도 됨
  • 요약

    • throw error에러를 바깥으로 전파
    • 없으면 에러가 조용히 묻혀서 디버깅이 어려워질 수 있음
    • "내가 책임지고 처리하겠다"는 케이스가 아니라면 → throw를 유지하는 게 좋음
  • return도 하고 throw error도 하면?

    • return을 한 다음 throw error를 써도 throw는 실행되지 않음
    • 왜냐하면 return은 그 함수의 실행을 즉시 종료시키기 때문임

🌟 throw error가 필요 없는 경우

함수 안에서 에러가 발생했지만,

  • 사용자에게 보여줄 적절한 대체값(fallback)을 정해뒀고,
  • fallback 값을 그 자리에서 return 해줬기 때문에,
  • 에러를 굳이 바깥으로 던질(throw) 필요가 없는 경우

➡️ 즉, 내부에서 문제를 감지하고 스스로 처리한 경우

  • 예시 상황: 위키피디아에서 장소 요약을 가져오는데 문서가 없는 경우

    export const getPlaceInformation = async (placeName: string) => {
      try {
       // ... 생략
      } catch (error) {
        // ✅ 여기서 에러를 완전히 처리
        console.warn(`[getPlaceInformation] '${placeName}' 요약을 가져올 수 없습니다.`);
        return "설명 없음"; // 👈 fallback 값을 리턴함
      }
    };
    • 이 경우에는 throw error가 필요 없음
      • 에러는 이미 console.warn()으로 로그 남겼고,
      • 사용자에게는 "설명 없음"이라는 대체 문자열을 보여주기로 했기 때문임
  • 반대 예시: fallback 리턴 없이 처리도 안 한 경우

    export const getPlaceInformation = async (placeName: string) => {
      try {
        // ... 생략
      } catch (error) {
        console.error("에러 발생!");
        // 여기서 아무것도 return 하지 않음
        // throw error도 하지 않음
        // 🚨 호출한 쪽은 에러 발생 사실을 모름
      }
    };
    • 여기서는 에러가 생겼지만 아무 fallback도 없고, throw도 안 했으니 호출한 쪽에서는 무슨 일이 일어났는지 모름
  • 언제 fallback 리턴하면 좋을까?

    • 문서가 없을 땐 → "설명 없음" 리턴
    • 네트워크 문제일 땐 → "인터넷 연결을 확인해주세요" 리턴
    • 사용자에게 기본값을 보여주면 되는 경우

🌟 fallback이란?

원래 보여주려는 콘텐츠가 아직 준비되지 않았거나 문제가 있을 때 임시로 보여주는 콘텐츠

  • 사용 상황

    1. React의 <Suspense>
      비동기 컴포넌트가 아직 로딩 중일 때 대신 보여주는 UI
      <Suspense fallback={<div>로딩 중...</div>}>
        <MyComponent />
      </Suspense>
      ➡️ MyComponent가 데이터를 받아오는 동안 <div>로딩 중...</div>이 대신 보여짐
    2. Next.js의 ErrorBoundary 혹은 notFound, loading.tsx 등에서
      컴포넌트가 에러가 났을 때 혹은 404일 때 대신 보여줄 수 있음
      <ErrorBoundary fallback={<div>에러 발생!</div>}>
        <MyComponent />
      </ErrorBoundary>
      ➡️ 내부에서 오류가 나면 fallback 컴포넌트가 화면에 나타남
  • 실제 상황 예시 (Suspense + React Query)

    import { Suspense } from "react";
    import { useQuery } from "@tanstack/react-query";
    
    const UserInfo = () => {
      const { data } = useSuspenseQuery({
        queryKey: ["user"],
        queryFn: fetchUserData.
      });
      
      return <div>{data.name}님 환영합니다!</div>;
    };
    
    const App = () => {
      <Suspense fallback={<div>사용자 정보를 불러오는 중입니다...</div>}>
        <UserInfo />
      </Suspense>
    };
    • UserInfo는 서버에서 데이터를 받아올 때까지 시간이 걸림
    • useSuspenseQuery는 로딩/에러 상태를 반환하지 않고, 항상 성공적으로 로드된 데이터만 제공
    • 로딩 중에는 <Suspense>의 fallback이, 에러 발생 시에는 <ErrorBoundary>의 fallback이 자동으로 보여짐
    • fallback={<div>...}에 있는 "로딩 중입니다" 문구가 먼저 보임
    • 데이터가 다 오면 원래 컴포넌트(UserInfo)가 보임
  • fallback은 왜 필요한가?

    이유설명
    사용자 경험 향상아무것도 안 보이는 상태보다 "로딩 중..."이라도 보여주는 게 훨씬 친절함
    에러 대비문제 생겨도 완전 흰 화면 대신 안내 메시지나 대체 콘텐츠 제공
    로딩 시간 숨김페이지가 버벅이거나 렌더링 지연될 때, 기다리는 느낌을 줄여줌

🌟 에러 케이스 분류

catch (error) {
  if (axios.isAxiosError(error)) {
    if (error.response?.status === 404) {
        // 케이스 1
        return "설명 없음";
    } else {
      // 케이스 2
      console.error("Axios 에러");
    }
  } else {
    // 케이스 3
    console.error("일반 에러");
  }
}
  • 케이스 1: AxiosError + 404 Not Found
    if (axios.isAxiosError(error)) {
      if (error.response?.status === 404) {
          console.warn(
             `[getPlaceInformation] ${placeName}에 대한 위키피디아 문서 없음`
           );
           return "설명 없음";
      }
    }
    • 정상적인 상황 중 하나로 간주함 (예: 서울은 있지만 '헝가리 강남구'는 없음)
    • 그래서 fallback 값을 반환해 사용자에게 보여주고 끝냄 → throw 안 함
  • 케이스 2: AxiosError + 그 외 오류 (500, 403 등)
    } else {
       console.error("[getPlaceInformation] Axios 에러: ", error.message);
    }
    • 위키 서버 오류(500), 권한 없음(403), 네트워크 오류 등
    • 개발자가 확인할 수 있도록 콘솔에 로그 남김
    • 이 경우는 심각한 문제일 수 있으므로throw 하는 게 좋음
  • 케이스 3: Axios가 아닌 일반 에러
    else {
         console.error("[getPlaceInformation] 일반 에러: ", error);
       }
    • 예: 코드 오류, JSON 파싱 실패, null 접근 등
    • Axios와 무관한 예외이므로 별도로 분리
    • 이것도 throw해서 상위에서 잡는 게 일반적

🌟 error instanceof AxiosError vs. axios.isAxiosError(error)

  1. error instanceof AxiosError

    import { AxiosError } from "axios";
    
    if (error instanceof AxiosError) {
      // Axios에서 발생한 오류 처리
    }
    • 작동 방식: instanceof는 JavaScript의 프로토타입 체인을 따라 errorAxiosError 클래스의 인스턴스인지 확인

    • 문제점

      • AxiosErrorAxios 패키지 내부에서만 정의된 클래스이므로, 버전이나 빌드 환경에 따라 instanceof가 작동하지 않을 수 있음
      • 특히 다음 상황에서는 실패할 수 있음
        • 여러 버전의 Axios가 프로젝트에 존재할 경우
        • Axios가 ESM(CommonJS) 호환이 맞지 않을 경우
        • error 객체가 JSON.stringify로 직렬화됐다가 다시 복원된 경우
  1. axios.isAxiosError(error)

    import axios from "axios";
    
    if (axios.isAxiosError(error)) {
      // Axios 오류 처리
    }
    • 작동 방식

      • 내부적으로 error.isAxiosError === true를 체크함
      • Axios가 던지는 에러에는 자동으로 isAxiosError: true가 붙어 있기 때문에 이 메서드로 확실히 식별할 수 있음
    • 장점

      • 안정성: 여러 버전/환경에서도 잘 작동함
      • 타입 보장: errorAxiosError로 좁혀줌
      • 유지보수: Axios 공식 권장 방식임
      • 신뢰성: 객체가 직렬화됐다가 복원돼도 인식이 가함
  • error instanceof AxiosError는 안 되는 이유

    • 다중 번들 / 중복 로딩 / 패키지 중복 버전 문제로 인해 런타임에 AxiosError가 서로 다른 클래스 인스턴스로 인식될 수 있기 때문
      ➡️ 그래서 공식 문서에서도 안전하게 axios.isAxiosError(error)를 사용하라고 권장함
  • axios.isAxiosError()가 더 좋은 이유

    • Cross-Version 안정성: instanceof는 다른 버전의 Axios가 사용되면 실패할 수 있지만, isAxiosError는 상관없음
    • 트랜스파일러/번들러 호환성: Webpack, Babel, Vite 등 빌드 환경에 상관없이 작동함
    • 공식 지원 방식: Axios 공식 문서에서 axios.isAxiosError()를 권장함
    • 명확한 타입 내로잉 지원: TypeScript에서 axios.isAxiosError()error 타입을 AxiosError로 좁혀주기 때문에 코드 작성 시 더욱 안전하고 자동 완성이 잘 됨

🌟 다양한 콘솔 메서드

  • 전체 콘솔 메서드 비교표

    메서드용도콘솔 표시 색상사용 예시표시 필터
    console.log()일반 로그기본(흰색)디버깅, 값 확인"All", "Logs"
    console.info()정보 메시지ℹ️ 아이콘 (색상은 브라우저마다 다름)앱 상태, API 응답 등"Info"
    console.warn()경고노란색 ⚠️잠재적 문제"Warnings"
    console.error()오류빨간색 ❌예외, 실패, 에러"Errors"
    console.debug()디버그 전용회색 또는 숨겨짐상세 디버깅 정보"Verbose" (숨김일 수 있음)
    console.trace()호출 스택 출력기본 + 스택트레이스함수 호출 경로 추적"Logs" 또는 "Trace"
  • 각 메서드 상세 설명

    1. console.log()

      • 가장 일반적인 출력 함수
      • 사용 목적: 값, 상태, 흐름 등을 확인
      • 예시
        console.log("현재 위치: ", location.pathname);
      • 결과
    2. console.info()

      • 정보 전달용 메시지
      • log()와 기능상 차이는 거의 없음
      • 브라우저에 따라 ℹ️ 아이콘이 붙거나 색이 달라질 수 있음
      • 예시
        console.info("API 요청 완료. 처리 중...");
      • 결과
    3. console.warn()

      • 경고 메시지 출력
      • 노란색 아이콘/배경으로 시각적 강조
      • 예시
        console.warn("필수 입력값이 비어있습니다.");
      • 결과
    4. console.error()

      • 오류 메시지 출력
      • 빨간색으로 표시되어 가장 눈에 띔
      • 에러 추적, 운영 로그 등에 중요
      • 예시
        console.error("서버 응답 오류:", err);
      • 결과
    5. console.debug()

      • 디버깅용 상세 로그
      • 기본적으로 콘솔에 표시되지 않을 수도 있음 (필터 설정 필요)
      • Chrome에서는 기본적으로 숨겨져 있어서, 콘솔 우측 상단 필터에서 "Verbose"를 켜야 보임
      • 개발 환경에서만 켜 두는 게 일반적임
      • 예시
        console.debug("useEffect 내부 상태 변화: ", pathname);
      • 결과

    6. console.trace()

      • 현재 코드가 어디서 호출됐는지 스택 트레이스를 출력
      • 디버깅 중 함수 호출 경로를 추적할 때 유용함
      • 예시
        function testTrace() {
          console.trace("이 함수는 어디서 호출됐을까?");
        }
        testTrace();
      • 결과

0개의 댓글