import React, { useState, useEffect } from 'react';
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
function FetchExample() {
const [post, setPost] = useState<Post | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
setError(null);
fetch('https://jsonplaceholder.typicode.com/posts/1') // API 엔드포인트
.then(response => {
// fetch는 네트워크 오류가 아닌 이상 (예: 404, 500) 에러를 throw하지 않음
// 따라서 응답 상태(ok 또는 status)를 직접 확인해야 함
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 응답 본문을 JSON으로 파싱 (이 과정도 Promise 반환)
return response.json();
})
.then(data => {
setPost(data);
})
.catch(err => {
console.error("Fetch error:", err);
setError(err);
})
.finally(() => {
setLoading(false);
});
}, []); // 마운트 시 한 번만 실행
if (loading) return <div>로딩 중...</div>;
if (error) return <div>오류 발생: {error.message}</div>;
if (!post) return null;
return (
<div>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
);
}
fetch('/api/users', {
method: 'POST', // HTTP 메서드
headers: {
'Content-Type': 'application/json', // 요청 본문 타입
// 'Authorization': 'Bearer YOUR_TOKEN', // 인증 토큰 등
},
body: JSON.stringify({ name: 'Alice', age: 30 }), // JavaScript 객체를 JSON 문자열로 변환
})
.then(response => response.json())
.then(data => console.log('Success:', data))
.catch(error => console.error('Error:', error));
response.ok
나 response.status
를 직접 확인하는 로직이 필수입니다.response.json()
, response.text()
등 별도의 메서드를 호출해야 하며, 이 과정도 Promise를 반환합니다.npm install axios
# 또는
yarn add axios
import React, { useState, useEffect } from 'react';
import axios from 'axios'; // axios 임포트
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
function AxiosExample() {
const [post, setPost] = useState<Post | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
setError(null);
axios.get<Post>('https://jsonplaceholder.typicode.com/posts/1') // GET 요청 및 응답 타입 지정
.then(response => {
// 응답 데이터는 response.data 안에 자동으로 파싱되어 있음
setPost(response.data);
})
.catch(err => {
// Axios는 4xx, 5xx 응답 상태 코드일 경우 에러(rejected Promise)로 처리함
console.error("Axios error:", err);
setError(err);
})
.finally(() => {
setLoading(false);
});
}, []);
if (loading) return <div>로딩 중...</div>;
// Axios 에러 객체에서 구체적인 정보 확인 가능 (err.response, err.request 등)
if (error) return <div>오류 발생: {axios.isAxiosError(error) ? error.message : '알 수 없는 오류'}</div>;
if (!post) return null;
return (
<div>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
);
}
axios.post('/api/users', { name: 'Bob', age: 25 }) // 두 번째 인자로 데이터 객체 전달
.then(response => console.log('Success:', response.data))
.catch(error => console.error('Error:', error.response?.data || error.message)); // 에러 응답 확인
catch
블록에서 에러를 처리할 수 있습니다.특징 | Fetch API | Axios |
---|---|---|
설치 필요 | 없음 (브라우저 내장) | 필요 (라이브러리) |
Promise 기반 | 예 | 예 |
JSON 변환 | 수동 (.json() 호출 필요) | 자동 |
HTTP 에러 처리 | 수동 (response.ok 확인 필요) | 자동 (Promise reject) |
요청 취소 | AbortController 사용 | AbortController 또는 CancelToken 사용 |
인터셉터 | 없음 (직접 구현 필요) | 있음 (매우 편리) |
타임아웃 | 없음 (직접 구현 필요) | 있음 (설정 간편) |
호환성 | 최신 브라우저 위주 | 구형 브라우저 지원 양호 |
번들 크기 | 영향 없음 | 라이브러리 크기만큼 증가 |
AbortController
인스턴스를 생성합니다.signal
속성을 fetch나 Axios 요청 옵션에 전달합니다.abort()
메서드를 호출합니다.import React, { useState, useEffect } from 'react';
import axios from 'axios'; // Axios 사용 예시
function SearchComponent() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!query) { // 검색어가 없으면 요청 안 함
setResults([]);
return;
}
// 1. AbortController 생성
const controller = new AbortController();
const fetchData = async () => {
setLoading(true);
try {
const response = await axios.get(`/api/search?q=${query}`, {
// 2. signal 전달
signal: controller.signal,
});
setResults(response.data);
} catch (error) {
// 요청 취소 시 Axios는 'CanceledError' 발생시킴
if (axios.isCancel(error)) {
console.log('Request canceled:', error.message);
} else {
console.error('Search error:', error);
}
} finally {
setLoading(false);
}
};
fetchData();
// cleanup 함수: 컴포넌트 언마운트 또는 query 변경 시 이전 요청 취소
return () => {
// 3. abort() 호출하여 요청 취소
console.log(`Canceling request for query: ${query}`);
controller.abort();
};
}, [query]); // query가 변경될 때마다 effect 재실행
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="검색어 입력..."
/>
{loading && <div>검색 중...</div>}
<ul>
{results.map((result: any) => (
<li key={result.id}>{result.name}</li>
))}
</ul>
</div>
);
}
useEffect
의 cleanup 함수에서 controller.abort()
를 호출하여, 컴포넌트가 언마운트되거나 의존성(여기서는 query
)이 변경되어 새로운 요청을 보내기 전에 이전 요청을 취소하는 것이 일반적인 패턴입니다.
Fetch API와 함께 사용할 때도 signal
옵션을 동일하게 전달하면 됩니다. 요청이 취소되면 Fetch는 'AbortError'라는 이름의 에러를 발생시킵니다.// Apollo Client 사용 예시 (설정은 생략)
import React from 'react';
import { useQuery, gql } from '@apollo/client';
const GET_USER = gql` /* ... GraphQL 쿼리 ... */ `;
interface UserData { /* ... */ }
interface UserVars { /* ... */ }
function UserProfileApollo({ userId }: { userId: string }) {
const { loading, error, data } = useQuery<UserData, UserVars>(GET_USER, {
variables: { userId },
});
// ... 로딩, 에러, 데이터 처리 ...
}
if (loading) ...
)으로 처리하는 대신, React 자체 기능인 useQuery
훅에 suspense: true
옵션을 사용하면, 데이터 로딩 중일 때 Promise를 throw하여 Suspense가 이를 감지하고 fallback을 보여주게 할 수 있습니다.import React, { Suspense } from 'react'; // Suspense 임포트
import { useQuery, QueryErrorResetBoundary } from 'react-query'; // QueryErrorResetBoundary 추가 (에러 처리용)
import { ErrorBoundary } from 'react-error-boundary'; // 에러 바운더리 라이브러리 예시
import axios from 'axios';
interface User { id: string; name: string; email: string; }
const fetchUser = async (userId: string): Promise<User> => {
const { data } = await axios.get(`/api/users/${userId}`);
return data;
};
// 데이터를 보여주는 실제 컴포넌트
function UserProfileData({ userId }: { userId: string }) {
const { data } = useQuery<User, Error>(
['user', userId],
() => fetchUser(userId),
{
suspense: true, // Suspense 모드 활성화
// useErrorBoundary: true, // ErrorBoundary와 함께 사용 시 권장
}
);
// isLoading, error 상태를 직접 처리할 필요 없음
return (
<div>
<h2>{data.name}</h2>
<p>Email: {data.email}</p>
</div>
);
}
// 에러 발생 시 보여줄 컴포넌트
function ErrorFallback({ error, resetErrorBoundary }: any) {
return (
<div role="alert">
<p>문제가 발생했습니다:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>다시 시도</button>
</div>
);
}
// Suspense와 ErrorBoundary로 감싸서 사용
function UserProfileSuspense({ userId }: { userId: string }) {
return (
// ErrorBoundary로 에러 처리 (Suspense는 로딩 상태만 처리)
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={reset}>
{/* Suspense로 로딩 상태 처리 */}
<Suspense fallback={<div>사용자 정보 로딩 중...</div>}>
<UserProfileData userId={userId} />
</Suspense>
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
}
다음 시간에는 프론트엔드 개발의보안과 안정성에 대해 알아보겠습니다. 웹 애플리케이션에서 발생할 수 있는 보안 위협(XSS, CSRF)과 이를 예방하는 방법, 그리고 인증/인가 처리 방식에 대해 이야기 나누겠습니다.