background
TypeScript3분

axios 기반 라이브러리 제작 - 2 npm 배포

2025년 8월 3일

React Easy API

음 axios로 쉽게 통신 할 수 있도록 라이브러리를 만들어봤는데 주요 기능에 대한 내용을 정리해보았다.


📋 목차

  1. 개요
  2. 주요 특징
  3. 설치 및 설정
  4. 기본 사용법
  5. 고급 사용법
  6. API 레퍼런스
  7. 실전 예제
  8. 베스트 프랙티스
  9. 트러블슈팅
  10. FAQ

🔍 개요

react-easy-api는 React 애플리케이션에서 Axios를 사용한 API 요청을 간편하게 처리할 수 있도록 도와주는 커스텀 훅 라이브러리이다. TypeScript를 완벽 지원하며, 로딩 상태 관리, 에러 핸들링, 응답 데이터 변환 등의 기능을 제공한다.

🎯 해결하는 문제

  • 반복적인 상태 관리 - loading, error, data 상태를 자동으로 관리
  • 일관된 에러 처리 - 통합된 에러 형식 제공
  • 타입 안정성 - TypeScript 완벽 지원
  • 코드 중복 제거 - 재사용 가능한 API 훅 생성
  • 유연한 설정 - 런타임 요청 설정 변경 가능

📊 프로젝트 정보

항목내용
패키지명react-easy-api
버전1.0.0
라이센스KimByeongNyeon
작성자KimByeongNyeon
GitHubKimByeongNyeon/react-easy-api
언어TypeScript
프레임워크React

⭐ 주요 특징

🔧 기술적 특징

  • TypeScript 완벽 지원: 강력한 타입 안정성과 IntelliSense 지원
  • React Hooks 기반: 최신 React 패턴 적용
  • Axios 통합: 업계 표준 HTTP 클라이언트 사용
  • ESM & CommonJS 지원: 다양한 번들러 환경에서 사용 가능
  • Zero Dependencies: React와 Axios 외 추가 의존성 없음

🎨 개발자 경험

  • 직관적인 API: 학습 곡선이 낮은 간단한 인터페이스
  • 풍부한 예제: 다양한 사용 사례 제공
  • 상세한 문서: 완전한 API 레퍼런스
  • TypeScript 지원: 컴파일 타임 타입 체킹

🚀 성능 특징

  • 자동 메모이제이션: useCallback으로 불필요한 리렌더링 방지
  • 경량화: 최소한의 번들 사이즈
  • 트리 쉐이킹: 사용하지 않는 코드 자동 제거

📦 설치 및 설정

1. 패키지 설치

bash
# npm 사용
npm install react-easy-api axios react

# yarn 사용
yarn add react-easy-api axios react

# pnpm 사용
pnpm add react-easy-api axios react

2. 필수 요구사항

패키지최소 버전설명
React≥16.8.0Hooks 지원 버전
Axios≥0.21.0HTTP 클라이언트

3. TypeScript 설정 (선택사항)

json
// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "skipLibCheck": true,
    "esModuleInterop": true
  }
}

🚀 기본 사용법

1. Axios 인스턴스 설정

typescript
// api/client.ts
import axios from 'axios';

export const apiClient = axios.create({
  baseURL: '<https://api.example.com>',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// 요청 인터셉터
apiClient.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 응답 인터셉터
apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // 토큰 만료 처리
      localStorage.removeItem('token');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

2. 기본 GET 요청

typescript
import React, { useEffect } from 'react';
import { useApi } from 'react-easy-api';
import { apiClient } from './api/client';

interface User {
  id: number;
  name: string;
  email: string;
}

function UserList() {
  const { loading, error, data, execute } = useApi<User[]>({
    endpoint: '/users',
    method: 'GET',
    axiosInstance: apiClient,
  });

  useEffect(() => {
    execute();
  }, [execute]);

  if (loading) return <div>로딩 중...</div>;
  if (error) return <div>에러: {error.message}</div>;
  if (!data) return <div>데이터가 없습니다</div>;

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>
          {user.name} - {user.email}
        </li>
      ))}
    </ul>
  );
}

3. POST 요청

typescript
interface CreateUserPayload {
  name: string;
  email: string;
}

function CreateUser() {
  const { loading, error, data, execute } = useApi<User, CreateUserPayload>({
    endpoint: '/users',
    method: 'POST',
    axiosInstance: apiClient,
    onSuccess: (user) => {
      console.log('사용자 생성 성공:', user);
      // 성공 후 처리 로직
    },
    onError: (error) => {
      console.error('사용자 생성 실패:', error);
      // 에러 처리 로직
    },
  });

  const handleSubmit = async (formData: CreateUserPayload) => {
    await execute(formData);
  };

  return (
    <div>
      {/* 폼 컴포넌트 */}
      <button onClick={() => handleSubmit({name: '김철수', email: 'kim@example.com'})} disabled={loading}>
        {loading ? '생성 중...' : '사용자 생성'}
      </button>
      {error && <p style={{ color: 'red' }}>에러: {error.message}</p>}
      {data && <p>생성된 사용자: {data.name}</p>}
    </div>
  );
}

🔥 고급 사용법

1. 응답 포맷터 사용

typescript
import { useApi, responseFormatters } from 'react-easy-api';

// 표준 API 응답 형식
interface ApiResponse<T> {
  data: T;
  message: string;
  success: boolean;
}

function UserListWithFormatter() {
  const { data, execute } = useApi<ApiResponse<User[]>, void, User[]>({
    endpoint: '/users',
    axiosInstance: apiClient,
    responseFormatter: responseFormatters.standard, // data 필드만 추출
  });

  // 이제 data는 User[] 타입입니다 (ApiResponse<User[]>가 아닌)
}

// 커스텀 응답 포맷터
function UserListWithCustomFormatter() {
  const { data, execute } = useApi<User[], User[]>({
    endpoint: '/users',
    axiosInstance: apiClient,
    responseFormatter: (rawData: User[]) => {
      return rawData
        .filter(user => user.email.includes('@company.com'))
        .map(user => ({
          ...user,
          displayName: `${user.name} (${user.email})`,
        }));
    },
  });
}

2. 동적 요청 설정

typescript
import { RequestConfig } from 'react-easy-api';

function DynamicUserSearch() {
  const [searchTerm, setSearchTerm] = useState('');
  const [page, setPage] = useState(1);

  const { loading, error, data, execute } = useApi<User[]>({
    endpoint: '/users', // 기본 엔드포인트
    axiosInstance: apiClient,
  });

  const handleSearch = () => {
    const config: RequestConfig = {
      url: '/users/search', // 다른 엔드포인트 사용
      params: {
        q: searchTerm,
        page: page,
        limit: 10,
      },
      headers: {
        'X-Search-Type': 'advanced',
      },
      timeout: 15000, // 더 긴 타임아웃
    };

    execute(undefined, config);
  };

  return (
    <div>
      <input
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="사용자 검색..."
      />
      <button onClick={handleSearch} disabled={loading}>
        검색
      </button>
    </div>
  );
}

3. createApiHook을 사용한 재사용 가능한 훅

typescript
import { createApiHook } from 'react-easy-api';

// 사용자 API용 베이스 훅 생성
const useUserApi = createApiHook<User>({
  method: 'GET',
  axiosInstance: apiClient,
  enableLogging: true,
  onError: (error) => {
    // 공통 에러 처리
    if (error.status === 404) {
      console.log('사용자를 찾을 수 없습니다');
    }
  },
});

// 특정 사용자 정보 가져오기
function UserProfile({ userId }: { userId: number }) {
  const { loading, error, data, execute } = useUserApi(`/users/${userId}`);

  useEffect(() => {
    execute();
  }, [execute, userId]);

  // 컴포넌트 렌더링...
}

// 사용자 목록 가져오기 (추가 옵션 포함)
function UserManagement() {
  const { loading, error, data, execute } = useUserApi('/users', {
    responseFormatter: (users: User[]) => users.filter(u => u.active),
    onSuccess: (users) => {
      console.log(`${users.length}명의 활성 사용자를 찾았습니다`);
    },
  });
}

4. 에러 처리 및 재시도

typescript
function RobustApiCall() {
  const [retryCount, setRetryCount] = useState(0);
  const maxRetries = 3;

  const { loading, error, data, execute, reset } = useApi<User[]>({
    endpoint: '/users',
    axiosInstance: apiClient,
    onError: (error) => {
      console.error('API 에러:', error);

      // 특정 에러에 대한 자동 재시도
      if (error.code === 'NETWORK_ERROR' && retryCount < maxRetries) {
        setTimeout(() => {
          setRetryCount(prev => prev + 1);
          execute();
        }, 1000 * (retryCount + 1)); // 지수 백오프
      }
    },
    onSuccess: () => {
      setRetryCount(0); // 성공시 재시도 카운트 리셋
    },
  });

  const handleRetry = () => {
    reset(); // 이전 에러 상태 클리어
    setRetryCount(0);
    execute();
  };

  return (
    <div>
      {loading && <p>로딩 중... {retryCount > 0 && `(재시도 ${retryCount}/${maxRetries})`}</p>}

      {error && (
        <div>
          <p>에러 발생: {error.message}</p>
          {error.status && <p>상태 코드: {error.status}</p>}
          <button onClick={handleRetry}>다시 시도</button>
        </div>
      )}

      {data && <UserList users={data} />}
    </div>
  );
}

📚 API 레퍼런스

useApi 훅

typescript
function useApi<T, P = void, R = T>(
  options: UseApiOptions<T, R>
): UseApiReturn<R, P>

매개변수

UseApiOptions<T, R>

속성타입필수기본값설명
endpointstring-API 엔드포인트 URL
method'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE''GET'HTTP 메서드
axiosInstanceAxiosInstance-사용할 Axios 인스턴스
responseFormatter(data: T) => Rundefined응답 데이터 변환 함수
onSuccess(data: R) => voidundefined성공 콜백 함수
onError(error: ApiError) => voidundefined에러 콜백 함수
enableLoggingbooleanfalse콘솔 로그 활성화 여부

반환값

UseApiReturn<R, P>

속성타입설명
loadingboolean요청 진행 중 여부
errorApiError | null에러 정보 (없으면 null)
dataR | null응답 데이터 (없으면 null)
execute(payload?: P, config?: RequestConfig) => Promise<R | null>API 요청 실행 함수
reset() => void상태 초기화 함수

타입 정의

ApiError

typescript
interface ApiError {
  message: string;      // 에러 메시지
  status?: number;      // HTTP 상태 코드
  code?: string;        // 에러 코드
  details?: unknown;    // 추가 에러 정보
}

RequestConfig

typescript
interface RequestConfig {
  url?: string;                          // 요청 URL (기본 endpoint 대신 사용)
  headers?: Record<string, string>;      // 추가 헤더
  params?: Record<string, unknown>;      // 쿼리 파라미터
  timeout?: number;                      // 타임아웃 (ms)
}

유틸리티 함수

responseFormatters

미리 정의된 응답 포맷터 컬렉션:

typescript
const responseFormatters = {
  // 표준 API 응답에서 data 필드 추출
  standard: <T>(response: { data: T; message: string; success: boolean }) => response.data,

  // FastAPI 응답 처리
  fastApi: <T>(response: T | { error: string }) => {
    if (typeof response === "object" && response !== null && "error" in response) {
      throw new Error(response.error);
    }
    return response;
  },

  // 페이지네이션 응답 유지
  paginated: <T>(response: { items: T[]; total: number; page: number; size: number }) => response,

  // 배열 데이터 추출
  arrayData: <T>(response: { results: T[] }) => response.results,
};

createApiHook

재사용 가능한 API 훅 생성기:

typescript
function createApiHook<T, P = void, R = T>(
  baseOptions: Omit<UseApiOptions<T, R>, 'endpoint'>
) => (
  endpoint: string,
  additionalOptions?: Partial<UseApiOptions<T, R>>
) => UseApiReturn<R, P>

🎯 실전 예제

1. 사용자 관리 시스템

typescript
// types/user.ts
export interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user';
  avatar?: string;
  createdAt: string;
  updatedAt: string;
}

export interface CreateUserRequest {
  name: string;
  email: string;
  role: 'admin' | 'user';
}

export interface UpdateUserRequest extends Partial<CreateUserRequest> {
  avatar?: File;
}

// hooks/useUserApi.ts
import { createApiHook } from 'react-easy-api';
import { apiClient } from '../api/client';
import { User } from '../types/user';

export const useUserApi = createApiHook<User>({
  axiosInstance: apiClient,
  enableLogging: process.env.NODE_ENV === 'development',
  onError: (error) => {
    // 글로벌 에러 처리
    if (error.status === 403) {
      console.error('권한이 없습니다');
    }
  },
});

// components/UserManagement.tsx
import React, { useState, useEffect } from 'react';
import { useApi } from 'react-easy-api';
import { useUserApi } from '../hooks/useUserApi';
import { User, CreateUserRequest } from '../types/user';

function UserManagement() {
  const [users, setUsers] = useState<User[]>([]);
  const [selectedUser, setSelectedUser] = useState<User | null>(null);

  // 사용자 목록 조회
  const {
    loading: loadingUsers,
    error: usersError,
    data: usersData,
    execute: fetchUsers
  } = useUserApi('/users');

  // 사용자 생성
  const {
    loading: creatingUser,
    error: createError,
    execute: createUser
  } = useApi<User, CreateUserRequest>({
    endpoint: '/users',
    method: 'POST',
    axiosInstance: apiClient,
    onSuccess: (newUser) => {
      setUsers(prev => [...prev, newUser]);
      console.log('사용자가 생성되었습니다:', newUser);
    },
  });

  // 사용자 삭제
  const {
    loading: deletingUser,
    execute: deleteUser
  } = useApi<void, void>({
    endpoint: '/users/:id',
    method: 'DELETE',
    axiosInstance: apiClient,
    onSuccess: () => {
      if (selectedUser) {
        setUsers(prev => prev.filter(u => u.id !== selectedUser.id));
        setSelectedUser(null);
      }
    },
  });

  useEffect(() => {
    fetchUsers();
  }, [fetchUsers]);

  useEffect(() => {
    if (usersData) {
      setUsers(usersData);
    }
  }, [usersData]);

  const handleCreateUser = async (userData: CreateUserRequest) => {
    await createUser(userData);
  };

  const handleDeleteUser = async (user: User) => {
    if (window.confirm(`"${user.name}" 사용자를 삭제하시겠습니까?`)) {
      await deleteUser(undefined, { url: `/users/${user.id}` });
    }
  };

  return (
    <div>
      <h1>사용자 관리</h1>

      {/* 사용자 목록 */}
      {loadingUsers && <p>사용자 목록을 불러오는 중...</p>}
      {usersError && <p>에러: {usersError.message}</p>}

      <div style={{ display: 'flex', gap: '20px' }}>
        <div style={{ flex: 1 }}>
          <h2>사용자 목록</h2>
          {users.map(user => (
            <div key={user.id} style={{
              padding: '10px',
              border: '1px solid #ccc',
              margin: '5px 0',
              backgroundColor: selectedUser?.id === user.id ? '#e3f2fd' : 'white'
            }}>
              <h3>{user.name}</h3>
              <p>이메일: {user.email}</p>
              <p>역할: {user.role}</p>
              <button onClick={() => setSelectedUser(user)}>선택</button>
              <button
                onClick={() => handleDeleteUser(user)}
                disabled={deletingUser}
                style={{ marginLeft: '10px' }}
              >
                {deletingUser ? '삭제 중...' : '삭제'}
              </button>
            </div>
          ))}
        </div>

        <div style={{ flex: 1 }}>
          <UserForm
            onSubmit={handleCreateUser}
            loading={creatingUser}
            error={createError}
          />
        </div>
      </div>
    </div>
  );
}

2. 실시간 데이터 동기화

typescript
// hooks/useRealtimeData.ts
import { useApi } from 'react-easy-api';
import { useEffect, useRef } from 'react';

export function useRealtimeData<T>(
  endpoint: string,
  intervalMs: number = 5000
) {
  const { loading, error, data, execute } = useApi<T>({
    endpoint,
    axiosInstance: apiClient,
    enableLogging: false, // 실시간 요청은 로그 비활성화
  });

  const intervalRef = useRef<NodeJS.Timeout>();

  useEffect(() => {
    // 초기 데이터 로드
    execute();

    // 주기적 업데이트 설정
    intervalRef.current = setInterval(() => {
      execute();
    }, intervalMs);

    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, [execute, intervalMs]);

  const refresh = () => {
    execute();
  };

  return { loading, error, data, refresh };
}

// components/Dashboard.tsx
function Dashboard() {
  const { loading, error, data: stats, refresh } = useRealtimeData<{
    users: number;
    orders: number;
    revenue: number;
  }>('/dashboard/stats', 10000); // 10초마다 업데이트

  return (
    <div>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <h1>대시보드</h1>
        <button onClick={refresh} disabled={loading}>
          {loading ? '새로고침 중...' : '새로고침'}
        </button>
      </div>

      {error && <p style={{ color: 'red' }}>데이터 로드 실패: {error.message}</p>}

      {stats && (
        <div style={{ display: 'flex', gap: '20px' }}>
          <div style={{ padding: '20px', border: '1px solid #ccc' }}>
            <h3>총 사용자</h3>
            <p style={{ fontSize: '2em' }}>{stats.users.toLocaleString()}</p>
          </div>
          <div style={{ padding: '20px', border: '1px solid #ccc' }}>
            <h3>총 주문</h3>
            <p style={{ fontSize: '2em' }}>{stats.orders.toLocaleString()}</p>
          </div>
          <div style={{ padding: '20px', border: '1px solid #ccc' }}>
            <h3>총 수익</h3>
            <p style={{ fontSize: '2em' }}>${stats.revenue.toLocaleString()}</p>
          </div>
        </div>
      )}
    </div>
  );
}

3. 파일 업로드

typescript
// components/FileUpload.tsx
import { useApi } from 'react-easy-api';
import { useState } from 'react';

interface UploadResponse {
  url: string;
  filename: string;
  size: number;
}

function FileUpload() {
  const [selectedFile, setSelectedFile] = useState<File | null>(null);
  const [uploadProgress, setUploadProgress] = useState(0);

  const { loading, error, data, execute } = useApi<UploadResponse, FormData>({
    endpoint: '/upload',
    method: 'POST',
    axiosInstance: apiClient,
    onSuccess: (response) => {
      console.log('파일 업로드 성공:', response);
      setSelectedFile(null);
      setUploadProgress(0);
    },
  });

  const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0];
    if (file) {
      setSelectedFile(file);
    }
  };

  const handleUpload = async () => {
    if (!selectedFile) return;

    const formData = new FormData();
    formData.append('file', selectedFile);
    formData.append('category', 'documents');

    // 업로드 진행률 추적을 위한 커스텀 설정
    const config = {
      headers: {
        'Content-Type': 'multipart/form-data',
      },
      timeout: 30000, // 30초 타임아웃
      onUploadProgress: (progressEvent: any) => {
        const progress = Math.round(
          (progressEvent.loaded * 100) / progressEvent.total
        );
        setUploadProgress(progress);
      },
    };

    await execute(formData, config);
  };

  return (
    <div>
      <h2>파일 업로드</h2>

      <input
        type="file"
        onChange={handleFileSelect}
        accept=".pdf,.doc,.docx,.jpg,.png"
      />

      {selectedFile && (
        <div style={{ margin: '10px 0' }}>
          <p>선택된 파일: {selectedFile.name}</p>
          <p>크기: {(selectedFile.size / 1024 / 1024).toFixed(2)} MB</p>
        </div>
      )}

      <button
        onClick={handleUpload}
        disabled={!selectedFile || loading}
        style={{ margin: '10px 0' }}
      >
        {loading ? '업로드 중...' : '업로드'}
      </button>

      {loading && (
        <div style={{ margin: '10px 0' }}>
          <div style={{
            width: '100%',
            backgroundColor: '#f0f0f0',
            borderRadius: '4px',
            overflow: 'hidden'
          }}>
            <div
              style={{
                width: `${uploadProgress}%`,
                height: '20px',
                backgroundColor: '#4caf50',
                transition: 'width 0.3s ease',
              }}
            />
          </div>
          <p>{uploadProgress}% 완료</p>
        </div>
      )}

      {error && (
        <p style={{ color: 'red' }}>
          업로드 실패: {error.message}
        </p>
      )}

      {data && (
        <div style={{ marginTop: '10px', padding: '10px', backgroundColor: '#e8f5e8' }}>
          <h3>업로드 성공!</h3>
          <p>파일명: {data.filename}</p>
          <p>크기: {(data.size / 1024 / 1024).toFixed(2)} MB</p>
          <a href={data.url} target="_blank" rel="noopener noreferrer">
            파일 보기
          </a>
        </div>
      )}
    </div>
  );
}

💡 베스트 프랙티스

1. 효율적인 상태 관리

typescript
// ❌ 잘못된 방법: 불필요한 리렌더링
function BadExample() {
  const { data, execute } = useApi({
    endpoint: '/users',
    axiosInstance: apiClient,
    onSuccess: (users) => {
      // 매 렌더링마다 새로운 함수 생성
      console.log('Success:', users);
    },
  });
}

// ✅ 올바른 방법: useCallback 사용
function GoodExample() {
  const handleSuccess = useCallback((users: User[]) => {
    console.log('Success:', users);
  }, []);

  const { data, execute } = useApi({
    endpoint: '/users',
    axiosInstance: apiClient,
    onSuccess: handleSuccess,
  });
}

2. 타입 안정성 확보

typescript
// ✅ 제네릭을 활용한 타입 안정성
interface ApiResponse<T> {
  data: T;
  message: string;
  success: boolean;
}

interface User {
  id: number;
  name: string;
  email: string;
}

// 명확한 타입 지정
const { data, execute } = useApi<ApiResponse<User[]>, void, User[]>({
  endpoint: '/users',
  axiosInstance: apiClient,
  responseFormatter: responseFormatters.standard,
});

// data는 User[] 타입으로 추론됨

3. 에러 경계 활용

typescript
// components/ErrorBoundary.tsx
import React from 'react';

interface Props {
  children: React.ReactNode;
  fallback?: React.ComponentType<{ error: Error }>;
}

interface State {
  hasError: boolean;
  error?: Error;
}

class ErrorBoundary extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('ErrorBoundary caught an error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      const FallbackComponent = this.props.fallback || DefaultErrorFallback;
      return <FallbackComponent error={this.state.error!} />;
    }

    return this.props.children;
  }
}

function DefaultErrorFallback({ error }: { error: Error }) {
  return (
    <div style={{ padding: '20px', border: '1px solid red', borderRadius: '4px' }}>
      <h2>오류가 발생했습니다</h2>
      <p>{error.message}</p>
      <button onClick={() => window.location.reload()}>페이지 새로고침</button>
    </div>
  );
}

4. API 응답 캐싱

typescript
// utils/cache.ts
class ApiCache {
  private cache = new Map<string, { data: any; timestamp: number }>();
  private readonly TTL = 5 * 60 * 1000; // 5분

  set(key: string, data: any) {
    this.cache.set(key, {
      data,
      timestamp: Date.now(),
    });
  }

  get(key: string) {
    const item = this.cache.get(key);
    if (!item) return null;

    if (Date.now() - item.timestamp > this.TTL) {
      this.cache.delete(key);
      return null;
    }

    return item.data;
  }

  clear() {
    this.cache.clear();
  }
}

export const apiCache = new ApiCache();

// hooks/useCachedApi.ts
import { useApi } from 'react-easy-api';
import { apiCache } from '../utils/cache';

export function useCachedApi<T>(endpoint: string) {
  const { loading, error, data, execute } = useApi<T>({
    endpoint,
    axiosInstance: apiClient,
    onSuccess: (data) => {
      apiCache.set(endpoint, data);
    },
  });

  const executeWithCache = useCallback(async () => {
    const cached = apiCache.get(endpoint);
    if (cached) {
      return cached;
    }
    return execute();
  }, [endpoint, execute]);

  return { loading, error, data, execute: executeWithCache };
}

🔧 트러블슈팅

1. 일반적인 문제들

"Module not found" 에러

bash
# React와 Axios가 설치되지 않은 경우
npm install react axios

# TypeScript 타입이 없는 경우
npm install --save-dev @types/react

CORS 에러

typescript
// 개발 환경에서 프록시 설정 (package.json)
{
  "name": "my-app",
  "proxy": "<http://localhost:3001>"
}

// 또는 Axios 인스턴스에서 설정
const apiClient = axios.create({
  baseURL: process.env.NODE_ENV === 'development'
    ? '/api'
    : '<https://api.production.com>',
});

무한 리렌더링

typescript
// ❌ 문제: execute가 의존성 배열에 없음
useEffect(() => {
  execute();
}, []); // execute가 변경되어도 재실행되지 않음

// ✅ 해결: execute를 의존성 배열에 추가
useEffect(() => {
  execute();
}, [execute]);

// 또는 useCallback으로 execute 함수 메모이제이션 확인

2. 성능 최적화

불필요한 요청 방지

typescript
// 조건부 실행
function ConditionalApi({ shouldFetch }: { shouldFetch: boolean }) {
  const { data, execute } = useApi({
    endpoint: '/expensive-data',
    axiosInstance: apiClient,
  });

  useEffect(() => {
    if (shouldFetch) {
      execute();
    }
  }, [shouldFetch, execute]);
}

// 디바운싱
import { useDebouncedCallback } from 'use-debounce';

function SearchWithDebounce() {
  const [query, setQuery] = useState('');

  const { loading, data, execute } = useApi({
    endpoint: '/search',
    axiosInstance: apiClient,
  });

  const debouncedSearch = useDebouncedCallback(
    (searchQuery: string) => {
      if (searchQuery.length > 2) {
        execute(undefined, { params: { q: searchQuery } });
      }
    },
    500
  );

  useEffect(() => {
    debouncedSearch(query);
  }, [query, debouncedSearch]);
}

태그

#TypeScript#library#axios