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 하나 빠졌음. 에러? 없음. 그냥 안 뜸.
리소스를 열심히 만들어놓고 사이드바에 안 나타나면 이런 순서로 확인해야 됨:
- 네임스페이스 오타 확인 (
ResourcesvsResource) discoverResources의for파라미터와 실제 네임스페이스 일치 여부in경로가 실제 디렉토리를 가리키는지 (app_path()기준)- 클래스가 추상 클래스는 아닌지
# 이런 에러 메시지가 나올 거라 기대했음
"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는 인증된 사용자만 접근 가능한 페이지에 적용됨. 이 구분 중요함 — Authenticate를 middleware에 넣으면 로그인 페이지 자체에 접근 못함. 무한 리다이렉트 루프 경험 가능.
멀티패널 — 관리자 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.php에customerguard 설정 필요함- 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 시리즈의 세 번째 글입니다.