Skip to content

5.6장: Blade + Fetch를 통한 데이터 표시

학습 시간: 50분


1. 하이브리드 접근 방식: 두 세계의 최고

우리는 페이지를 두 가지 방법으로 구축할 수 있습니다:

  1. 전체 서버 측 렌더링 (SSR): Laravel은 행성 목록을 포함한 모든 HTML을 생성합니다. 업데이트(삭제, 추가) 시 페이지가 완전히 다시 로드됩니다.
  2. 전체 클라이언트 측 렌더링 (CSR): Laravel은 빈 HTML "셸"을 반환하고, JavaScript는 API에서 모든 데이터를 요청하여 클라이언트에서 렌더링합니다. (이는 단일 페이지 애플리케이션 - SPA 접근 방식입니다.)

우리의 선택은 하이브리드 접근 방식입니다:

  • 첫 로드 (SSR): Laravel은 준비된 행성 목록이 포함된 페이지를 즉시 반환합니다. 이는 빠르고 SEO에 좋습니다. 사용자는 즉시 콘텐츠를 볼 수 있습니다.
  • 후속 작업 (CSR): JavaScript는 사용자 작업(버튼 클릭)을 가로채고 API와 상호 작용하여 전체 페이지를 다시 로드하지 않고 페이지의 필요한 부분만 업데이트합니다.

💡 우주 비유:

다리에 들어서면 중앙 관제 센터(SSR)에서 인쇄된 주요 항해 지도가 즉시 제공됩니다. 이미 손에 있으므로 기다릴 필요가 없습니다. 하지만 그 후 태블릿에서 "실시간 모드"(CSR)를 활성화하면 위성으로부터 실시간 업데이트를 받기 시작하여 지도에 있는 객체를 다시 그립니다.


2. 1단계: 페이지 준비

우리는 행성 목록 페이지인 resources/views/planets/index.blade.php를 사용하여 작업할 것입니다. 이 페이지는 컨트롤러에서 전달된 데이터를 이미 표시할 수 있습니다. 이제 우리는 JS를 통해 작동할 제어 요소를 추가할 것입니다.

"목록 새로고침" 버튼과 알림 컨테이너를 추가합니다:

    <div class="controls">
        <h2>알려진 모든 행성 목록</h2>
        <button id="refresh-btn">API를 통해 새로고침</button>
    </div>
    <div id="notification-area" class="notification"></div>
    <hr>
    {{-- 이 div는 동적 업데이트를 위한 우리의 컨테이너가 될 것입니다 --}}
    <div id="planet-list-container" class="planet-list">
        {{-- 초기 목록을 렌더링하는 "자식" 뷰를 포함합니다 --}}
        @include('planets.partials.list', ['planets' => $planets])
    </div>

@include('planets.partials.list', ...)에 주목하세요. 우리는 목록 표시 로직을 별도의 재사용 가능한 파일로 분리했습니다.


2단계: 재사용 가능한 "부분" 뷰 (Partial) 생성

반복되는 부분을 별도의 파일로 분리하는 것은 좋은 습관입니다.

resources/views/planets/partials/list.blade.php 파일을 생성합니다:

@forelse($planets as $planet)
    <div class="planet-card" id="planet-card-{{ $planet->id }}">
        <h3>{{ $planet->name }}</h3>
        <p>태양계: {{ $planet->solar_system }}</p>
        <p>직경: {{ number_format($planet->size_km, 0, '.', ' ') }} km</p>
        <a href="{{ route('planets.show', $planet) }}">자세히 알아보기 &rarr;</a>
        <button class="delete-btn" data-id="{{ $planet->id }}" data-url="{{ route('api.planets.destroy', $planet) }}">
            폐기
        </button>
    </div>
@empty
    <p>데이터베이스에 행성이 없습니다.</p>
@endforelse
  • 중요: 삭제 버튼의 URL이 이제 API 경로를 위해 생성된다는 점에 유의하십시오: route('api.planets.destroy', $planet). 이를 위해서는 routes/api.php에 다음 이름이 지정된 리소스가 있는지 확인하십시오: Route::apiResource('planets', ...)->name('api.planets');

3단계: 동적 업데이트를 위한 JavaScript 작성

이제 가장 흥미로운 부분입니다. 버튼을 통해 API에서 최신 행성 목록을 요청하고 이를 다시 렌더링하는 JavaScript를 만들 것입니다.

public/js/planet-manager.js 파일을 생성하고 layouts/app.blade.php에 연결합니다.

document.addEventListener('DOMContentLoaded', () => {
    const refreshBtn = document.getElementById('refresh-btn');
    const planetListContainer = document.getElementById('planet-list-container');
    const notificationArea = document.getElementById('notification-area');

    // 알림 표시 함수
    function showNotification(message, isError = false) {
        notificationArea.textContent = message;
        notificationArea.className = isError ? 'notification error' : 'notification success';
        setTimeout(() => {
            notificationArea.textContent = '';
            notificationArea.className = 'notification';
        }, 3000);
    }

    // 행성 카드 하나를 렌더링하는 함수
    function createPlanetCardHtml(planet) {
        // 중요: 우리는 partial 뷰에서와 동일한 HTML을 생성합니다
        return `
            <div class="planet-card" id="planet-card-${planet.id}">
                <h3>${planet.name}</h3>
                <p>태양계: ${planet.solar_system}</p>
                <p>직경: ${new Intl.NumberFormat().format(planet.size_km)} km</p>
                <a href="/planets/${planet.id}">자세히 알아보기 &rarr;</a>
                <button class="delete-btn" data-id="${planet.id}" data-url="/api/planets/${planet.id}">
                    폐기 (JS)
                </button>
            </div>
        `;
    }

    // 행성 목록을 요청하고 다시 렌더링하는 함수
    async function fetchAndRenderPlanets() {
        showNotification('궤도 위성에서 최신 데이터를 요청 중...');
        try {
            const response = await fetch('/api/planets', {
                headers: { 'Accept': 'application/json' }
            });

            if (!response.ok) {
                throw new Error('데이터를 가져오는 중 네트워크 오류가 발생했습니다.');
            }

            const planets = await response.json(); // Laravel은 기본적으로 페이지가 매겨진 리소스에 대해 { data: [...] }를 반환합니다.

            planetListContainer.innerHTML = ''; // 이전 목록 지우기

            if (planets.data.length === 0) {
                planetListContainer.innerHTML = '<p>데이터베이스에 행성이 없습니다.</p>';
            } else {
                planets.data.forEach(planet => {
                    const cardHtml = createPlanetCardHtml(planet);
                    planetListContainer.innerHTML += cardHtml;
                });
            }
            showNotification('데이터가 성공적으로 업데이트되었습니다!', false);
        } catch (error) {
            console.error('행성 목록 업데이트 중 오류 발생:', error);
            showNotification(error.message, true);
        }
    }

    // 버튼에 이벤트 핸들러 연결
    if (refreshBtn) {
        refreshBtn.addEventListener('click', fetchAndRenderPlanets);
    }

    // 여기로 이전 장의 삭제 로직을 옮겨와서
    // 모든 JS가 한 곳에 있도록 할 수 있습니다.
});

3. 최종 확인

  1. 서버를 시작합니다 (php artisan serve를 실행하거나 Herd가 작동하는지 확인).
  2. 필요하다면 데이터베이스를 다시 생성합니다: php artisan migrate:fresh --seed.
  3. 브라우저에서 /planets 페이지를 엽니다.
    • 서버에서 생성된 행성 목록을 즉시 볼 수 있어야 합니다.
  4. "API를 통해 새로고침" 버튼을 클릭합니다.
    • 로딩 알림이 표시됩니다.
    • 목록이 잠시 사라졌다가 다시 나타나야 하며, 이번에는 API에서 받은 데이터를 기반으로 JavaScript에 의해 생성될 것입니다.

하이브리드 모델을 성공적으로 구현했습니다!


복습 퀴즈

1. 하이브리드 렌더링 방식(SSR + CSR)이란 무엇인가요?

2. 초기 서버 렌더링(SSR)의 주요 장점은 무엇인가요?

3. 예제에서 `@include('planets.partials.list')`를 사용하는 이유는 무엇인가요?

4. JavaScript 코드에서 카드 HTML 구조를 중복하고 있습니다. 이를 피할 수 있는 더 발전된 방법은 무엇일까요?

5. API(`/api/planets`)와 JavaScript 코드(`createPlanetCardHtml`)가 일관된 데이터/HTML을 생성하는 것이 왜 중요한가요?


🚀 5장 완료를 축하합니다!

Blade의 기초부터 대화형 하이브리드 페이지 생성에 이르기까지 긴 여정을 거쳤습니다. 다음을 배웠습니다:

  • Blade 템플릿 및 레이아웃 생성 및 사용.
  • CRUD 작업을 위한 웹 경로 및 컨트롤러 구성.
  • CSRF 토큰을 사용하여 웹 폼 및 AJAX 요청 보호.
  • 페이지를 새로 고치지 않고 API와 동적으로 상호 작용하도록 JavaScript 통합.

당신의 비행 제어 센터는 이제 완벽하게 작동하고, 안전하며, 대화형입니다. 이제 다음 큰 단계인 이 접근 방식을 다른 프레임워크와 비교하고 프로덕션에 대한 모범 사례를 배우는 준비가 되었습니다.