“백엔드는 Laravel API, 프론트는 React로 어드민 만들자.”
이 결정을 내리는 순간, 프로젝트가 2개가 된다.
React 어드민의 실제 비용
React로 어드민 대시보드를 만들면 이런 일이 생긴다.
1. API를 따로 만들어야 한다
// Laravel: 유저 목록 API
Route::get('/api/admin/users', [AdminUserController::class, 'index']);
Route::get('/api/admin/users/{id}', [AdminUserController::class, 'show']);
Route::post('/api/admin/users', [AdminUserController::class, 'store']);
Route::put('/api/admin/users/{id}', [AdminUserController::class, 'update']);
Route::delete('/api/admin/users/{id}', [AdminUserController::class, 'destroy']);
// React: API 호출 레이어
const fetchUsers = async (page: number, filters: UserFilters) => {
const params = new URLSearchParams({ page: String(page), ...filters });
const res = await fetch(`/api/admin/users?${params}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
};
유저 하나 CRUD 하려면 라우트 5개 + API 호출 함수 5개 + 에러 핸들링. 모델이 10개면 이 작업을 10번 반복한다.
2. 상태 관리가 필요하다
// React Query, Zustand, Redux... 뭘 쓸 건가?
const { data, isLoading, error } = useQuery({
queryKey: ['users', page, filters],
queryFn: () => fetchUsers(page, filters),
});
리스트 페이지네이션, 필터 상태, 폼 상태, 에러 상태, 로딩 상태. 각각 관리해야 한다. 모델마다.
3. 테이블 컴포넌트를 만들어야 한다
// 정렬, 필터, 페이지네이션, 선택, 벌크 액션...
<DataTable
columns={columns}
data={users}
sorting={sorting}
onSortingChange={setSorting}
pagination={pagination}
onPaginationChange={setPagination}
filters={filters}
onFilterChange={setFilters}
selectedRows={selected}
onSelectionChange={setSelected}
bulkActions={[{ label: '삭제', action: handleBulkDelete }]}
/>
TanStack Table을 써도 설정 코드만 100줄이 넘는다. 정렬 하나 바꾸려면 프론트와 백엔드 양쪽을 수정해야 한다.
4. 인증 토큰을 관리해야 한다
// JWT 발급, 리프레시, 만료 처리, 인터셉터...
axios.interceptors.response.use(
(res) => res,
async (error) => {
if (error.response?.status === 401) {
const newToken = await refreshToken();
error.config.headers.Authorization = `Bearer ${newToken}`;
return axios(error.config);
}
return Promise.reject(error);
}
);
Laravel은 세션 인증이 기본인데, React를 분리하는 순간 토큰 기반으로 바꿔야 한다. 리프레시 로직, 만료 처리, CORS 설정. 어드민 패널에 이 복잡도가 필요한가?
5. 빌드 파이프라인이 2개가 된다
backend/ → composer install → php artisan serve
frontend/ → npm install → npm run build → nginx/vercel 배포
배포할 때마다 2개를 동기화해야 한다. API 필드 하나 바꾸면 프론트도 바꿔야 한다. 타입이 안 맞으면 런타임에서 터진다.
Filament는 이 모든 걸 안 해도 된다
php artisan make:filament-resource User --generate
이 한 줄이면 유저 CRUD 전체가 만들어진다. 목록, 생성, 수정, 삭제, 검색, 필터, 페이지네이션.
API? 필요 없다
Filament은 Livewire 기반이다. 서버에서 렌더링하고, 사용자 인터랙션은 Livewire가 처리한다. API 레이어가 아예 없다. 라우트 5개, 호출 함수 5개, 에러 핸들링 — 전부 불필요.
테이블? PHP로 선언한다
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')->sortable()->searchable(),
TextColumn::make('email')->sortable(),
TextColumn::make('created_at')->dateTime('Y-m-d'),
])
->filters([
Filter::make('verified')
->query(fn (Builder $query) => $query->whereNotNull('email_verified_at')),
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
]);
}
정렬, 검색, 필터, 액션. HTML 한 줄 안 짰다. 컬럼 추가하고 싶으면 TextColumn::make('phone') 한 줄 추가하면 끝이다. 프론트엔드를 수정할 필요가 없다. 프론트엔드가 없으니까.
폼? 같은 방식이다
public static function configure(Form $form): Form
{
return $form
->schema([
TextInput::make('name')->required()->maxLength(255),
TextInput::make('email')->email()->required()->unique(),
Select::make('role')->options([
'admin' => '관리자',
'user' => '일반 유저',
]),
DateTimePicker::make('email_verified_at'),
]);
}
유효성 검사, 유니크 체크, 셀렉트 옵션 — PHP 한 파일에서 전부 선언한다. React 폼 라이브러리(React Hook Form, Formik)의 등록/유효성/에러 처리를 생각하면 코드량 차이가 10배다.
인증? Laravel 세션 그대로
// config/filament.php에서 가드 지정하면 끝
->authGuard('web')
JWT 발급 없고, 리프레시 토큰 없고, CORS 없다. Laravel의 세션 인증을 그대로 쓴다. 어드민 패널에 토큰 기반 인증이 필요한 경우는 거의 없다.
권한? Laravel Policy 그대로
// UserPolicy.php — 이미 Laravel에 있는 것
public function update(User $authUser, User $targetUser): bool
{
return $authUser->hasRole('admin');
}
Filament이 자동으로 Policy를 체크한다. Gate, Policy, Middleware — Laravel에서 쓰던 방식 그대로. 새로 배울 게 없다.
배포? 하나다
laravel-app/ → composer install → php artisan serve
프로젝트 1개. 빌드 1번. 배포 1번. API-프론트 동기화 문제 자체가 존재하지 않는다.
비교 요약
| 작업 | React 어드민 | Filament |
|---|---|---|
| 유저 CRUD | API 5개 + 컴포넌트 5개 + 상태 관리 | --generate 한 줄 |
| 테이블 정렬/필터 | TanStack Table 설정 100줄+ | ->sortable()->searchable() |
| 폼 유효성 | React Hook Form + Zod/Yup | ->required()->email()->unique() |
| 인증 | JWT + 리프레시 + CORS | 세션 기반, 설정 0 |
| 권한 | 커스텀 미들웨어 + 프론트 가드 | Laravel Policy 자동 적용 |
| 배포 | 2개 프로젝트, 2번 빌드 | 1개 프로젝트, 1번 빌드 |
| 모델 10개 CRUD | 2~3주 | 하루 |
”React가 더 자유롭지 않나요?”
맞다. React는 픽셀 단위로 커스텀할 수 있다.
근데 어드민 패널이다. 사용자는 내부 직원이다. Dribbble에 올릴 디자인이 아니라 데이터를 관리하는 도구다. 테이블이 보이고, 폼이 작동하고, 권한이 맞으면 된다.
커스텀 UI가 정말 필요한 곳은 고객이 보는 화면이다. 어드민은 아니다. 고객 화면은 React/Next.js로 만들고, 어드민은 Filament로 만들면 된다. 섞어 쓰는 게 정답이다.
”나중에 복잡해지면 React로 옮겨야 하지 않나요?”
Filament은 멀티 패널을 지원한다.
// 관리자 패널
->id('admin')->path('admin')
// 유저 패널
->id('app')->path('app')
하나의 Laravel 앱에서 관리자용 패널과 유저용 패널을 분리할 수 있다. 각각 다른 리소스, 다른 인증, 다른 UI. 이 정도면 대부분의 SaaS 어드민 요구사항을 커버한다.
그래도 부족하면 그때 React로 만들어도 늦지 않다. 처음부터 React로 시작하는 건 과잉 설계다. 어드민은 빨리 만들고 빨리 쓰는 게 이긴다.
정리
React로 어드민 대시보드를 따로 만드는 건:
- API 레이어를 직접 만들겠다는 것
- 상태 관리를 직접 하겠다는 것
- 인증 토큰을 직접 관리하겠다는 것
- 2개의 프로젝트를 유지보수하겠다는 것
Filament를 쓰면:
- PHP 한 파일에서 CRUD가 끝난다
- 테이블, 폼, 권한이 선언형으로 완성된다
- 프로젝트는 하나, 배포도 하나다
- 모델 10개 CRUD를 하루 만에 만든다
어드민 패널에 React의 자유도가 필요한 순간은 거의 오지 않는다. 오면 그때 만들면 된다. 지금은 Filament로 하루 만에 만들고, 절약한 2주를 고객이 보는 화면에 쓰는 게 맞다.
composer require filament/filament:"^5.0"
php artisan filament:install --panels
php artisan make:filament-resource User --generate
3줄이면 시작할 수 있다.