Fetch API와 TypeScript 결합하기: 강력한 웹 개발의 시너지 🚀
웹 개발 세계에서 데이터 통신과 타입 안정성은 두 가지 핵심 요소입니다. Fetch API는 네트워크 요청을 간편하게 만들어주고, TypeScript는 JavaScript에 정적 타입을 추가하여 코드의 안정성을 높여줍니다. 이 두 기술을 결합하면 어떤 마법이 일어날까요? 🎩✨
이 가이드에서는 Fetch API와 TypeScript를 함께 사용하는 방법을 상세히 알아보겠습니다. 초보자부터 전문가까지, 모든 개발자가 이해하고 적용할 수 있는 실용적인 접근법을 제시할 것입니다. 특히 재능넷과 같은 플랫폼에서 활동하는 프리랜서 개발자들에게 유용한 내용이 될 것입니다.
자, 이제 TypeScript의 강력한 타입 시스템과 Fetch API의 유연성을 결합하여 더 안전하고 효율적인 웹 애플리케이션을 만드는 여정을 시작해볼까요? 🌟
1. Fetch API 소개 📡
Fetch API는 현대 웹 개발에서 필수적인 도구입니다. AJAX 요청을 보내고 받는 데 사용되는 이 인터페이스는 XMLHttpRequest를 대체하여 더 강력하고 유연한 기능을 제공합니다.
1.1 Fetch API의 특징
- Promise 기반: 비동기 프로그래밍을 더 쉽게 만듭니다.
- 간결한 문법: 복잡한 설정 없이 간단하게 사용할 수 있습니다.
- 스트림 지원: 대용량 데이터를 효율적으로 처리할 수 있습니다.
- CORS 지원: 크로스 오리진 요청을 안전하게 처리합니다.
1.2 기본 사용법
Fetch API의 기본적인 사용법을 살펴보겠습니다:
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
이 간단한 예제에서 볼 수 있듯이, Fetch API는 URL을 인자로 받아 Promise를 반환합니다. 이 Promise는 Response 객체로 해결되며, 이를 통해 응답 데이터에 접근할 수 있습니다.
1.3 응답 처리
Fetch API는 다양한 형식의 응답을 처리할 수 있습니다:
- response.json(): JSON 형식의 응답을 파싱합니다.
- response.text(): 텍스트 형식의 응답을 반환합니다.
- response.blob(): 이미지나 파일 등의 바이너리 데이터를 처리합니다.
- response.formData(): form 데이터를 처리합니다.
1.4 요청 옵션 설정
Fetch API는 다양한 옵션을 통해 요청을 커스터마이즈할 수 있습니다:
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ key: 'value' })
})
.then(response => response.json())
.then(data => console.log(data));
이 예제에서는 POST 요청을 보내고, 헤더와 본문을 설정하는 방법을 보여줍니다.
1.5 에러 처리
Fetch API에서 주의해야 할 점은 네트워크 오류가 아닌 한 Promise는 거부되지 않는다는 것입니다. 따라서 응답의 상태 코드를 확인하는 것이 중요합니다:
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
이렇게 Fetch API의 기본적인 사용법을 살펴보았습니다. 이제 TypeScript와 어떻게 결합하여 사용할 수 있는지 알아보겠습니다. 🧐
2. TypeScript 기초 🏗️
TypeScript는 JavaScript의 슈퍼셋 언어로, 정적 타입 검사와 최신 ECMAScript 기능을 제공합니다. Fetch API와 결합하기 전에, TypeScript의 핵심 개념을 간단히 살펴보겠습니다.
2.1 TypeScript의 장점
- 정적 타입 검사: 컴파일 시점에 오류를 잡아낼 수 있습니다.
- 향상된 IDE 지원: 자동 완성, 리팩토링 등의 기능이 강화됩니다.
- 객체 지향 프로그래밍 지원: 클래스, 인터페이스 등을 사용할 수 있습니다.
- 모듈 시스템: 코드를 모듈화하여 관리할 수 있습니다.
2.2 기본 타입
TypeScript에서 제공하는 기본 타입들을 살펴보겠습니다:
let isDone: boolean = false;
let decimal: number = 6;
let color: string = "blue";
let list: number[] = [1, 2, 3];
let tuple: [string, number] = ["hello", 10];
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
let notSure: any = 4;
let unusable: void = undefined;
let u: undefined = undefined;
let n: null = null;
2.3 인터페이스
인터페이스는 TypeScript에서 타입을 정의하는 강력한 방법입니다:
interface User {
name: string;
age: number;
email?: string; // 선택적 속성
readonly id: number; // 읽기 전용 속성
}
let user: User = {
name: "John",
age: 30,
id: 1
};
2.4 제네릭
제네릭을 사용하면 재사용 가능한 컴포넌트를 만들 수 있습니다:
function identity<T>(arg: T): T {
return arg;
}
let output = identity<string>("myString");
2.5 타입 추론
TypeScript는 많은 경우 타입을 자동으로 추론할 수 있습니다:
let x = 3; // number로 추론됨
let y = [0, 1, null]; // (number | null)[]로 추론됨
2.6 유니온 타입
변수가 여러 타입 중 하나일 수 있음을 나타냅니다:
let multiType: number | string;
multiType = 20; // OK
multiType = "twenty"; // OK
2.7 타입 가드
런타임에 타입을 좁혀나가는 방법입니다:
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
이러한 TypeScript의 기본 개념들은 Fetch API와 결합할 때 매우 유용하게 사용됩니다. 특히 API 응답의 타입을 정의하고 처리하는 데 큰 도움이 됩니다. 다음 섹션에서는 이 두 기술을 어떻게 효과적으로 결합할 수 있는지 살펴보겠습니다. 💡
3. Fetch API와 TypeScript 결합하기 🤝
이제 Fetch API와 TypeScript를 결합하여 사용하는 방법을 자세히 알아보겠습니다. 이 결합은 네트워크 요청의 안정성과 예측 가능성을 크게 향상시킵니다.
3.1 기본적인 Fetch 요청에 타입 추가하기
먼저, 간단한 GET 요청에 타입을 추가해 보겠습니다:
interface User {
id: number;
name: string;
email: string;
}
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
return response.json() as Promise<User>;
}
// 사용 예
fetchUser(1)
.then(user => console.log(user.name))
.catch(error => console.error(error));
이 예제에서는 User
인터페이스를 정의하고, fetchUser
함수가 Promise<User>
를 반환하도록 명시했습니다. 이렇게 하면 반환된 데이터의 구조를 명확히 알 수 있고, IDE의 자동 완성 기능을 활용할 수 있습니다.
3.2 POST 요청 처리하기
이번에는 POST 요청을 TypeScript와 함께 사용해 보겠습니다:
interface CreateUserRequest {
name: string;
email: string;
}
interface CreateUserResponse {
id: number;
name: string;
email: string;
createdAt: string;
}
async function createUser(userData: CreateUserRequest): Promise<CreateUserResponse> {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});
if (!response.ok) {
throw new Error('Failed to create user');
}
return response.json() as Promise<CreateUserResponse>;
}
// 사용 예
const newUser: CreateUserRequest = {
name: "John Doe",
email: "john@example.com"
};
createUser(newUser)
.then(createdUser => console.log(`Created user with ID: ${createdUser.id}`))
.catch(error => console.error(error));
이 예제에서는 요청 본문(CreateUserRequest
)과 응답 본문(CreateUserResponse
)에 대한 인터페이스를 각각 정의했습니다. 이를 통해 요청과 응답 데이터의 구조를 명확히 할 수 있습니다.
3.3 제네릭을 활용한 재사용 가능한 Fetch 함수
여러 API 엔드포인트에서 사용할 수 있는 재사용 가능한 Fetch 함수를 만들어 보겠습니다:
async function fetchData<T>(url: string, options?: RequestInit): Promise<T> {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json() as Promise<T>;
}
// 사용 예
interface Product {
id: number;
name: string;
price: number;
}
fetchData<Product[]>('https://api.example.com/products')
.then(products => {
products.forEach(product => console.log(`${product.name}: $${product.price}`));
})
.catch(error => console.error(error));
interface Order {
id: number;
productId: number;
quantity: number;
}
const newOrder: Order = {
id: 1,
productId: 100,
quantity: 2
};
fetchData<Order>('https://api.example.com/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newOrder),
})
.then(order => console.log(`Order created with ID: ${order.id}`))
.catch(error => console.error(error));
이 제네릭 fetchData
함수는 다양한 타입의 데이터를 처리할 수 있으며, 각 요청에 대해 적절한 타입을 지정할 수 있습니다.
3.4 에러 처리 개선하기
TypeScript를 사용하여 더 구체적인 에러 처리를 구현할 수 있습니다:
class ApiError extends Error {
constructor(public status: number, message: string) {
super(message);
this.name = 'ApiError';
}
}
async function fetchWithErrorHandling<T>(url: string, options?: RequestInit): Promise<T> {
const response = await fetch(url, options);
if (!response.ok) {
throw new ApiError(response.status, `HTTP error! status: ${response.status}`);
}
return response.json() as Promise<T>;
}
// 사용 예
fetchWithErrorHandling<User>('https://api.example.com/users/1')
.then(user => console.log(user.name))
.catch(error => {
if (error instanceof ApiError) {
console.error(`API Error ${error.status}: ${error.message}`);
} else {
console.error('An unexpected error occurred:', error);
}
});
이 예제에서는 커스텀 ApiError
클래스를 정의하여 API 관련 오류를 더 구체적으로 처리할 수 있게 했습니다.
3.5 응답 헤더 및 상태 처리
때로는 응답 본문뿐만 아니라 헤더나 상태 코드도 처리해야 할 수 있습니다:
interface ApiResponse<T> {
data: T;
status: number;
headers: Headers;
}
async function fetchWithMetadata<T>(url: string, options?: RequestInit): Promise<ApiResponse<T>> {
const response = await fetch(url, options);
const data = await response.json() as T;
return {
data,
status: response.status,
headers: response.headers,
};
}
// 사용 예
fetchWithMetadata<User>('https://api.example.com/users/1')
.then(({ data, status, headers }) => {
console.log(`User: ${data.name}`);
console.log(`Status: ${status}`);
console.log(`Content-Type: ${headers.get('Content-Type')}`);
})
.catch(error => console.error(error));
이 접근 방식을 사용하면 응답 데이터와 함께 상태 코드와 헤더 정보도 쉽게 접근할 수 있습니다.
3.6 인터셉터 패턴 구현하기
인터셉터 패턴을 사용하여 모든 요청과 응답을 가로채고 수정할 수 있습니다:
type RequestInterceptor = (config: RequestInit) => RequestInit;
type ResponseInterceptor = (response: Response) => Response | Promise<Response>;
class FetchClient {
private requestInterceptors: RequestInterceptor[] = [];
private responseInterceptors: ResponseInterceptor[] = [];
addRequestInterceptor(interceptor: RequestInterceptor) {
this.requestInterceptors.push(interceptor);
}
addResponseInterceptor(interceptor: ResponseInterceptor) {
this.responseInterceptors.push(interceptor);
}
async fetch<T>(url: string, options: RequestInit = {}): Promise<T> {
let config = { ...options };
for (const interceptor of this.requestInterceptors) {
config = interceptor(config);
}
let response = await fetch(url, config);
for (const interceptor of this.responseInterceptors) {
response = await interceptor(response);
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json() as Promise<T>;
}
}
// 사용 예
const client = new FetchClient();
client.addRequestInterceptor((config) => {
config.headers = {
...config.headers,
'Authorization': 'Bearer token123',
};
return config;
});
client.addResponseInterceptor(async (response) => {
if (response.status === 401) {
// 토큰 갱신 로직
const newToken = await refreshToken();
// 원래 요청 재시도
return fetch(response.url, {
...response,
headers: {
...response.headers,
'Authorization': `Bearer ${newToken}`,
},
});
}
return response;
});
client.fetch<User>('https://api.example.com/users/1')
.then(user => console.log(user.name))
.catch(error => console.error(error));
이 패턴을 사용하면 인증 토큰 추가, 에러 처리, 로깅 등의 공통 로직을 모든 요청에 쉽게 적용할 수 있습니다.
3.7 취소 가능한 요청 구현하기
TypeScript와 함께 AbortController
를 사용하여 취소 가능한 Fetch 요청을 구현할 수 있습니다:
interface CancellableFetch<T> {
promise: Promise<T>;
cancel: () => void;
}
function cancellableFetch<T>(url: string, options?: RequestInit): CancellableFetch<T> {
const controller = new AbortController();
const signal = controller.signal;
const promise = fetch(url, { ...options, signal })
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json() as Promise<T>;
});
return {
promise,
cancel: () => controller.abort()
};
}
// 사용 예
const { promise, cancel } = cancellableFetch<User>('https://api.example.com/users/1');
const timeoutId = setTimeout(() => {
cancel();
console.log('Request cancelled due to timeout');
}, 5000);
promise
.then(user => {
clearTimeout(timeoutId);
console.log(user.name);
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('Request was cancelled');
} else {
console.error('An error occurred:', error);
}
});
이 구현을 통해 장시간 실행되는 요청을 필요에 따라 취소할 수 있습니다.
이러한 다양한 기법들을 활용하면 Fetch API와 TypeScript를 결합하여 강력하고 유연한 네트워크 통신 로직을 구현할 수 있습니다. 다음 섹션에서는 이러한 기법들을 실제 프로젝트에 적용하는 방법에 대해 더 자세히 알아보겠습니다. 🚀
4. 실제 프로젝트에 적용하기 🏗️
지금까지 배운 Fetch API와 TypeScript 결합 기법을 실제 프로젝트에 적용하는 방법을 살펴보겠습니다. 이를 통해 코드의 재사용성, 유지보수성, 그리고 타입 안정성을 크게 향상시킬 수 있습니다.
4.1 API 클라이언트 구현하기
먼저, 재사용 가능한 API 클라이언트를 구현해 보겠습니다:
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async get<T>(endpoint: string): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json() as Promise<T>;
}
async post<T>(endpoint: string, data: any): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json() as Promise<T>;
}
// PUT, DELETE 등의 다른 메서드도 유사하게 구현할 수 있습니다.
}
// 사용 예
const api = new ApiClient('https://api.example.com');
interface User {
id: number;
name: string;
email: string;
}
api.get<User[]>('/users')
.then(users => users.forEach(user => console.log(user.name)))
.catch(error => console.error(error));
api.post<User>('/users', { name: 'John Doe', email: 'john@example.com' })
.then(user => console.log(`Created user with ID: ${user.id}`))
.catch(error => console.error(error));
4.2 서비스 레이어 구현하기
API 클라이언트를 사용하여 특정 도메인의 서비스 레이어를 구현할 수 있습니다:
class UserService {
private api: ApiClient;
constructor(api: ApiClient) {
this.api = api;
}
async getUsers(): Promise<User[]> {
return this.api.get<User[]>('/users');
}
async getUserById(id: number): Promise<User> {
return this.api.get<User>(`/users/${id}`);
}
async createUser(userData: Omit<User, 'id'>): Promise<User> {
return this.api.post<User>('/users', userData);
}
}
// 사용 예
const api = new ApiClient('https://api.example.com');
const userService = new UserService(api);
userService.getUsers()
.then(users => users.forEach(user => console.log(user.name)))
.catch(error => console.error(error));
userService.createUser({ name: 'Jane Doe', email: 'jane@example.com' })
.then(user => console.log(`Created user with ID: ${user.id}`))
.catch(error => console.error(error));
4.3 상태 관리와 통합하기
API 호출 결과를 상태 관리 라이브러리(예: Redux)와 통합할 수 있습니다:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
interface UserState {
users: User[];
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
}
const initialState: UserState = {
users: [],
status: 'idle',
error: null
};
export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
const response = await api.get<User[]>('/users');
return response;
});
const userSlice = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'succeeded';
state.users = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message || null;
});
},
});
export default userSlice.reducer;
// 컴포넌트에서 사용
import { useDispatch, useSelector } from 'react-redux';
import { fetchUsers } from './userSlice';
function UserList() {
const dispatch = useDispatch();
const { users, status, error } = useSelector((state: RootState) => state.users);
useEffect(() => {
if (status === 'idle') {
dispatch(fetchUsers());
}
}, [status, dispatch]);
if (status === 'loading') {
return <div>Loading...</div>;
}
if (status === 'failed') {
return <div>Error: {error}</div>;
}
return (
<ul>
{users.map(user => (
<li key="{user.id}">{user.name}</li>
))}
</ul>
);
}
4.4 테스트 작성하기
TypeScript를 사용하면 API 호출에 대한 단위 테스트와 통합 테스트를 더 쉽게 작성할 수 있습니다:
import { ApiClient } from './apiClient';
import { UserService } from './userService';
// Mock ApiClient
jest.mock('./apiClient');
describe('UserService', () => {
let apiClient: jest.Mocked<apiclient>;
let userService: UserService;
beforeEach(() => {
apiClient = new ApiClient('') as jest.Mocked<apiclient>;
userService = new UserService(apiClient);
});
test('getUsers should return an array of users', async () => {
const mockUsers: User[] = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Doe', email: 'jane@example.com' },
];
apiClient.get.mockResolvedValue(mockUsers);
const users = await userService.getUsers();
expect(users).toEqual(mockUsers);
expect(apiClient.get).toHaveBeenCalledWith('/users');
});
test('createUser should return a new user', async () => {
const newUser = { name: 'New User', email: 'new@example.com' };
const createdUser: User = { id: 3, ...newUser };
apiClient.post.mockResolvedValue(createdUser);
const user = await userService.createUser(newUser);
expect(user).toEqual(createdUser);
expect(apiClient.post).toHaveBeenCalledWith('/users', newUser);
});
});
</apiclient></apiclient>
4.5 에러 처리 및 로깅
TypeScript를 사용하여 더 강력한 에러 처리 및 로깅 시스템을 구현할 수 있습니다:
class ApiError extends Error {
constructor(public status: number, message: string) {
super(message);
this.name = 'ApiError';
}
}
class Logger {
static error(message: string, error?: Error) {
console.error(`[ERROR] ${message}`, error);
// 여기에 외부 로깅 서비스로 에러를 보내는 로직을 추가할 수 있습니다.
}
static info(message: string) {
console.log(`[INFO] ${message}`);
}
}
class ApiClient {
// ... 이전 코드 ...
private async request<t>(url: string, options: RequestInit = {}): Promise<t> {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new ApiError(response.status, `HTTP error! status: ${response.status}`);
}
return response.json() as Promise<t>;
} catch (error) {
if (error instanceof ApiError) {
Logger.error(`API Error: ${error.message}`, error);
} else {
Logger.error('Unexpected error occurred', error as Error);
}
throw error;
}
}
async get<t>(endpoint: string): Promise<t> {
return this.request<t>(`${this.baseUrl}${endpoint}`);
}
async post<t>(endpoint: string, data: any): Promise<t> {
return this.request<t>(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
}
}
// 사용 예
const api = new ApiClient('https://api.example.com');
api.get<user>('/users')
.then(users => {
Logger.info(`Retrieved ${users.length} users`);
users.forEach(user => console.log(user.name));
})
.catch(error => {
if (error instanceof ApiError) {
console.error(`API Error ${error.status}: ${error.message}`);
} else {
console.error('An unexpected error occurred:', error);
}
});
</user></t></t></t></t></t></t></t></t></t>
4.6 환경 설정 및 타입 정의 파일
프로젝트의 환경 설정을 위해 TypeScript 설정 파일(tsconfig.json)과 타입 정의 파일을 활용할 수 있습니다:
// tsconfig.json
{
"compilerOptions": {
"target": "es6",
"module": "esnext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}
// types.d.ts
declare module '*.svg' {
const content: any;
export default content;
}
declare module '*.png' {
const content: any;
export default content;
}
interface User {
id: number;
name: string;
email: string;
}
interface ApiResponse<t> {
data: T;
message: string;
status: number;
}
</t>
이러한 설정과 타입 정의를 통해 프로젝트 전반에 걸쳐 일관된 타입 체크와 자동 완성 기능을 활용할 수 있습니다.
4.7 성능 최적화
TypeScript를 사용하여 API 호출의 성능을 최적화할 수 있습니다. 예를 들어, 캐싱 메커니즘을 구현할 수 있습니다:
class CacheManager<t> {
private cache: Map<string data: t timestamp: number> = new Map();
private ttl: number;
constructor(ttl: number = 60000) { // 기본 TTL: 1분
this.ttl = ttl;
}
set(key: string, data: T): void {
this.cache.set(key, { data, timestamp: Date.now() });
}
get(key: string): T | null {
const cached = this.cache.get(key);
if (!cached) return null;
if (Date.now() - cached.timestamp > this.ttl) {
this.cache.delete(key);
return null;
}
return cached.data;
}
clear(): void {
this.cache.clear();
}
}
class ApiClient {
private cache: CacheManager<any>;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
this.cache = new CacheManager();
}
async get<t>(endpoint: string, useCache: boolean = true): Promise<t> {
const cacheKey = `GET:${endpoint}`;
if (useCache) {
const cachedData = this.cache.get(cacheKey);
if (cachedData) return cachedData as T;
}
const data = await this.request<t>(`${this.baseUrl}${endpoint}`);
this.cache.set(cacheKey, data);
return data;
}
// ... 다른 메서드들 ...
}
// 사용 예
const api = new ApiClient('https://api.example.com');
async function fetchUsersTwice() {
console.time('First call');
await api.get<user>('/users');
console.timeEnd('First call');
console.time('Second call (cached)');
await api.get<user>('/users');
console.timeEnd('Second call (cached)');
}
fetchUsersTwice();
</user></user></t></t></t></any></string></t>
이러한 최적화 기법을 통해 반복적인 API 호출의 성능을 크게 향상시킬 수 있습니다.
이렇게 Fetch API와 TypeScript를 결합하여 실제 프로젝트에 적용하면, 코드의 안정성과 유지보수성이 크게 향상됩니다. 또한, 개발자 경험도 개선되어 생산성이 높아집니다. 다음 섹션에서는 이러한 기술을 사용할 때의 모범 사례와 주의사항에 대해 알아보겠습니다. 🚀