안녕하세요, 웹 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 |