# Laravel 11: 처음부터 완전한 애플리케이션 구축하기 > Laravel 11로 인증, REST API, Eloquent ORM, 배포까지 완전한 애플리케이션을 구축하는 종합 가이드입니다. 실용적인 코드 예제와 함께 설명합니다. - Published: 2026-01-14 - Updated: 2026-04-08 - Author: SharpSkill - Tags: laravel, php, rest api, eloquent, tutorial - Reading time: 15 min --- Laravel 11은 간소화된 아키텍처와 최적화된 성능으로 모던 PHP 개발의 기준을 새롭게 정립합니다. 이번 메이저 릴리스에서는 많은 기본 설정 파일이 제거되었고, 더 가벼운 구조가 도입되었으며, 개발자 경험이 크게 향상되었습니다. 본 가이드에서는 태스크 관리 애플리케이션의 구축 과정을 설치부터 배포까지 단계별로 다룹니다. > **Laravel 11 주요 특징** > > Laravel 11은 미니멀리스트 구조를 채택했습니다. Kernel 파일이 제거되고, 설정이 bootstrap/app.php로 통합되었으며, app/ 디렉토리가 깔끔해졌습니다. PHP 8.2 이상이 필수이며, SQLite가 기본 통합되어 빠르게 개발을 시작할 수 있습니다. ## Installation and Project Setup Laravel 11은 Composer 또는 Laravel 인스톨러를 통해 설치할 수 있습니다. 새로운 프로젝트 구조는 더 가벼워졌으며, 초기에 관리해야 할 설정 파일이 적습니다. ```bash # terminal # Install the Laravel installer (recommended) composer global require laravel/installer # Create a new Laravel 11 project laravel new task-manager # Or directly with Composer composer create-project laravel/laravel task-manager # Navigate to the project cd task-manager ``` 인스톨러에서는 스타터 킷(Breeze, Jetstream), 테스트 프레임워크(Pest, PHPUnit), 데이터베이스 등 여러 가지 옵션을 대화형으로 선택할 수 있습니다. ```bash # terminal # Start the development server php artisan serve # Server starts at http://localhost:8000 ``` ### 데이터베이스 설정 Laravel 11은 기본적으로 SQLite를 사용하며, 개발 환경에 적합합니다. 프로덕션 애플리케이션에서는 MySQL 또는 PostgreSQL 설정을 `.env` 파일에서 진행합니다. ```env # .env # MySQL configuration DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=task_manager DB_USERNAME=root DB_PASSWORD=secret # PostgreSQL configuration (alternative) # DB_CONNECTION=pgsql # DB_HOST=127.0.0.1 # DB_PORT=5432 # DB_DATABASE=task_manager # DB_USERNAME=postgres # DB_PASSWORD=secret ``` 데이터베이스 연결 확인은 Artisan 명령어를 사용합니다. ```bash # terminal # Verify database connection php artisan db:show # Run default migrations php artisan migrate ``` ## Creating Models and Migrations Eloquent ORM은 표현력 있는 모델을 통해 데이터 조작을 단순화합니다. `make:model` 명령어로 모델, 마이그레이션, 컨트롤러, 팩토리를 한 번에 생성할 수 있습니다. ```bash # terminal # Generate Task model with migration, factory, seeder, and controller php artisan make:model Task -mfsc # -m : migration # -f : factory # -s : seeder # -c : controller ``` 이 명령어는 태스크 관리에 필요한 4개의 핵심 파일을 생성합니다. ```php id(); // Relationship with the owner user $table->foreignId('user_id')->constrained()->cascadeOnDelete(); // Task title (required) $table->string('title'); // Detailed description (optional) $table->text('description')->nullable(); // Status: pending, in_progress, completed $table->string('status')->default('pending'); // Priority: low, medium, high $table->string('priority')->default('medium'); // Due date (optional) $table->date('due_date')->nullable(); // Completion marker $table->timestamp('completed_at')->nullable(); // Automatic timestamps (created_at, updated_at) $table->timestamps(); // Soft deletes for trash functionality $table->softDeletes(); // Indexes to optimize frequent queries $table->index(['user_id', 'status']); $table->index('due_date'); }); } public function down(): void { Schema::dropIfExists('tasks'); } }; ``` 이 마이그레이션은 관계, 인덱스, 소프트 딜리트를 포함한 테이블 구조를 정의합니다. 소프트 딜리트를 통해 실수로 인한 데이터 손실을 방지할 수 있습니다. ```php 'date', 'completed_at' => 'datetime', ]; } // Constants for valid statuses public const STATUS_PENDING = 'pending'; public const STATUS_IN_PROGRESS = 'in_progress'; public const STATUS_COMPLETED = 'completed'; // Constants for priorities public const PRIORITY_LOW = 'low'; public const PRIORITY_MEDIUM = 'medium'; public const PRIORITY_HIGH = 'high'; // Relationship: a task belongs to a user public function user(): BelongsTo { return $this->belongsTo(User::class); } // Scope: pending tasks public function scopePending(Builder $query): Builder { return $query->where('status', self::STATUS_PENDING); } // Scope: in-progress tasks public function scopeInProgress(Builder $query): Builder { return $query->where('status', self::STATUS_IN_PROGRESS); } // Scope: completed tasks public function scopeCompleted(Builder $query): Builder { return $query->where('status', self::STATUS_COMPLETED); } // Scope: overdue tasks public function scopeOverdue(Builder $query): Builder { return $query->where('due_date', '<', now()) ->whereNull('completed_at'); } // Scope: tasks for a specific user public function scopeForUser(Builder $query, int $userId): Builder { return $query->where('user_id', $userId); } // Accessor: check if task is overdue public function getIsOverdueAttribute(): bool { return $this->due_date && $this->due_date->isPast() && !$this->completed_at; } // Method: mark as completed public function markAsCompleted(): void { $this->update([ 'status' => self::STATUS_COMPLETED, 'completed_at' => now(), ]); } } ``` Task 모델은 Eloquent 스코프를 활용하여 읽기 쉽고 재사용 가능한 쿼리를 구현합니다. 상수로 상태와 우선순위의 유효한 값을 중앙에서 관리합니다. > **소프트 딜리트** > > SoftDeletes 트레이트는 deleted_at 컬럼을 추가합니다. 삭제된 태스크는 데이터베이스에서 제거되지 않고 삭제 표시만 됩니다. withTrashed()를 사용하면 삭제된 레코드도 쿼리에 포함시킬 수 있습니다. ## Setting Up Authentication with Breeze Laravel Breeze는 Blade 뷰 또는 API를 갖춘 완전한 인증 기능을 제공합니다. 몇 분이면 설치가 완료되며, 라우트, 컨트롤러, 뷰, 테스트가 모두 생성됩니다. ```bash # terminal # Install Laravel Breeze composer require laravel/breeze --dev # Install the Blade stack (traditional) php artisan breeze:install blade # Or for API only (SPA/mobile) php artisan breeze:install api # Compile assets npm install && npm run build # Run migrations (users, sessions tables, etc.) php artisan migrate ``` Breeze는 회원가입, 로그인, 비밀번호 재설정, 이메일 인증을 위한 인증 라우트, 컨트롤러, 뷰를 생성합니다. ```php 'datetime', 'password' => 'hashed', ]; } // Relationship: a user owns multiple tasks public function tasks(): HasMany { return $this->hasMany(Task::class); } // Shortcut: user's pending tasks public function pendingTasks(): HasMany { return $this->tasks()->pending(); } // Shortcut: user's overdue tasks public function overdueTasks(): HasMany { return $this->tasks()->overdue(); } } ``` ## Creating Controllers and Routes 컨트롤러는 HTTP 요청과 모델 사이의 비즈니스 로직을 조율하는 역할을 합니다. Laravel 11에서는 싱글 액션 컨트롤러 또는 RESTful 리소스 사용을 권장합니다. ```php user() ->tasks() ->when($request->status, fn($q, $status) => $q->where('status', $status)) ->when($request->priority, fn($q, $priority) => $q->where('priority', $priority)) ->orderBy('due_date') ->orderBy('priority', 'desc') ->paginate(15); return view('tasks.index', compact('tasks')); } // Creation form public function create(): View { return view('tasks.create'); } // Store a new task public function store(Request $request): RedirectResponse { // Validate incoming data $validated = $request->validate([ 'title' => 'required|string|max:255', 'description' => 'nullable|string|max:5000', 'priority' => 'required|in:low,medium,high', 'due_date' => 'nullable|date|after_or_equal:today', ]); // Create the task linked to the user $request->user()->tasks()->create($validated); return redirect() ->route('tasks.index') ->with('success', 'Task created successfully.'); } // Display a task public function show(Task $task): View { // Verify user ownership Gate::authorize('view', $task); return view('tasks.show', compact('task')); } // Edit form public function edit(Task $task): View { Gate::authorize('update', $task); return view('tasks.edit', compact('task')); } // Update a task public function update(Request $request, Task $task): RedirectResponse { Gate::authorize('update', $task); $validated = $request->validate([ 'title' => 'required|string|max:255', 'description' => 'nullable|string|max:5000', 'status' => 'required|in:pending,in_progress,completed', 'priority' => 'required|in:low,medium,high', 'due_date' => 'nullable|date', ]); // Update completed_at if status = completed if ($validated['status'] === Task::STATUS_COMPLETED && !$task->completed_at) { $validated['completed_at'] = now(); } $task->update($validated); return redirect() ->route('tasks.index') ->with('success', 'Task updated.'); } // Delete a task public function destroy(Task $task): RedirectResponse { Gate::authorize('delete', $task); $task->delete(); return redirect() ->route('tasks.index') ->with('success', 'Task deleted.'); } // Mark as completed (quick action) public function complete(Task $task): RedirectResponse { Gate::authorize('update', $task); $task->markAsCompleted(); return back()->with('success', 'Task completed!'); } } ``` 이 컨트롤러는 인가에 Gate를, 유효성 검사에 내장 기능을, 라우트 모델 바인딩을 활용하여 간결하고 안전한 코드를 구현합니다. ### 라우트 정의 라우트는 URL을 정의하고 컨트롤러 액션과 연결합니다. Laravel 11에서는 라우트가 `routes/web.php`에 집중됩니다. ```php group(function () { // Dashboard with statistics Route::get('/dashboard', function () { $user = auth()->user(); return view('dashboard', [ 'totalTasks' => $user->tasks()->count(), 'pendingTasks' => $user->tasks()->pending()->count(), 'completedTasks' => $user->tasks()->completed()->count(), 'overdueTasks' => $user->tasks()->overdue()->count(), ]); })->name('dashboard'); // RESTful routes for tasks Route::resource('tasks', TaskController::class); // Quick action to complete a task Route::patch('/tasks/{task}/complete', [TaskController::class, 'complete']) ->name('tasks.complete'); // Profile routes (generated by Breeze) Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); }); // Authentication routes (generated by Breeze) require __DIR__.'/auth.php'; ``` `Route::resource()` 메서드는 index, create, store, show, edit, update, destroy 등 7개의 표준 RESTful 라우트를 자동으로 생성합니다. ## Setting Up Authorization Policies 폴리시는 각 모델의 인가 로직을 중앙에서 관리합니다. 어떤 사용자가 리소스에 대해 어떤 작업을 수행할 수 있는지 정의합니다. ```bash # terminal # Generate the policy for Task php artisan make:policy TaskPolicy --model=Task ``` ```php is_admin) { return true; } return null; // Continue to specific methods } // Can view the list of tasks public function viewAny(User $user): bool { return true; // Any authenticated user } // Can view a specific task public function view(User $user, Task $task): bool { return $user->id === $task->user_id; } // Can create a task public function create(User $user): bool { return true; } // Can update a task public function update(User $user, Task $task): bool { return $user->id === $task->user_id; } // Can delete a task public function delete(User $user, Task $task): bool { return $user->id === $task->user_id; } // Can restore a soft-deleted task public function restore(User $user, Task $task): bool { return $user->id === $task->user_id; } // Can permanently delete public function forceDelete(User $user, Task $task): bool { return $user->id === $task->user_id; } } ``` Laravel은 명명 규칙에 따라 폴리시를 자동으로 검색합니다. `TaskPolicy`는 `Task` 모델에 자동으로 적용됩니다. > **폴리시 보안** > > 폴리시는 보안의 핵심입니다. 폴리시가 없으면 사용자가 URL을 조작하여 다른 사용자의 태스크에 접근할 수 있습니다. 리소스 소유권 확인은 반드시 구현해야 합니다. ## Building a REST API Laravel은 전용 라우트, JSON 리소스, Sanctum 인증을 통해 RESTful API 생성을 단순화합니다. ```bash # terminal # Install Sanctum (included by default since Laravel 11) php artisan install:api # This command: # - Publishes Sanctum migrations # - Adds the HasApiTokens trait to the User model # - Configures API routes ``` ```php user() ->tasks() ->when($request->status, fn($q, $s) => $q->where('status', $s)) ->when($request->priority, fn($q, $p) => $q->where('priority', $p)) ->when($request->boolean('overdue'), fn($q) => $q->overdue()) ->latest() ->paginate($request->input('per_page', 15)); return TaskResource::collection($tasks); } // Create a task public function store(Request $request): JsonResponse { $validated = $request->validate([ 'title' => 'required|string|max:255', 'description' => 'nullable|string|max:5000', 'priority' => 'required|in:low,medium,high', 'due_date' => 'nullable|date|after_or_equal:today', ]); $task = $request->user()->tasks()->create($validated); return response()->json([ 'message' => 'Task created successfully.', 'data' => new TaskResource($task), ], 201); } // Display a task public function show(Task $task): TaskResource { $this->authorize('view', $task); return new TaskResource($task); } // Update a task public function update(Request $request, Task $task): TaskResource { $this->authorize('update', $task); $validated = $request->validate([ 'title' => 'sometimes|required|string|max:255', 'description' => 'nullable|string|max:5000', 'status' => 'sometimes|required|in:pending,in_progress,completed', 'priority' => 'sometimes|required|in:low,medium,high', 'due_date' => 'nullable|date', ]); if (isset($validated['status']) && $validated['status'] === Task::STATUS_COMPLETED && !$task->completed_at) { $validated['completed_at'] = now(); } $task->update($validated); return new TaskResource($task->fresh()); } // Delete a task public function destroy(Task $task): JsonResponse { $this->authorize('delete', $task); $task->delete(); return response()->json([ 'message' => 'Task deleted successfully.', ]); } } ``` ### API 리소스를 활용한 데이터 변환 API 리소스는 Eloquent 모델을 구조화된 JSON 응답으로 변환하며, 노출되는 데이터를 정밀하게 제어합니다. ```php $this->id, 'title' => $this->title, 'description' => $this->description, 'status' => $this->status, 'priority' => $this->priority, 'due_date' => $this->due_date?->format('Y-m-d'), 'is_overdue' => $this->is_overdue, 'completed_at' => $this->completed_at?->toISOString(), 'created_at' => $this->created_at->toISOString(), 'updated_at' => $this->updated_at->toISOString(), // Conditional inclusion of the user 'user' => $this->whenLoaded('user', fn() => [ 'id' => $this->user->id, 'name' => $this->user->name, ]), ]; } } ``` ### Sanctum 인증 기반 API 라우트 ```php user(); })->middleware('auth:sanctum'); // API routes protected by Sanctum Route::middleware('auth:sanctum')->group(function () { Route::apiResource('tasks', TaskController::class); // Dashboard statistics Route::get('/dashboard/stats', function (Request $request) { $user = $request->user(); return response()->json([ 'total' => $user->tasks()->count(), 'pending' => $user->tasks()->pending()->count(), 'in_progress' => $user->tasks()->inProgress()->count(), 'completed' => $user->tasks()->completed()->count(), 'overdue' => $user->tasks()->overdue()->count(), ]); }); }); ``` ## Automated Testing with Pest Laravel 11은 기본적으로 Pest PHP를 통합하고 있으며, 우아하고 표현력 있는 테스트 프레임워크입니다. 테스트를 통해 수정 후에도 애플리케이션이 올바르게 동작하는지 보장할 수 있습니다. ```php create(); $tasks = Task::factory()->count(5)->for($user)->create(); $response = $this->actingAs($user)->get('/tasks'); $response->assertOk(); $response->assertViewHas('tasks'); }); // Test: a user can create a task test('user can create a task', function () { $user = User::factory()->create(); $response = $this->actingAs($user)->post('/tasks', [ 'title' => 'New task', 'description' => 'Task description', 'priority' => 'high', 'due_date' => now()->addDays(7)->format('Y-m-d'), ]); $response->assertRedirect('/tasks'); $this->assertDatabaseHas('tasks', [ 'user_id' => $user->id, 'title' => 'New task', 'priority' => 'high', ]); }); // Test: title validation required test('task title is required', function () { $user = User::factory()->create(); $response = $this->actingAs($user)->post('/tasks', [ 'title' => '', 'priority' => 'medium', ]); $response->assertSessionHasErrors('title'); }); // Test: a user cannot view another user's task test('user cannot view another users task', function () { $user = User::factory()->create(); $otherUser = User::factory()->create(); $task = Task::factory()->for($otherUser)->create(); $response = $this->actingAs($user)->get("/tasks/{$task->id}"); $response->assertForbidden(); }); // Test: mark a task as completed test('user can complete a task', function () { $user = User::factory()->create(); $task = Task::factory()->for($user)->create(['status' => 'pending']); $response = $this->actingAs($user)->patch("/tasks/{$task->id}/complete"); $response->assertRedirect(); $task->refresh(); expect($task->status)->toBe('completed'); expect($task->completed_at)->not->toBeNull(); }); // API test: retrieve tasks test('api returns paginated tasks', function () { $user = User::factory()->create(); Task::factory()->count(20)->for($user)->create(); $response = $this->actingAs($user, 'sanctum') ->getJson('/api/tasks?per_page=10'); $response->assertOk() ->assertJsonCount(10, 'data') ->assertJsonStructure([ 'data' => [ '*' => ['id', 'title', 'status', 'priority', 'due_date'], ], 'links', 'meta', ]); }); ``` Pest를 사용한 테스트 실행 방법은 다음과 같습니다. ```bash # terminal # Run all tests php artisan test # Tests with code coverage php artisan test --coverage # Tests for a specific file php artisan test tests/Feature/TaskTest.php # Parallel tests (faster) php artisan test --parallel ``` ## Production Optimizations 배포 전에 몇 가지 최적화를 통해 Laravel 애플리케이션의 성능을 향상시킬 수 있습니다. ```bash # terminal # Cache configuration php artisan config:cache # Cache routes php artisan route:cache # Cache views php artisan view:cache # Optimize Composer autoloader composer install --optimize-autoloader --no-dev # Generate asset manifest npm run build ``` ### 프로덕션 .env 파일 설정 ```env # .env.production APP_NAME="Task Manager" APP_ENV=production APP_DEBUG=false APP_URL=https://taskmanager.example.com # Cache and session via Redis (recommended) CACHE_DRIVER=redis SESSION_DRIVER=redis QUEUE_CONNECTION=redis REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 # Database DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_DATABASE=task_manager_prod DB_USERNAME=app_user DB_PASSWORD=strong_password_here # Mail MAIL_MAILER=smtp MAIL_HOST=smtp.mailgun.org MAIL_PORT=587 MAIL_USERNAME=postmaster@taskmanager.example.com MAIL_PASSWORD=secret MAIL_ENCRYPTION=tls ``` > **민감한 변수 관리** > > .env 파일을 Git 저장소에 커밋해서는 안 됩니다. .env.example을 템플릿으로 사용하고, 민감한 변수는 프로덕션 서버에 직접 설정하거나 시크릿 매니저를 통해 관리해야 합니다. ## Conclusion Laravel 11은 간소화된 구조와 지능적인 컨벤션으로 모던 PHP 개발을 크게 단순화합니다. 본 가이드에서는 완전한 애플리케이션 구축의 기본을 다루었습니다. Eloquent 모델, 인증, RESTful 컨트롤러, 인가 폴리시, Sanctum API, 자동화된 테스트를 포괄적으로 설명했습니다. ### 고품질 Laravel 애플리케이션을 위한 체크리스트 - 모든 스키마 변경에 마이그레이션을 사용할 것 - 폴리시를 구현하여 리소스 접근을 보호할 것 - 컨트롤러에서 모든 사용자 입력을 유효성 검사할 것 - API 리소스로 노출 데이터를 제어할 것 - 핵심 기능에 대한 테스트를 작성할 것 - 배포 전 캐시와 최적화를 설정할 것 - 민감한 환경 변수를 보호할 것 Laravel 생태계는 리액티브 인터페이스를 위한 Livewire, 큐 관리를 위한 Horizon, 뛰어난 성능을 위한 Octane 등 다양한 패키지와 함께 계속 발전하고 있습니다. 이러한 기본기를 마스터하면 견고하고 유지보수 가능한 PHP 애플리케이션을 구축할 수 있습니다. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/laravel/laravel-11-building-complete-application