로딩 UI 구현하기
현대 웹 애플리케이션에서는 데이터 로딩이나 비동기 작업이 발생하는 동안 사용자에게 적절한 피드백을 제공하는 것이 매우 중요합니다. 아무런 반응이 없는 화면은 사용자에게 혼란과 불편함을 줄 수 있으며, 이는 애플리케이션 이탈로 이어질 수도 있습니다. Next.js App Router는 이러한 사용자 경험을 개선하기 위해 데이터가 로드되는 동안 보여줄 로딩 UI(Loading UI) 를 쉽게 구현할 수 있는 기능을 제공합니다.
이 절에서는 loading.tsx
파일을 사용하여 로딩 UI를 구현하는 방법, 그리고 이 기능이 React의 Suspense와 어떻게 연동되는지 자세히 알아보겠습니다.
로딩 UI의 필요성과 loading.tsx
의 역할
사용자가 페이지에 접속하거나 특정 작업을 수행할 때, 백엔드에서 데이터를 가져오는 데는 시간이 소요될 수 있습니다. 이 짧은 시간 동안 사용자에게 "무언가 진행 중"이라는 시각적인 단서를 제공하는 것이 로딩 UI의 역할입니다.
Next.js App Router는 특정 라우트 세그먼트 내에서 데이터 로딩이 발생할 때 자동으로 활성화되는 loading.tsx
파일을 통해 로딩 UI를 구현합니다.
loading.tsx
의 주요 특징
- 동일한 라우트 세그먼트에 위치:
loading.tsx
파일은 로딩 상태를 표시하고자 하는page.tsx
파일과 동일한 폴더(라우트 세그먼트)에 위치해야 합니다. - Suspense와 연동:
loading.tsx
는 React의 Suspense 경계(Boundary) 역할의 일부로 작동합니다. 데이터 페칭과 같은 비동기 작업이 시작되면loading.tsx
가 렌더링되고, 데이터 로딩이 완료되면 실제 페이지 (page.tsx
)가 그 자리를 대체합니다. - 스트리밍(Streaming): Next.js는 서버에서 HTML을 스트리밍하는 기능을 지원합니다. 이는 페이지의 일부가 먼저 사용자에게 표시된 후, 데이터 로딩이 완료되는 대로 나머지 부분이 점진적으로 나타나게 하여 사용자 경험을 향상시킵니다.
loading.tsx
는 이 스트리밍의 초기 폴백(fallback) 콘텐츠로 사용됩니다. - 기본적으로 서버 컴포넌트:
loading.tsx
파일도 기본적으로 서버 컴포넌트로 동작합니다. 물론 클라이언트 컴포넌트로 만들 수도 있습니다.
loading.tsx
구현 실습
이전 절에서 만들었던 게시물 목록 페이지(src/app/posts/page.tsx
)와 상세 페이지(src/app/posts/[id]/page.tsx
)에 로딩 UI를 추가하여 데이터 로딩 시 사용자에게 피드백을 제공해 봅시다.
-
src/app/posts/loading.tsx
파일 생성:src/app/posts
폴더 안에loading.tsx
파일을 생성합니다.my-next-app/ └── src/ └── app/ ├── posts/ │ ├── [id]/ │ │ └── page.tsx │ ├── loading.tsx <- 여기에 loading.tsx 생성 │ └── page.tsx └── ...
-
src/app/posts/loading.tsx
내용 작성: 데이터를 로드하는 동안 사용자에게 표시될 간단한 UI를 작성합니다.src/app/posts/loading.tsx // src/app/posts/loading.tsx import React from 'react'; export default function PostsLoading() { return ( <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '200px', backgroundColor: '#f8f8f8', border: '1px solid #ddd', borderRadius: '8px', padding: '20px', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }}> <div className="spinner" style={{ border: '4px solid rgba(0, 0, 0, 0.1)', width: '36px', height: '36px', borderRadius: '50%', borderLeftColor: '#09f', animation: 'spin 1s ease infinite' }}></div> <p style={{ marginTop: '15px', fontSize: '1.1em', color: '#555' }}>게시물 목록을 불러오는 중입니다...</p> {/* CSS 애니메이션을 위한 스타일 태그 (실제로는 globals.css에 넣는 것이 더 좋습니다) */} <style jsx>{` @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `}</style> </div> ); }
설명
- 간단한 로딩 스피너와 메시지를 포함합니다.
- CSS 애니메이션을 인라인으로 추가했지만, 실제 프로젝트에서는
globals.css
파일이나 모듈 CSS 파일에 정의하는 것이 좋습니다.
-
src/app/posts/[id]/loading.tsx
파일 생성 (동적 라우트용): 마찬가지로, 게시물 상세 페이지를 위한 로딩 UI도 추가합니다.[id]
폴더 안에loading.tsx
를 생성해야 합니다.my-next-app/ └── src/ └── app/ ├── posts/ │ ├── [id]/ │ │ └── loading.tsx <- 여기에 loading.tsx 생성 │ │ └── page.tsx │ ├── loading.tsx │ └── page.tsx └── ...
-
src/app/posts/[id]/loading.tsx
내용 작성:src/app/posts/[id]/loading.tsx // src/app/posts/[id]/loading.tsx import React from 'react'; export default function PostDetailLoading() { return ( <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '150px', backgroundColor: '#f0faff', border: '1px dashed #09f', borderRadius: '5px', padding: '15px', marginTop: '20px' }}> <p style={{ fontSize: '1.2em', color: '#09f' }}>게시물 내용을 불러오는 중입니다...</p> <div className="dot-spinner" style={{ display: 'flex', gap: '5px', marginTop: '10px' }}> <div style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: '#09f', animation: 'blink 1s infinite' }}></div> <div style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: '#09f', animation: 'blink 1s infinite 0.2s' }}></div> <div style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: '#09f', animation: 'blink 1s infinite 0.4s' }}></div> </div> <style jsx>{` @keyframes blink { 0%, 100% { opacity: 0.2; } 50% { opacity: 1; } } `}</style> </div> ); }
실습 확인:
개발 서버(npm run dev
)를 실행한 후,
http://localhost:3000/posts
로 접속합니다. 네트워크 지연이 시뮬레이션되지 않으면 로딩 UI가 너무 빨리 사라질 수 있습니다. 개발자 도구(F12)의 Network 탭에서 "Fast 3G" 또는 "Slow 3G"로 네트워크 속도를 조절해 보세요. 게시물 목록이 나타나기 전에 "게시물 목록을 불러오는 중입니다..." 메시지가 잠시 표시될 것입니다.- 게시물 목록에서 "더 보기" 링크를 클릭하여 상세 페이지로 이동합니다. 마찬가지로 상세 페이지가 로드되기 전에 "게시물 내용을 불러오는 중입니다..." 메시지가 표시될 것입니다.
로딩 UI의 작동 원리: Suspense와 스트리밍
Next.js의 loading.tsx
는 내부적으로 React의 Suspense 컴포넌트를 사용하여 구현됩니다.
Next.js는 라우트 세그먼트를 렌더링할 때, 그 내부에 있는 비동기 작업(예: fetch
호출)을 감지합니다. 비동기 작업이 아직 완료되지 않았다면, Next.js는 가장 가까운 상위 loading.tsx
컴포넌트를 찾아 해당 UI를 폴백(fallback) 으로 렌더링합니다. 데이터 로딩이 완료되면 폴백 UI는 사라지고 실제 page.tsx
컴포넌트의 콘텐츠가 그 자리를 차지하게 됩니다.
이 과정은 서버에서부터 시작됩니다. Next.js 서버는 먼저 레이아웃과 loading.tsx
파일을 포함하는 HTML의 초기 부분을 스트리밍하여 클라이언트에 보냅니다. 클라이언트는 이 초기 HTML을 받아 즉시 렌더링하므로, 사용자는 빈 화면 대신 로딩 UI를 보게 됩니다. 이후 데이터 로딩이 완료되면, 나머지 실제 페이지 콘텐츠 HTML이 스트리밍되어 클라이언트에서 기존 로딩 UI를 대체합니다. 이 방식은 초기 페이지 로딩 시간을 체감상 크게 단축시키는 효과가 있습니다.
로딩 UI의 적용 범위
loading.tsx
파일은 자신이 위치한 라우트 세그먼트와 그 하위의 모든 라우트 세그먼트에 적용됩니다.- 만약 상위 폴더에
loading.tsx
가 있고 하위 폴더에도loading.tsx
가 있다면, 가장 가까운 상위loading.tsx
가 활성화됩니다. - 레이아웃(
layout.tsx
) 파일 내에서 발생하는 데이터 페칭에 대해서도loading.tsx
가 작동합니다. 이는 레이아웃 자체가 비동기 컴포넌트일 때 특히 유용합니다.
클라이언트 훅 사용 시 주의점
loading.tsx
컴포넌트는 기본적으로 서버 컴포넌트입니다. 만약 로딩 UI 내에서 useSearchParams
와 같은 클라이언트 전용 React 훅을 사용해야 한다면, 해당 loading.tsx
파일 상단에 반드시 "use client"
지시어를 추가하여 클라이언트 컴포넌트로 만들어야 합니다.
// src/app/some-route/loading.tsx (클라이언트 훅 사용 예시)
"use client"; // 이 파일을 클라이언트 컴포넌트로 만듭니다.
import { useSearchParams } from 'next/navigation';
export default function LoadingWithParams() {
const searchParams = useSearchParams();
const query = searchParams.get('q');
return (
<div>
<p>데이터 로딩 중입니다: {query ? `"${query}" 검색 결과` : '콘텐츠'}</p>
{/* ... 스피너 등 */}
</div>
);
}
로딩 UI는 사용자 경험을 향상시키는 간단하지만 매우 효과적인 방법입니다. Next.js의 loading.tsx
를 통해 비동기 데이터 로딩 상황에서도 사용자에게 끊김 없는 피드백을 제공하고, 웹 애플리케이션의 체감 성능을 크게 개선할 수 있습니다.