TanStack Start를 사용하여 외부 API 호출하기
이 가이드에서는 라우트 로더를 사용하여 외부 API 호출을 TanStack Start 애플리케이션에 통합하는 방법을 설명합니다. TanStack Start에서 TMDB API를 사용해 인기 영화를 가져오고, TanStack Start 앱에서 데이터를 가져오는 방법을 알아봅니다.
이 튜토리얼의 전체 코드는 GitHub에서 확인할 수 있습니다.
학습할 내용
- TanStack Start에서 외부 API 통합 설정하기
- 서버 측 데이터 가져오기를 위한 라우트 로더 구현하기
- 가져온 데이터로 반응형 UI 컴포넌트 구축하기
- 로딩 상태 처리 및 오류 관리하기
사전 요구 사항
- React와 TypeScript에 대한 기본 지식
- 컴퓨터에 Node.js(v18 이상)와
pnpm가 설치되어 있어야 합니다 - TMDB API 키(themoviedb.org에서 무료로 발급)
알아두면 좋은 내용
TanStack Start 프로젝트 설정하기
먼저 새 TanStack Start 프로젝트를 생성합니다:
pnpx create-start-app movie-discovery
cd movie-discovery
이 스크립트를 실행하면 몇 가지 설정 질문이 표시됩니다. 원하는 옵션을 선택하거나 Enter 키를 눌러 기본값을 적용할 수 있습니다.
선택적으로 --add-on 플래그를 전달하여 Shadcn, Clerk, Convex, TanStack Query 등의 옵션을 사용할 수 있습니다.
설정이 완료되면 의존성을 설치하고 개발 서버를 시작합니다:
pnpm i
pnpm dev
프로젝트 구조 이해하기
이 시점의 프로젝트 구조는 다음과 같아야 합니다:
/movie-discovery
├── src/
│ ├── routes/
│ │ ├── __root.tsx # Root layout
│ │ ├── index.tsx # Home page
│ │ └── fetch-movies.tsx # Movie fetching route
│ ├── types/
│ │ └── movie.ts # Movie type definitions
│ ├── router.tsx # Router configuration
│ ├── routeTree.gen.ts # Generated route tree
│ └── styles.css # Global styles
├── public/ # Static assets
├── vite.config.ts or rsbuild.config.ts # TanStack Start configuration
├── package.json # Project dependencies
└── tsconfig.json # TypeScript configuration
프로젝트 설정이 완료되면 localhost:3000에서 앱에 접근할 수 있습니다. 기본 TanStack Start 시작 페이지가 표시되어야 합니다.
1단계: TMDB_AUTH_TOKEN이 포함된 .env 파일 설정하기
TMDB API에서 영화 데이터를 가져오려면 인증 토큰이 필요합니다. themoviedb.org에서 무료로 발급받을 수 있습니다.
먼저 API 키를 위한 환경 변수를 설정합니다. 프로젝트 루트에 .env 파일을 생성합니다:
touch .env
이 파일에 TMDB API 토큰을 추가합니다:
TMDB_AUTH_TOKEN=your_bearer_token_here
중요: API 키를 안전하게 보호하려면 .env을 .gitignore 파일에 반드시 추가합니다.
2단계: 데이터 타입 정의
영화 데이터를 위한 TypeScript 인터페이스를 생성합니다. src/types/movie.ts에 새 파일을 생성합니다:
// src/types/movie.ts
export interface Movie {
id: number
title: string
overview: string
poster_path: string | null
backdrop_path: string | null
release_date: string
vote_average: number
popularity: number
}
export interface TMDBResponse {
page: number
results: Movie[]
total_pages: number
total_results: number
}
3단계: API 데이터 가져오기 함수가 포함된 라우트 생성
TMDB API를 호출하기 위해 서버에서 데이터를 가져오는 서버 함수를 생성합니다. 이 방식은 API 자격 증명을 클라이언트에 절대 노출하지 않아 안전하게 보호합니다.
TMDB API에서 데이터를 가져오는 라우트를 생성합니다. src/routes/fetch-movies.tsx에 새 파일을 생성합니다:
// src/routes/fetch-movies.tsx
import { createFileRoute } from '@tanstack/react-router'
import type { Movie, TMDBResponse } from '../types/movie'
import { createServerFn } from '@tanstack/react-start'
const API_URL =
'https://api.themoviedb.org/3/discover/movie?include_adult=false&include_video=false&language=en-US&page=1&sort_by=popularity.desc'
const fetchPopularMovies = createServerFn().handler(
async (): Promise<TMDBResponse> => {
const response = await fetch(API_URL, {
headers: {
accept: 'application/json',
Authorization: `Bearer ${process.env.TMDB_AUTH_TOKEN}`,
},
})
if (!response.ok) {
throw new Error(`Failed to fetch movies: ${response.statusText}`)
}
return response.json()
},
)
export const Route = createFileRoute('/fetch-movies')({
component: MoviesPage,
loader: async (): Promise<{ movies: Movie[]; error: string | null }> => {
try {
const moviesData = await fetchPopularMovies()
return { movies: moviesData.results, error: null }
} catch (error) {
console.error('Error fetching movies:', error)
return { movies: [], error: 'Failed to load movies' }
}
},
})
여기서 수행되는 작업:
createServerFn()은 서버에서만 실행되는 전용 함수를 생성하여TMDB_AUTH_TOKEN환경 변수가 클라이언트에 노출되지 않도록 합니다. 서버 함수는 TMDB API에 인증된 요청을 보내고 파싱된 JSON 응답을 반환합니다.- 사용자가 /fetch-movies를 방문하면 라우트 로더가 서버에서 실행되며, 페이지를 렌더링하기 전에 서버 함수를 호출합니다
- 오류 처리를 통해 컴포넌트가 항상 유효한 데이터 구조, 즉 영화 목록 또는 오류 메시지가 포함된 빈 배열을 받도록 합니다
- 이 패턴은 서버 측 렌더링, 자동 타입 안전성, 안전한 API 자격 증명 처리를 기본으로 제공합니다.
4단계: 영화 컴포넌트 구축
이제 영화 데이터를 표시할 컴포넌트를 생성합니다. 다음 컴포넌트를 동일한 fetch-movies.tsx 파일에 추가합니다:
// MovieCard component
const MovieCard = ({ movie }: { movie: Movie }) => {
return (
<div
className="bg-white/10 border border-white/20 rounded-lg overflow-hidden backdrop-blur-sm shadow-md hover:shadow-xl transition-all duration-300 hover:scale-105"
aria-label={`Movie: ${movie.title}`}
role="group"
>
{movie.poster_path && (
<img
src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`}
alt={movie.title}
className="w-full h-64 object-cover"
/>
)}
<div className="p-4">
<MovieDetails movie={movie} />
</div>
</div>
)
}
// MovieDetails component
const MovieDetails = ({ movie }: { movie: Movie }) => {
return (
<>
<h3 className="text-lg font-semibold mb-2 line-clamp-2">{movie.title}</h3>
<p className="text-sm text-gray-300 mb-3 line-clamp-3 h-10">
{movie.overview}
</p>
<div className="flex justify-between items-center text-xs text-gray-400">
<span>{movie.release_date}</span>
<span className="flex items-center">
⭐️ {movie.vote_average.toFixed(1)}
</span>
</div>
</>
)
}
5단계: MoviesPage 컴포넌트 생성
마지막으로 로더 데이터를 사용하는 기본 컴포넌트를 생성합니다:
// MoviesPage component
const MoviesPage = () => {
const { movies, error } = Route.useLoaderData()
return (
<div
className="flex items-center justify-center min-h-screen p-4 text-white"
style={{
backgroundColor: '#000',
backgroundImage:
'radial-gradient(ellipse 60% 60% at 0% 100%, #444 0%, #222 60%, #000 100%)',
}}
role="main"
aria-label="Popular Movies Section"
>
<div className="w-full max-w-6xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10">
<h1 className="text-3xl mb-6 font-bold text-center">Popular Movies</h1>
{error && (
<div
className="text-red-400 text-center mb-4 p-4 bg-red-900/20 rounded-lg"
role="alert"
>
{error}
</div>
)}
{movies.length > 0 ? (
<div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
aria-label="Movie List"
>
{movies.slice(0, 12).map((movie) => (
<MovieCard key={movie.id} movie={movie} />
))}
</div>
) : (
!error && (
<div className="text-center text-gray-400" role="status">
Loading movies...
</div>
)
)}
</div>
</div>
)
}
모든 요소가 함께 작동하는 방식 이해
sequenceDiagram
autonumber
actor U as User
participant R as Router (TanStack Start)
participant L as Route Loader (/fetch-movies)
participant A as External API (TMDB)
participant V as MoviesPage (UI)
U->>R: Navigate to /fetch-movies
R->>L: Invoke loader (server-side)
L->>A: GET /movie/popular\nAuthorization: Bearer <TOKEN>
A-->>L: JSON TMDBResponse
alt response.ok
L-->>R: { movies, error: null }
R->>V: Render SSR with movies
V-->>U: HTML with movie grid
else non-ok / error
L-->>R: { movies: [], error: "Failed to load movies" }
R->>V: Render SSR with error alert
V-->>U: HTML with error state
end
note over L,V: Loader validates response.ok,\nreturns data or error for initial render
애플리케이션의 각 부분이 함께 작동하는 방식을 살펴봅니다:
- 라우트 로더: 사용자가
/fetch-movies을 방문하면 로더 함수가 서버에서 실행됩니다 - API 호출: 로더가
fetchPopularMovies()을 호출하면 TMDB에 HTTP 요청을 보냅니다 - 서버 측 렌더링: 서버에서 데이터를 가져와 클라이언트 측의 부하를 줄입니다
- 컴포넌트 렌더링:
MoviesPage컴포넌트가Route.useLoaderData()을 통해 데이터를 받습니다 - UI 렌더링: 가져온 데이터로 영화 카드가 렌더링됩니다
6단계: 애플리케이션 테스트
이제 http://localhost:3000/fetch-movies에 방문하여 애플리케이션을 테스트할 수 있습니다. 모든 항목이 올바르게 설정되었다면 포스터, 제목, 평점이 포함된 인기 영화 그리드가 표시됩니다. 앱은 다음과 같이 표시됩니다:

결론
TanStack Start를 사용해 외부 API와 통합되는 영화 탐색 앱을 성공적으로 구축했습니다. 이 튜토리얼에서는 서버 측 데이터 가져오기에 라우트 로더를 사용하고 외부 데이터로 UI 컴포넌트를 구축하는 방법을 살펴보았습니다.
TanStack Start에서 빌드 시점에 데이터를 가져오는 방식은 블로그 게시물이나 제품 페이지와 같은 정적 콘텐츠에 적합하지만, 대화형 앱에는 적합하지 않습니다. 실시간 업데이트, 캐싱 또는 무한 스크롤과 같은 기능이 필요하다면 클라이언트 측에서 TanStack Query를 사용해야 합니다. TanStack Query를 사용하면 내장 캐싱, 백그라운드 업데이트 및 원활한 사용자 상호작용을 통해 동적 데이터를 쉽게 처리할 수 있습니다. 정적 콘텐츠에는 TanStack Start를, 대화형 기능에는 TanStack Query를 사용하면 빠르게 로드되는 페이지와 사용자가 기대하는 모든 최신 기능을 함께 제공할 수 있습니다.