icon
5장 : 페이지 및 레이아웃 컴포넌트

로딩 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를 추가하여 데이터 로딩 시 사용자에게 피드백을 제공해 봅시다.

  1. 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
            └── ...
  2. 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 파일에 정의하는 것이 좋습니다.
  3. 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
            └── ...
  4. 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
// 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를 통해 비동기 데이터 로딩 상황에서도 사용자에게 끊김 없는 피드백을 제공하고, 웹 애플리케이션의 체감 성능을 크게 개선할 수 있습니다.