TypeScript에서 타입 안전 API 호출을 마스터하여 강력하고 유지 관리 가능하며 오류 없는 웹 애플리케이션을 만드세요. 모범 사례 및 고급 기술을 알아보세요.
Type-Safe API Calls with TypeScript: A Comprehensive Guide
현대 웹 개발에서 API와 상호 작용하는 것은 기본적인 작업입니다. TypeScript는 강력한 타입 시스템을 통해 타입 안전 API 호출을 가능하게 함으로써 애플리케이션의 안정성과 유지 관리성을 보장하는 데 중요한 이점을 제공합니다. 이 가이드에서는 TypeScript의 기능을 활용하여 모범 사례, 고급 기술 및 실제 예제를 다루면서 강력하고 오류 없는 API 상호 작용을 구축하는 방법을 살펴봅니다.
Why Type Safety Matters for API Calls
API를 사용할 때 기본적으로 외부 소스에서 오는 데이터를 처리하게 됩니다. 이 데이터가 예상한 형식과 항상 일치하지 않을 수 있으므로 런타임 오류 및 예기치 않은 동작이 발생할 수 있습니다. 타입 안전성은 수신하는 데이터가 미리 정의된 구조를 준수하는지 확인하여 개발 프로세스 초기에 잠재적인 문제를 감지함으로써 중요한 보호 계층을 제공합니다.
- Reduced Runtime Errors: 컴파일 시 타입 검사를 통해 프로덕션 환경에 도달하기 전에 타입 관련 오류를 식별하고 수정할 수 있습니다.
- Improved Code Maintainability: 명확한 타입 정의는 코드를 더 쉽게 이해하고 수정할 수 있도록 하여 리팩토링 중에 버그가 발생할 위험을 줄입니다.
- Enhanced Code Readability: 타입 주석은 중요한 문서를 제공하여 개발자가 예상되는 데이터 구조를 더 쉽게 이해할 수 있도록 합니다.
- Better Developer Experience: 타입 검사 및 자동 완성에 대한 IDE 지원은 개발자 경험을 크게 향상시키고 오류 가능성을 줄입니다.
Setting Up Your TypeScript Project
API 호출을 시작하기 전에 TypeScript 프로젝트가 설정되어 있는지 확인하십시오. 처음부터 시작하는 경우 다음을 사용하여 새 프로젝트를 초기화할 수 있습니다.
npm init -y
npm install typescript --save-dev
tsc --init
이렇게 하면 기본 TypeScript 컴파일러 옵션이 있는 `tsconfig.json` 파일이 생성됩니다. 이러한 옵션을 프로젝트의 요구 사항에 맞게 사용자 지정할 수 있습니다. 예를 들어 더 엄격한 타입 검사를 위해 strict 모드를 활성화할 수 있습니다.
// tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Defining Types for API Responses
타입 안전 API 호출을 달성하는 첫 번째 단계는 API에서 수신할 것으로 예상되는 데이터 구조를 나타내는 TypeScript 타입을 정의하는 것입니다. 이는 일반적으로 `interface` 또는 `type` 선언을 사용하여 수행됩니다.
Using Interfaces
인터페이스는 객체의 모양을 정의하는 강력한 방법입니다. 예를 들어 API에서 사용자 목록을 가져오는 경우 다음과 같은 인터페이스를 정의할 수 있습니다.
interface User {
id: number;
name: string;
email: string;
address?: string; // Optional property
phone?: string; // Optional property
website?: string; // Optional property
company?: {
name: string;
catchPhrase: string;
bs: string;
};
}
속성 이름 뒤의 `?`는 속성이 선택 사항임을 나타냅니다. 이는 특정 필드가 누락될 수 있는 API 응답을 처리하는 데 유용합니다.
Using Types
타입은 인터페이스와 유사하지만 유니온 타입 및 교차 타입을 정의하는 기능을 포함하여 더 많은 유연성을 제공합니다. 타입을 사용하여 위와 동일한 결과를 얻을 수 있습니다.
type User = {
id: number;
name: string;
email: string;
address?: string; // Optional property
phone?: string; // Optional property
website?: string; // Optional property
company?: {
name: string;
catchPhrase: string;
bs: string;
};
};
간단한 객체 구조의 경우 인터페이스와 타입은 종종 상호 교환 가능합니다. 그러나 타입은 더 복잡한 시나리오를 처리할 때 더욱 강력해집니다.
Making API Calls with Axios
Axios는 JavaScript 및 TypeScript에서 API 요청을 보내기 위한 인기 있는 HTTP 클라이언트입니다. 깨끗하고 직관적인 API를 제공하므로 다양한 HTTP 메서드, 요청 헤더 및 응답 데이터를 쉽게 처리할 수 있습니다.
Installing Axios
npm install axios
Making a Typed API Call
Axios를 사용하여 타입 안전 API 호출을 수행하려면 `axios.get` 메서드를 사용하고 제네릭을 사용하여 예상되는 응답 타입을 지정할 수 있습니다.
import axios from 'axios';
async function fetchUsers(): Promise {
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/users');
return response.data;
} catch (error) {
console.error('Error fetching users:', error);
throw error;
}
}
fetchUsers().then(users => {
users.forEach(user => {
console.log(user.name);
});
});
이 예에서 `axios.get
Handling Different HTTP Methods
Axios는 `GET`, `POST`, `PUT`, `DELETE` 및 `PATCH`를 포함한 다양한 HTTP 메서드를 지원합니다. 해당 메서드를 사용하여 다양한 타입의 API 요청을 수행할 수 있습니다. 예를 들어 새 사용자를 생성하려면 `axios.post` 메서드를 사용할 수 있습니다.
async function createUser(user: Omit): Promise {
try {
const response = await axios.post('https://jsonplaceholder.typicode.com/users', user);
return response.data;
} catch (error) {
console.error('Error creating user:', error);
throw error;
}
}
const newUser = {
name: 'John Doe',
email: 'john.doe@example.com',
address: '123 Main St',
phone: '555-1234',
website: 'example.com',
company: {
name: 'Example Corp',
catchPhrase: 'Leading the way',
bs: 'Innovative solutions'
}
};
createUser(newUser).then(user => {
console.log('Created user:', user);
});
이 예에서 `Omit
Using the Fetch API
Fetch API는 HTTP 요청을 보내기 위한 내장 JavaScript API입니다. Axios보다 기본적이지만 TypeScript와 함께 사용하여 타입 안전 API 호출을 달성할 수도 있습니다. 요구 사항에 맞는 경우 종속성을 추가하지 않기 위해 선호할 수 있습니다.
Making a Typed API Call with Fetch
Fetch를 사용하여 타입 안전 API 호출을 수행하려면 `fetch` 함수를 사용한 다음 예상되는 응답 타입을 지정하여 응답을 JSON으로 파싱할 수 있습니다.
async function fetchUsers(): Promise {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: User[] = await response.json();
return data;
} catch (error) {
console.error('Error fetching users:', error);
throw error;
}
}
fetchUsers().then(users => {
users.forEach(user => {
console.log(user.name);
});
});
이 예에서 `const data: User[] = await response.json();`는 TypeScript에게 응답 데이터를 `User` 객체의 배열로 처리해야 한다고 알려줍니다. 이를 통해 TypeScript는 타입 검사 및 자동 완성을 수행할 수 있습니다.
Handling Different HTTP Methods with Fetch
Fetch를 사용하여 다양한 타입의 API 요청을 수행하려면 `method` 및 `body` 옵션과 같은 다양한 옵션과 함께 `fetch` 함수를 사용할 수 있습니다. 예를 들어 새 사용자를 생성하려면 다음 코드를 사용할 수 있습니다.
async function createUser(user: Omit): Promise {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(user)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: User = await response.json();
return data;
} catch (error) {
console.error('Error creating user:', error);
throw error;
}
}
const newUser = {
name: 'John Doe',
email: 'john.doe@example.com',
address: '123 Main St',
phone: '555-1234',
website: 'example.com',
company: {
name: 'Example Corp',
catchPhrase: 'Leading the way',
bs: 'Innovative solutions'
}
};
createUser(newUser).then(user => {
console.log('Created user:', user);
});
Handling API Errors
오류 처리는 API 호출의 중요한 측면입니다. API는 네트워크 연결 문제, 서버 오류 및 잘못된 요청을 포함하여 여러 가지 이유로 실패할 수 있습니다. 애플리케이션이 충돌하거나 예상치 못한 동작을 표시하지 않도록 이러한 오류를 정상적으로 처리하는 것이 중요합니다.
Using Try-Catch Blocks
비동기 코드에서 오류를 처리하는 가장 일반적인 방법은 try-catch 블록을 사용하는 것입니다. 이를 통해 API 호출 중에 발생하는 예외를 catch하고 적절하게 처리할 수 있습니다.
async function fetchUsers(): Promise {
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/users');
return response.data;
} catch (error) {
console.error('Error fetching users:', error);
// Handle the error, e.g., display an error message to the user
throw error; // Re-throw the error to allow calling code to handle it as well
}
}
Handling Specific Error Codes
API는 종종 발생한 오류 타입을 나타내기 위해 특정 오류 코드를 반환합니다. 이러한 오류 코드를 사용하여 보다 구체적인 오류 처리를 제공할 수 있습니다. 예를 들어 404 Not Found 오류와 500 Internal Server Error에 대해 다른 오류 메시지를 표시할 수 있습니다.
async function fetchUser(id: number): Promise {
try {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`);
return response.data;
} catch (error: any) {
if (error.response?.status === 404) {
console.log(`User with ID ${id} not found.`);
return null; // Or throw a custom error
} else {
console.error('Error fetching user:', error);
throw error;
}
}
}
fetchUser(123).then(user => {
if (user) {
console.log('User:', user);
} else {
console.log('User not found.');
}
});
Creating Custom Error Types
보다 복잡한 오류 처리 시나리오의 경우 사용자 지정 오류 타입을 생성하여 다양한 타입의 API 오류를 나타낼 수 있습니다. 이를 통해 보다 구조화된 오류 정보를 제공하고 오류를 보다 효과적으로 처리할 수 있습니다.
class ApiError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
this.name = 'ApiError';
}
}
async function fetchUser(id: number): Promise {
try {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`);
return response.data;
} catch (error: any) {
if (error.response?.status === 404) {
throw new ApiError(404, `User with ID ${id} not found.`);
} else {
console.error('Error fetching user:', error);
throw new ApiError(500, 'Internal Server Error'); //Or any other suitable status code
}
}
}
fetchUser(123).catch(error => {
if (error instanceof ApiError) {
console.error(`API Error: ${error.statusCode} - ${error.message}`);
} else {
console.error('An unexpected error occurred:', error);
}
});
Data Validation
TypeScript의 타입 시스템을 사용하더라도 런타임에 API에서 수신하는 데이터의 유효성을 검사하는 것이 중요합니다. API는 예고 없이 응답 구조를 변경할 수 있으며 TypeScript 타입이 API의 실제 응답과 항상 완벽하게 동기화되지 않을 수 있습니다.
Using Zod for Runtime Validation
Zod는 런타임 데이터 유효성 검사를 위한 널리 사용되는 TypeScript 라이브러리입니다. 데이터의 예상 구조를 설명하는 스키마를 정의한 다음 런타임에 해당 스키마에 대해 데이터의 유효성을 검사할 수 있습니다.
Installing Zod
npm install zod
Validating API Responses with Zod
Zod를 사용하여 API 응답의 유효성을 검사하려면 TypeScript 타입에 해당하는 Zod 스키마를 정의한 다음 `parse` 메서드를 사용하여 데이터의 유효성을 검사할 수 있습니다.
import { z } from 'zod';
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
address: z.string().optional(),
phone: z.string().optional(),
website: z.string().optional(),
company: z.object({
name: z.string(),
catchPhrase: z.string(),
bs: z.string(),
}).optional(),
});
type User = z.infer;
async function fetchUsers(): Promise {
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/users');
const data = z.array(userSchema).parse(response.data);
return data;
} catch (error) {
console.error('Error fetching users:', error);
throw error;
}
}
이 예에서 `z.array(userSchema).parse(response.data)`는 응답 데이터가 `userSchema`를 준수하는 객체의 배열인지 확인합니다. 데이터가 스키마를 준수하지 않으면 Zod가 오류를 발생시키고 이를 적절하게 처리할 수 있습니다.
Advanced Techniques
Using Generics for Reusable API Functions
제네릭을 사용하면 다양한 타입의 데이터를 처리할 수 있는 재사용 가능한 API 함수를 작성할 수 있습니다. 예를 들어 모든 API 엔드포인트에서 데이터를 가져와 올바른 타입으로 반환할 수 있는 제네릭 `fetchData` 함수를 만들 수 있습니다.
async function fetchData(url: string): Promise {
try {
const response = await axios.get(url);
return response.data;
} catch (error) {
console.error(`Error fetching data from ${url}:`, error);
throw error;
}
}
// Usage
fetchData('https://jsonplaceholder.typicode.com/users').then(users => {
console.log('Users:', users);
});
fetchData<{ title: string; body: string }>('https://jsonplaceholder.typicode.com/todos/1').then(todo => {
console.log('Todo', todo)
});
Using Interceptors for Global Error Handling
Axios는 코드에서 처리하기 전에 요청 및 응답을 가로챌 수 있는 인터셉터를 제공합니다. 인터셉터를 사용하여 오류 로깅 또는 사용자에게 오류 메시지 표시와 같은 전역 오류 처리를 구현할 수 있습니다.
axios.interceptors.response.use(
(response) => response,
(error) => {
console.error('Global error handler:', error);
// Display an error message to the user
return Promise.reject(error);
}
);
Using Environment Variables for API URLs
코드에 API URL을 하드 코딩하지 않으려면 환경 변수를 사용하여 URL을 저장할 수 있습니다. 이렇게 하면 개발, 스테이징 및 프로덕션과 같은 다양한 환경에 맞게 애플리케이션을 더 쉽게 구성할 수 있습니다.
`.env` 파일 및 `dotenv` 패키지를 사용하는 예입니다.
// .env
API_URL=https://api.example.com
// Install dotenv
npm install dotenv
// Import and configure dotenv
import * as dotenv from 'dotenv'
dotenv.config()
const apiUrl = process.env.API_URL || 'http://localhost:3000'; // provide a default value
async function fetchData(endpoint: string): Promise {
try {
const response = await axios.get(`${apiUrl}/${endpoint}`);
return response.data;
} catch (error) {
console.error(`Error fetching data from ${apiUrl}/${endpoint}:`, error);
throw error;
}
}
Conclusion
타입 안전 API 호출은 강력하고 유지 관리 가능하며 오류 없는 웹 애플리케이션을 구축하는 데 필수적입니다. TypeScript는 API 응답에 대한 타입을 정의하고, 런타임에 데이터의 유효성을 검사하고, 오류를 정상적으로 처리할 수 있는 강력한 기능을 제공합니다. 이 가이드에 설명된 모범 사례 및 기술을 따르면 API 상호 작용의 품질과 안정성을 크게 향상시킬 수 있습니다.
TypeScript와 Axios 및 Zod와 같은 라이브러리를 사용하면 API 호출이 타입 안전하고 데이터 유효성이 검사되며 오류가 정상적으로 처리되는지 확인할 수 있습니다. 이를 통해 보다 강력하고 유지 관리 가능한 애플리케이션을 만들 수 있습니다.
TypeScript의 타입 시스템을 사용하더라도 항상 런타임에 데이터의 유효성을 검사해야 합니다. API가 변경될 수 있으며 타입이 API의 실제 응답과 항상 완벽하게 동기화되지 않을 수 있습니다. 런타임에 데이터의 유효성을 검사하면 애플리케이션에서 문제가 발생하기 전에 잠재적인 문제를 catch할 수 있습니다.
Happy coding!