라라벨 N+1 문제 해결, Eager Loading으로 데이터베이스 글 목록 출력하기

포스트 썸네일 이미지

이전 포스트에서는 Post 모델에 Fillable 속성을 선언하고, 회원과 글 사이의일대다(HasMany) 관계 정의를 거쳐 실제 저장(Store) 로직까지 완성해 보았다.

오늘은 데이터베이스에 저장된 글들을 가져오면서, 모델 관계 설정을 통해 글쓴이(User) 정보까지 성능 저하 없이 최신순으로 화면에 렌더링하는 과정을 구현해 보겠다.




이전 포스트




글과 회원의 관계 정의


app/Models/Post.php에 추가할 코드

public function user(): BelongsTo
{
    return $this->belongsTo(User::class);
}

비어있던 class 내부에 이 코드를 추가하자.


  • public function user(): BelongsTo: Post 모델에서 user()라는 이름으로 관계 메서드를 정의하며, 이 메서드는 라라벨의 일대다 역관계(BelongsTo) 객체를 반환한다는 뜻이다.
  • return $this->belongsTo(User::class);: '하나의 글(Post)은 특정 한 명의 회원(User)에게 속해 있다'라는 관계를 정의하는 것이다. 이 코드가 있어야만 화면단에서 $post->user->name처럼 글을 통해 작성자의 이름에 접근할 수 있게 된다.




app/Models/Post.php에 추가할 코드

use Illuminate\Database\Eloquent\Relations\BelongsTo;

역시 마지막으로 상단에 이 코드도 잊지 말고 추가해야 한다.

앞으로는 이 임포트Import 하는 코드에 대한 설명은 생략하도록 하겠다.





PostController의 index 메서드 수정


PostController.php의 수정 전 코드

public function index()
{
    return view('posts.index');
}

이전에 만들었던 PostController의 index 메서드의 코드다.

여기를 아래와 같이 수정하자.




PostController.php의 수정 후 코드

public function index()
{
    return view('posts.index', [
        'posts' => Post::with('user')->latest()->get(),
    ]);
}

이전에는 별다른 복잡한 로직 없이, resources/views/posts/index.blade.php 파일을 찾아서 브라우저에 화면을 반환(return view)하는 단순한 역할을 했었다.

그런데 수정 후에는 글의 데이터를 가져올 수 있게 됐다.


  • Post::: posts 테이블의 데이터를 다루기 위해 Post 모델 클래스를 호출한다.
  • with('user'): 정말 중요한 코드다. 라라벨 백엔드 성능 최적화의 핵심인 Eager Loading(즉시 로딩) 기법이다. 데이터베이스에서 글들을 긁어올 때, 그 글을 쓴 유저들의 정보까지 한 방에 미리 묶어서 가져오라고 명령하는 것이다. 이 코드가 없으면 글이 100개일 때 유저 이름을 알아내려고 DB에 100번 더 접근하는 최악의 성능 저하(N+1 문제)가 발생한다. 필수적인 코드다.
  • latest(): 데이터베이스 내부의 created_at(생성일) 컬럼을 기준으로 자동으로 최신순(내림차순, DESC) 정렬을 수행하는 라라벨 쿼리 빌더의 편의 함수다.
  • get(): 데이터베이스에 최종 쿼리를 날려, 조건에 맞는 복수의 글 데이터를 라라벨 컬렉션Collection 객체로 받아온다. 그런데 get()은 모든 데이터를 한꺼번에 가져오기 때문에 나중에 데이터가 많아질 때 문제가 생긴다. 실제 운영하는 웹사이트에서는 페이지네이션 기능을 이용하는데, 이것에 대해서는 다음에 정리하겠다.
  • return view('posts.index', [...]);: 그렇게 받아온 'posts' 컬렉션 데이터를 블레이드 뷰 파일로 넘겨주며 화면을 렌더링Rendering 한다.





index.blade.php 파일에 코드 추가


posts/index.blade.php

<x-app-layout>
    <div class="max-w-2xl mx-auto p-4 sm:p-6 lg:p-8">
        <form method="POST" action="{{ route('posts.store') }}">
            {{-- 생략 --}}
        </form>

        <div class="mt-6 bg-white shadow-sm rounded-lg divide-y">
            @foreach ($posts as $post)
            <div class="p-6 flex space-x-2">
                <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-600 -scale-x-100" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
                    <path stroke-linecap="round" stroke-linejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
                </svg>
                <div class="flex-1">
                    <div class="flex justify-between items-center">
                        <div>
                            <span class="text-gray-800">{{ $post->user->name }}</span>
                            <time datetime="{{ $post->created_at->toIso8601String() }}" class="ml-2 text-sm text-gray-600">{{ $post->created_at->isoFormat('YYYY-MM-DD ddd H:mm') }}</time>
                        </div>
                    </div>
                    <p class="mt-4 text-lg text-gray-900">{{ $post->message }}</p>
                </div>
            </div>
            @endforeach
        </div>
    </div>
</x-app-layout>

<form> 아래쪽에 코드가 추가됐다.


  • @foreach ($posts as $post) 부터 @endforeach: 컨트롤러로부터 전달받은 $posts 컬렉션Collection 객체를 순회하는 블레이드 반복문Loop 지시어다. 데이터베이스에서 조회된 게시글의 개수만큼 내부의 HTML 구조를 반복하여 렌더링 한다. 예를 들어, 데이터베이스에 저장된 글이 5개라면 이 내부의 HTML 코드가 5번 반복 수행되며 5개의 글을 화면에 출력하게 된다.
  • {{ $post->user->name }}: 아까 Post.php 모델에 정의된 user 관계성Relationship 메서드를 통해 해당 게시글을 작성한 사용자의 이름(name) 컬럼 데이터를 화면에 출력하는 코드다. 컨트롤러에서 즉시 로딩Eager Loading을 통해 유저 데이터를 이미 함께 조회해 왔기 때문에, 추가적인 데이터베이스 쿼리 발생 없이 메모리에 로드된 유저 객체의 프로퍼티를 즉시 가져와 표현한다.
  • datetime="{{ $post->created_at->toIso8601String() }}": time 태그의 datetime 속성에는 컴퓨터나 검색엔진 봇이 정확한 날짜와 시각을 인식할 수 있도록 ISO 8601 표준 포맷 문자열을 넣어주는 것이 웹 표준 정석이다. 이렇게 코드를 짜고 브라우저에서 '소스 보기'를 하면, 내부적으로 정확히 대한민국 시간대(+09:00) 정보까지 포함된 완벽한 표준 시간이 깔끔하게 들어가는 것을 확인할 수 있다.
  • {{ $post->created_at->isoFormat('YYYY-MM-DD ddd H:mm') }}: 라라벨의 날짜 컬럼(created_at)은 기본적으로 카본Carbon이라는 날짜 라이브러리 객체로 다뤄진다. 이 코드로 카본 객체가 제공하는 isoFormat() 함수를 사용하여 한국인들이 보기 편한 직관적인 날짜 포맷(예: 2026-06-03 수 13:15)으로 변환하여 화면에 출력한다.
  • {{ $post->message }}: posts 테이블의 message 컬럼에 저장된 게시글의 본문 텍스트 데이터를 그대로 화면에 출력하는 코드다. 중괄호 두 개({{ }})를 사용하는 블레이드 출력 문법은 내부적으로 php의 htmlspecialchars() 함수를 거치도록 자동 처리되므로, 사용자가 악의적인 스크립트를 입력하더라도 XSS(크로스 사이트 스크립팅) 공격으로부터 안전하게 본문을 렌더링한다.




위의 설명과 관련된 이전 포스트들이다.





글 목록이 표시되고 있는 모습

이 모든 코드들의 변화로 인해 이렇게 글 목록이 표시되고 있다.


개발자 도구를 통해 time 태그의 datetime 속성에도 제대로 ISO 8601 표준 포맷 문자열이 들어가는 것을 확인할 수 있다.




원래 전통적인 웹 게시판 구조에서는 글 목록만 보여주는 index, 글 작성 폼을 띄우는 create, 개별 글을 상세히 보여주는 show 페이지를 각각 따로 분리해서 라우팅하는 것이 정석이다.

하지만 여기서는 빠른 흐름을 위해 페이지 이동 없이 X(트위터) 같은 피드 스타일로 축약하여 index 한 페이지에서 모든 일을 처리하도록 설계했다.


일단 여기서는 라라벨 Blade로 간단하게 CRUD를 구현하고, 나중에 라이브 와이어LIVEWIRE를 이용해서 Create 페이지와 Show 페이지를 따로 만드는 과정을 보여줄 예정이다.




다음 단계로는 작성한 글을 수정하고 삭제하는 'U(Update)'와 'D(Delete)' 과정을 순서대로 구현해 보겠다.

이 글이 도움이 됐거나 유익했다면 스크롤을 조금만 더 내려서 댓글을 남겨주세요. (비로그인도 가능합니다!)
응원이나 피드백이 담긴 댓글은 제가 계속 블로그를 해나갈 수 있는 원동력이 됩니다. 😊

지인에게 보여주고 싶은 글이었다면 URL을 복사해서 메신저나 소셜 미디어에 공유해 주세요.
이전 포스트

댓글 쓰기

0 Comments

문의하기 양식