본문 바로가기
4주차

API 에러 처리: 사용자 경험 향상을 위해서.

by kdggspy 2025. 5. 15.

안녕하세요, 웹 YB 신지수입니다 !

 

이번 과제에서 React와 TypeScript를 활용한 로그인/회원가입 기능을 구현하면서, API 통신 과정에서 발생하는 다양한 에러를 어떻게 효과적으로 처리할 수 있을지 고민했습니다.

 

에러는 불가피하게 발생하지만, 이를 어떻게 처리하느냐에 따라 사용자 경험이 크게 달라질 수 있다고 생각합니다. 이번 글에서는 제가 프로젝트에 적용한 API 에러 처리 전략과 그 과정에서 배운 점들을 공유하려고 합니다 ☺️

 

🚨 API 에러, 왜 신경 써야 할까 ?

API를 사용하다 보면 에러 상황은 피할 수 없습니다. 하지만 이때 단순히 "오류가 발생했습니다."라는 메시지를 보여주는 것과, 구체적인 원인과 해결 방법을 안내하는 것은 사용자 경험 측면에서 큰 차이를 만들어낸다고 생각합니다. 에러 처리가 중요한 이유는 단순히 문제를 피하기 위함이 아니라, 더 나은 사용자 경험과 서비스 신뢰성, 개발 생산성을 위한 필수 요소이기 때문입니다 !!

 

1. 사용자 만족도 향상

명확하고 친절한 에러 메시지는 사용자가 무엇이 잘못되었는지 빠르게 이해하고, 어떻게 해결할 수 있는지를 안내해 줍니다.
ex)

❌ "에러가 발생했습니다." → 사용자는 당황하고 앱을 종료할 수 있음

✅ "로그인 세션이 만료되었습니다. 다시 로그인해 주세요." → 사용자 행동 유도 + 만족도 유지
-> 이는 스트레스 없는 사용자 경험을 만드는 데 핵심적인 역할을 합니다.

 

2. 서비스 신뢰성 제공

에러 상황에서도 앱이 논리적으로 대응하고 있다는 느낌을 주면, 사용자는 서비스를 더 안정적이고 신뢰할 수 있다고 인식합니다.

 

3. 개발 효율성

체계적으로 구조화된 에러 처리 방식은 문제 발생 시 디버깅을 빠르게 할 수 있도록 돕고, 유지 보수 또한 훨씬 쉬워집니다.
ex) 로그에 ERR401_TOKEN_EXPIRED 같은 에러 코드를 남기면, 개발자는 바로 어떤 문제가 발생했는지 파악하고 대응할 수 있습니다.

 

🔍 API 에러의 종류와 처리 방법

🤮 API 에러의 종류

1. 네트워크 에러 (인터넷 연결 문제)

사용자의 인터넷 연결이 끊기거나 서버가 일시적으로 접속 불가능한 상황에서 발생합니다.

try {
  const response = await client.get('/api/users/me');
} catch (error) {
  if (!navigator.onLine) {
    alert('인터넷 연결을 확인해 주세요.');
  } else if (axios.isAxiosError(error) && !error.response) {
    alert('서버와의 연결이 원활하지 않습니다. 잠시 후 다시 시도해주세요.');
  }
}

 

2. 서버 에러 (5xx)

서버 내부 오류로 인한 에러입니다. 이는 백엔드 개발자가 해결해야 하는 문제이지만, 사용자에게는 친절한 메시지를 제공해야 합니다.

if (error.response?.status >= 500) {
  alert('서비스에 일시적인 장애가 발생했습니다. 잠시 후 다시 시도해 주세요.');
  console.error('서버 에러:', error.response?.data);
}

 

3. 클라이언트 에러 (4xx)

가장 일반적인 에러로, 사용자의 잘못된 요청이나 인증 문제로 발생합니다.

if (error.response?.status === 401) {
  // 인증 에러
  alert('로그인이 필요한 서비스입니다.');
  navigate('/login');
} else if (error.response?.status === 403) {
  // 권한 에러
  alert('접근 권한이 없습니다.');
} else if (error.response?.status === 404) {
  // 리소스 없음
  alert('요청하신 정보를 찾을 수 없습니다.');
}

 

🤓 API 에러 처리 방법

1. axios 인터셉터로 전역 에러 처리하기

에러 처리 코드를 각 컴포넌트에 반복해서 작성하는 것은 비효율적입니다. 저는 axios 인터셉터를 활용해 전역적으로 에러를 처리하는 방식을 적용했습니다.

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

export const client = axios.create({
  baseURL: 'https://api.atsopt-seminar4.site',
  headers: {
    'Content-Type': 'application/json',
  },
});

// 요청 인터셉터 - 토큰이 필요한 요청에 토큰 추가
client.interceptors.request.use(
  (config) => {
    const userId = localStorage.getItem('userId');
    if (userId) {
      config.headers.userId = userId;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 응답 인터셉터 - 공통 에러 처리
client.interceptors.response.use(
  (response) => {
    return response;
  },
  (error) => {
    if (error.response) {
      // 서버에서 응답이 왔지만 상태 코드가 2xx가 아닌 경우
      console.error('API Error:', error.response.data);

      // 인증 에러인 경우 (401) - 로그인 페이지로 리다이렉트
      if (error.response.status === 401 && !window.location.pathname.includes('/login')) {
        localStorage.removeItem('userId');
        window.location.href = '/login';
      }
    } else if (error.request) {
      // 요청이 발생했지만 응답을 받지 못한 경우
      console.error('API Request Error:', error.request);
    } else {
      // 요청 설정 단계에서 오류가 발생한 경우
      console.error('API Config Error:', error.message);
    }

    return Promise.reject(error);
  }
);

 

이렇게 구현하면 모든 API 요청에 대한 기본적인 에러 처리가 자동으로 적용됩니다. 특히 인증 관련 에러(401)가 발생했을 때 자동으로 로그인 페이지로 리다이렉트시키는 로직은 매우 유용했습니다 !

 

2. TypeScript로 API 응답 및 에러 타입 정의하기

API 응답과 에러 메시지를 TypeScript로 타입 정의하면 에러 처리 과정이 더욱 안전하고 효율적입니다.

// types/api.ts
export interface ApiResponse<T> {
  success: boolean;
  code: string;
  message: string;
  data: T | null;
}

export interface ApiErrorResponse {
  success: boolean;
  code: string;
  message: string;
}

 

이렇게 정의한 타입을 사용하면 에러 메시지를 정확하게 추출하고 타입 안전성을 보장할 수 있습니다.

try {
  const response = await login({ loginId, password });
  // 성공 처리
} catch (error: unknown) {
  if (error instanceof AxiosError && error.response?.data) {
    const errorData = error.response.data as ApiErrorResponse;
    setError(errorData.message || '로그인에 실패했습니다.');
  } else {
    setError('로그인 중 오류가 발생했습니다.');
  }
}

 

3. 커스텀 훅으로 API 호출 및 에러 처리 로직 추상화하기

반복되는 API 호출과 에러 처리 로직을 커스텀 훅으로 추상화하면 코드 재사용성을 높이고 일관된 에러 처리가 가능합니다.

// hooks/useApi.ts
export function useApi<T>() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [data, setData] = useState<T | null>(null);

  const callApi = async <R>(
    apiFunc: () => Promise<ApiResponse<R>>,
    successCallback?: (data: R) => void
  ) => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await apiFunc();
      
      if (response.success && response.data) {
        setData(response.data as unknown as T);
        successCallback?.(response.data);
        return response.data;
      } else {
        setError(response.message);
        return null;
      }
    } catch (error: unknown) {
      if (error instanceof AxiosError && error.response?.data) {
        const errorData = error.response.data as ApiErrorResponse;
        setError(errorData.message || '요청 처리 중 오류가 발생했습니다.');
      } else {
        setError('요청 처리 중 오류가 발생했습니다.');
      }
      return null;
    } finally {
      setLoading(false);
    }
  };

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

 

이 커스텀 훅을 사용하면 다음과 같이 컴포넌트에서 API 호출 및 에러 처리를 간결하게 구현할 수 있습니다.

// 사용 예시
const LoginForm: React.FC = () => {
  const { loading, error, callApi } = useApi();
  const navigate = useNavigate();
  const [id, setId] = useState('');
  const [password, setPassword] = useState('');

  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault();
    
    const userData = await callApi(
      () => login({ loginId: id, password }),
      (data) => {
        localStorage.setItem('userId', data.userId.toString());
        navigate('/mypage');
      }
    );
  };

  return (
    <form onSubmit={handleLogin}>
      {/* 입력 필드 */}
      {error && <ErrorMessage message={error} />}
      <button type="submit" disabled={loading}>
        {loading ? '로그인 중...' : '로그인'}
      </button>
    </form>
  );
};

 

4. 404 페이지와 라우팅 에러 처리

사용자가 존재하지 않는 페이지에 접근했을 때도 적절한 안내가 필요합니다. React Router를 사용할 때는 와일드카드 경로를 추가하여 모든 매칭되지 않는 경로를 404 페이지로 처리할 수 있습니다.

// App.tsx
function App() {
  return (
    <div className={themeClass}>
      <Router>
        <Routes>
          <Route path="/" element={<Login />} />
          <Route path="/login" element={<Login />} />
          <Route path="/signup" element={<SignUp />} />
          <Route path="/mypage" element={<MyPage />} />
          <Route path="/mypage/info" element={<MyPage />} />
          <Route path="/mypage/search" element={<MyPage />} />
          {/* 404 페이지 처리 */}
          <Route path="*" element={<NotFound />} />
        </Routes>
      </Router>
    </div>
  );
}

 

과제에서는 구현하지 않았지만, 실제 서비스에서는 NotFound 컴포넌트를 구현하여 사용자에게 페이지를 찾을 수 없다는 메시지와 함께 홈으로 돌아갈 수 있는 버튼을 제공하면 좋을 것 같습니다.

 

이번 과제를 통해 에러 처리와 명확한 메시지 제공의 중요성을 배울 수 있었습니다. 여러분도 사용자와 개발자를 모두 배려하는 에러 처리 문화를 함께 만들어가시길 바랍니다 ☺️☺️

 

🔗 참고자료

https://axios-http.com/docs/interceptors

 

Interceptors | Axios Docs

Interceptors You can intercept requests or responses before they are handled by then or catch. axios.interceptors.request.use(function (config) { return config; }, function (error) { return Promise.reject(error); }); axios.interceptors.response.use(functio

axios-http.com

 

https://reactrouter.com/6.28.0/start/concepts#not-found-routes

 

Main Concepts v6.28.0 | React Router

 

reactrouter.com

 

https://www.typescriptlang.org/docs/handbook/2/narrowing.html

 

Documentation - Narrowing

Understand how TypeScript uses JavaScript knowledge to reduce the amount of type syntax in your projects.

www.typescriptlang.org

 

'4주차' 카테고리의 다른 글

GIT 브랜치 전략  (1) 2025.06.13
커밋 메시지 컨벤션을 지키는 방법  (0) 2025.05.14
대중적인 API instance 세팅과 고려해야할 점!  (0) 2025.05.13
네이밍 규칙  (1) 2025.05.13
CommonJS와 ES Modules  (0) 2025.05.13