Panel Provider가 뭔데 — Filament의 진입점 해부

TL;DR

Filament에서 AdminPanelProvider.php 하나가 패널의 모든 설정을 결정함. URL 경로, 색상, 리소스 자동탐색, 미들웨어 전부 여기서 체이닝으로 설정됨. 그런데 이 파일을 providers.php에 등록 안 하면? 아무 에러 없이 패널이 그냥 안 뜸. auto-discovery 네임스페이스 오타 내면? 역시 에러 없이 리소스가 조용히 사라짐. Filament의 침묵은 무섭다는 거 알게 됨.


AdminPanelProvider.php — 이게 진짜 본진임

Filament 프로젝트를 처음 만들면 app/Providers/Filament/AdminPanelProvider.php가 생김. 처음에 "얘가 뭐 하는 앤데?" 싶었음. 열어보니까 모든 게 여기 있었음.

class AdminPanelProvider extends PanelProvider
{
    public function panel(Panel $panel): Panel
    {
        return $panel
            ->default()
            ->id('admin')
            ->path('admin')
            ->login()
            ->colors([
                'primary' => Color::Amber,
            ])
            ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
            ->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
            ->pages([
                Dashboard::class,
            ])
            ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
            ->widgets([])
            ->middleware([
                EncryptCookies::class,
                AddQueuedCookiesToResponse::class,
                StartSession::class,
                AuthenticateSession::class,
                ShareErrorsFromSession::class,
                VerifyCsrfToken::class,
                SubstituteBindings::class,
                DisableBladeIconComponents::class,
                DispatchServingFilamentEvent::class,
            ])
            ->authMiddleware([
                Authenticate::class,
            ]);
    }
}

이게 전부임. 이 체이닝 하나로 패널의 모든 것이 결정됨. 하나씩 뜯어보겠음.


메서드 해부 — 하나씩 까보기

->id('admin')

패널의 고유 식별자. 멀티패널 할 때 이걸로 구분함. 내부적으로 라우트 네임 프리픽스, 캐시 키 등에 쓰임. 보통 'admin'으로 두면 됨.

->default()

여러 패널이 있을 때 이 패널을 기본값으로 설정함. /로 접근하면 이 패널로 리다이렉트됨. 패널이 하나뿐이어도 이거 안 붙이면 기본 리다이렉트가 안 됨. 삽질 포인트 하나 추가.

->path('admin')

URL 경로임. yourapp.com/admin으로 접근하게 됨. 이걸 'dashboard'로 바꾸면 yourapp.com/dashboard가 됨. 단순한데 처음엔 id랑 헷갈려서 둘 다 바꿔야 하나 고민했음. id는 내부 식별자, path는 URL. 다른 거임.

->login()

로그인 페이지 활성화. 이거 한 줄이면 /admin/login에 로그인 폼이 생김. 커스텀 로그인 페이지 쓰고 싶으면 ->login(MyLoginPage::class)으로 넘기면 됨. 안 쓰면 인증 없이 열리는 게 아니라 authMiddleware에서 막히고 라라벨 기본 로그인으로 튕겨남.

->colors()

패널 전체 색상 테마 설정.

->colors([
    'primary' => Color::Amber,
])

Color 클래스에 프리셋이 잔뜩 있음 — Amber, Blue, Emerald, Rose 등. 커스텀 색상도 됨:

->colors([
    'primary' => Color::hex('#6366f1'),
    'danger' => Color::Red,
])

처음에 'primary' => '#6366f1'로 문자열 넘겼다가 안 먹혀서 당황했음. Color::hex()로 감싸야 됨.

->discoverResources() / ->discoverWidgets() / ->discoverPages()

이게 진짜 핵심이자 최대 함정임.

->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')

in은 실제 파일 시스템 경로, for는 PHP 네임스페이스임. Filament이 이 경로를 스캔해서 리소스/위젯/페이지를 자동으로 등록해줌. php artisan make:filament-resource로 만들면 자동으로 잡힘.

그런데 여기서 대참사가 발생할 수 있음.

Auto-Discovery 함정 — 조용한 실패의 세계

네임스페이스와 파일 경로가 안 맞으면 에러가 안 남. 그냥 리소스가 사이드바에서 사라짐. 없는 것처럼.

실제로 겪은 상황:

# 파일 위치
app/Filament/Resources/Products/ProductResource.php

# 파일 안의 네임스페이스
namespace App\\Filament\\Resources\\Products;  ← 이게 맞음
namespace App\\Filament\\Resource\\Products;   ← s 하나 빠졌음. 에러? 없음. 그냥 안 뜸.

리소스를 열심히 만들어놓고 사이드바에 안 나타나면 이런 순서로 확인해야 됨:

  1. 네임스페이스 오타 확인 (Resources vs Resource)
  2. discoverResourcesfor 파라미터와 실제 네임스페이스 일치 여부
  3. in 경로가 실제 디렉토리를 가리키는지 (app_path() 기준)
  4. 클래스가 추상 클래스는 아닌지
# 이런 에러 메시지가 나올 거라 기대했음
"Resource ProductResource not found in namespace App\\Filament\\Resources"

# 실제로 나온 에러 메시지
# (없음)

진짜 아무것도 안 나옴. Filament의 침묵이 무섭다고 한 이유가 이거임. 디버깅하려면 Filament::getResources()tinker에서 찍어보는 게 제일 빠름:

php artisan tinker
>>> \\Filament\\Facades\\Filament::getPanel('admin')->getResources()

여기서 내 리소스가 안 보이면 네임스페이스 문제 확정.

->middleware() / ->authMiddleware()

middleware는 모든 요청에 적용됨 (로그인 페이지 포함). authMiddleware는 인증된 사용자만 접근 가능한 페이지에 적용됨. 이 구분 중요함 — Authenticatemiddleware에 넣으면 로그인 페이지 자체에 접근 못함. 무한 리다이렉트 루프 경험 가능.


멀티패널 — 관리자 vs 고객 포탈

패널 하나로 시작했다가 고객용 포탈도 필요해지는 순간이 옴. Filament은 멀티패널을 지원함.

php artisan make:filament-panel customer

그러면 app/Providers/Filament/CustomerPanelProvider.php가 생김:

class CustomerPanelProvider extends PanelProvider
{
    public function panel(Panel $panel): Panel
    {
        return $panel
            ->id('customer')
            ->path('portal')
            ->login()
            ->authGuard('customer')  // ← 별도 guard!
            ->colors([
                'primary' => Color::Blue,
            ])
            ->discoverResources(in: app_path('Filament/Customer/Resources'), for: 'App\\Filament\\Customer\\Resources')
            ->discoverPages(in: app_path('Filament/Customer/Pages'), for: 'App\\Filament\\Customer\\Pages')
            ->discoverWidgets(in: app_path('Filament/Customer/Widgets'), for: 'App\\Filament\\Customer\\Widgets');
    }
}

핵심 포인트:

  • ->default() 안 붙임 — 관리자 패널이 기본이니까
  • ->authGuard('customer')config/auth.phpcustomer guard 설정 필요함
  • discover 경로를 Filament/Customer/ 하위로 — 관리자 리소스와 고객 리소스 섞이지 않게

Customer 모델에 Authenticatable 구현해야 하는 거 잊지 말 것. 잊으면 이런 에러 나옴:

Argument #1 ($user) must be of type Illuminate\\Contracts\\Auth\\Authenticatable

유용한 추가 메서드들

->brandName()

->brandName('한빛공방 관리자')

사이드바 상단에 표시되는 브랜드명. 안 쓰면 config('app.name')이 나옴.

->spa()

이거 한 줄이면 SPA 모드 활성화됨. 페이지 이동 시 전체 새로고침 대신 AJAX로 컨텐츠만 교체함. 체감 속도 확 빨라짐.

->databaseNotifications()

라라벨의 데이터베이스 알림을 Filament UI에 바로 연결해줌. 상단 벨 아이콘에 알림이 뜸.

->sidebarCollapsibleOnDesktop()

데스크탑에서 사이드바 접기 가능하게 해줌. 대시보드 위젯이 많으면 화면 넓게 쓸 수 있어서 유용함.


providers.php — 이거 빼먹으면 끝

bootstrap/providers.php에 등록돼 있어야 됨:

return [
    App\\Providers\\AppServiceProvider::class,
    App\\Providers\\Filament\\AdminPanelProvider::class,  // ← 이거
];

수동으로 파일 복사해서 만들면 여기 등록 안 하는 실수를 할 수 있음. 그러면 /admin 접속 시 그냥 404. Filament 입장에선 패널이 존재하지 않으니까 당연히 라우트도 없는 거임. 이것도 조용한 실패 시리즈.


FAQ

Q: ->id()->path()를 같은 값으로 안 해도 됨?

됨. id는 코드 내부에서 패널을 참조할 때, path는 URL에만 영향을 줌. 특별한 이유 없으면 맞추는 게 편함.

Q: discoverResources를 안 쓰고 수동 등록만 해도 됨?

됨. ->resources([ProductResource::class, ...]) 직접 배열로 넘기면 됨. 네임스페이스 실수 디버깅이 싫으면 수동 등록도 나쁘지 않음.

Q: 패널 프로바이더를 두 개 만들었는데 둘 다 ->default() 붙이면?

마지막에 등록된 게 이김. 에러는 안 남. 이것도 조용한 실패. Filament은 진짜 과묵함.


이전: 왜 이 조합이냐 — Filament 셋업에서 내린 선택들 | 설치하자마자 넣은 플러그인들

다음: 커스텀 테마 만들기 — Vite + Tailwind 세팅

이 글은 Building with Filament 시리즈의 세 번째 글입니다.