> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Laravel 패키지 개발

> 서비스 프로바이더를 핵심으로 하는 Laravel 패키지 개발 방법을, 설정·뷰·마이그레이션·파사드 공개부터 자동 감지·지연 로딩까지 망라합니다.

## 패키지란

Laravel에서 말하는 패키지는 애플리케이션에 기능을 추가하는 Composer 패키지입니다. 패키지는 크게 두 종류로 나뉩니다.

* **스탠드얼론 패키지** — Laravel에 의존하지 않는 범용 PHP 라이브러리(예: Carbon, Pest)
* **Laravel 패키지** — 라우트, 컨트롤러, 뷰, 설정 등 Laravel과 통합된 기능을 가진 패키지

이 가이드에서는 후자, 즉 Laravel 전용 패키지 개발을 다룹니다. 패키지 개발에는 서비스 프로바이더, 파사드, 설정 파일 공개 등 Laravel의 내부 구조에 대한 깊이 있는 이해가 필요합니다.

<Info>
  패키지의 테스트를 작성할 때는 [Orchestra Testbench](https://github.com/orchestral/testbench)를 사용합니다. 일반적인 Laravel 애플리케이션과 동일하게 패키지의 테스트를 작성할 수 있습니다.
</Info>

## 패키지 자동 감지

Laravel은 패키지를 설치할 때 `composer.json`의 `extra.laravel` 섹션을 읽어 서비스 프로바이더와 파사드를 자동 등록합니다.

```json theme={null}
"extra": {
    "laravel": {
        "providers": [
            "Acme\\Courier\\CourierServiceProvider"
        ],
        "aliases": {
            "Courier": "Acme\\Courier\\Facades\\Courier"
        }
    }
}
```

이 설정을 추가하면 사용자는 `bootstrap/providers.php`를 수동으로 편집하지 않아도 패키지가 자동으로 로드됩니다.

### 자동 감지 비활성화

사용자 측에서 특정 패키지의 자동 감지를 비활성화하고 싶은 경우, 애플리케이션의 `composer.json`에 설정합니다.

```json theme={null}
"extra": {
    "laravel": {
        "dont-discover": [
            "acme/courier"
        ]
    }
}
```

## 서비스 프로바이더의 역할

서비스 프로바이더는 패키지의 진입점입니다. 뷰, 설정, 마이그레이션, 라우트 등의 리소스를 Laravel에 등록하는 처리를 여기에 집약합니다.

서비스 프로바이더는 `Illuminate\Support\ServiceProvider`를 상속하며, `register`와 `boot`의 두 메서드를 가집니다.

```php theme={null}
<?php

namespace Acme\Courier;

use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    /**
     * 패키지의 서비스를 등록
     */
    public function register(): void
    {
        // 서비스 컨테이너에 대한 바인딩은 여기서 수행
        $this->mergeConfigFrom(
            __DIR__.'/../config/courier.php', 'courier'
        );

        $this->app->singleton(CourierManager::class, function ($app) {
            return new CourierManager($app['config']['courier']);
        });
    }

    /**
     * 패키지의 서비스를 부트스트랩
     */
    public function boot(): void
    {
        // 리소스 등록은 여기서 수행
        $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
        $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');
        $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier');

        $this->publishesMigrations([
            __DIR__.'/../database/migrations' => database_path('migrations'),
        ]);

        $this->publishes([
            __DIR__.'/../config/courier.php' => config_path('courier.php'),
        ], 'courier-config');

        $this->publishes([
            __DIR__.'/../resources/views' => resource_path('views/vendor/courier'),
        ], 'courier-views');
    }
}
```

<Warning>
  `register` 메서드 내부에서 이벤트 리스너, 라우트, 뷰 등을 등록하지 마세요. 아직 로드되지 않은 다른 서비스 프로바이더의 서비스를 잘못 사용할 가능성이 있습니다. 바인딩 이외의 처리는 반드시 `boot` 메서드에서 수행합니다.
</Warning>

## 설정 파일 Publish

### publishes() — 파일을 공개

`boot` 메서드에서 `publishes()`를 호출하면 사용자가 `vendor:publish` 명령어로 설정 파일을 자신의 애플리케이션으로 복사할 수 있습니다.

```php theme={null}
public function boot(): void
{
    $this->publishes([
        __DIR__.'/../config/courier.php' => config_path('courier.php'),
    ]);
}
```

공개 후 설정값은 일반적인 config 접근과 동일한 방법으로 가져올 수 있습니다.

```php theme={null}
$value = config('courier.option');
```

### mergeConfigFrom() — 기본값과 병합

`register` 메서드에서 `mergeConfigFrom()`을 사용하면 사용자가 설정 파일을 공개하지 않은 경우에도 패키지의 기본값이 사용됩니다.

```php theme={null}
public function register(): void
{
    $this->mergeConfigFrom(
        __DIR__.'/../config/courier.php', 'courier'
    );
}
```

<Warning>
  `mergeConfigFrom()`은 중첩된 배열의 깊은 레벨까지는 병합하지 않습니다. 다차원 배열을 가진 설정에서는 사용자가 일부만 정의한 경우 나머지 옵션이 병합되지 않을 수 있습니다.
</Warning>

### 태그로 공개 그룹 나누기

`publishes()`의 두 번째 인자로 태그를 지정하면, 사용자가 필요한 리소스만 선택해서 공개할 수 있습니다.

```php theme={null}
public function boot(): void
{
    $this->publishes([
        __DIR__.'/../config/courier.php' => config_path('courier.php'),
    ], 'courier-config');

    $this->publishesMigrations([
        __DIR__.'/../database/migrations/' => database_path('migrations'),
    ], 'courier-migrations');
}
```

```shell theme={null}
# 설정 파일만 공개
php artisan vendor:publish --tag=courier-config

# 프로바이더가 제공하는 모든 파일을 공개
php artisan vendor:publish --provider="Acme\Courier\CourierServiceProvider"
```

## 라우트 등록

`loadRoutesFrom()`을 사용하여 라우트 파일을 로드합니다. 애플리케이션의 라우트 캐시가 활성화되어 있는 경우 자동으로 건너뜁니다.

```php theme={null}
public function boot(): void
{
    $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
}
```

라우트 파일에서는 패키지의 컨트롤러를 지정합니다.

```php theme={null}
// routes/web.php
use Acme\Courier\Http\Controllers\TrackingController;
use Illuminate\Support\Facades\Route;

Route::prefix('courier')->group(function () {
    Route::get('/track/{id}', [TrackingController::class, 'show'])
        ->name('courier.track');
});
```

## 마이그레이션 Publish

`publishesMigrations()`를 사용하면 마이그레이션 파일을 공개할 수 있습니다. 공개 시 Laravel이 자동으로 타임스탬프를 갱신합니다.

```php theme={null}
public function boot(): void
{
    $this->publishesMigrations([
        __DIR__.'/../database/migrations' => database_path('migrations'),
    ]);
}
```

## 뷰 Publish

### loadViewsFrom() — 뷰 등록

`loadViewsFrom()`으로 뷰 디렉터리를 등록합니다. 두 번째 인자의 네임스페이스를 사용하여 `package::view` 형식으로 뷰를 참조합니다.

```php theme={null}
public function boot(): void
{
    $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');
}
```

등록 후 뷰는 패키지 네임스페이스로 참조합니다.

```php theme={null}
Route::get('/dashboard', function () {
    return view('courier::dashboard');
});
```

Laravel은 뷰를 두 곳에서 찾습니다. 먼저 애플리케이션의 `resources/views/vendor/courier` 디렉터리를 확인하고, 없으면 패키지의 뷰 디렉터리를 사용합니다. 이를 통해 사용자가 뷰를 커스터마이즈할 수 있습니다.

### 뷰 공개

```php theme={null}
public function boot(): void
{
    $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');

    $this->publishes([
        __DIR__.'/../resources/views' => resource_path('views/vendor/courier'),
    ], 'courier-views');
}
```

### Blade 컴포넌트 등록

컴포넌트를 패키지에 포함시키는 경우, `boot` 메서드에서 등록합니다.

```php theme={null}
use Illuminate\Support\Facades\Blade;
use Acme\Courier\View\Components\AlertComponent;

public function boot(): void
{
    Blade::component('courier-alert', AlertComponent::class);
}
```

컴포넌트 네임스페이스를 사용하여 일괄 등록할 수도 있습니다.

```php theme={null}
use Illuminate\Support\Facades\Blade;

public function boot(): void
{
    Blade::componentNamespace('Acme\\Courier\\View\\Components', 'courier');
}
```

```blade theme={null}
{{-- 개별 등록의 경우 --}}
<x-courier-alert />

{{-- 네임스페이스 등록의 경우 --}}
<x-courier::alert />
```

## 번역 파일 Publish

`loadTranslationsFrom()`으로 번역 파일을 등록합니다. 번역은 `package::file.key` 형식으로 참조합니다.

```php theme={null}
public function boot(): void
{
    $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier');

    $this->publishes([
        __DIR__.'/../lang' => $this->app->langPath('vendor/courier'),
    ]);
}
```

```php theme={null}
// 번역 사용
echo trans('courier::messages.welcome');
```

JSON 번역 파일을 사용할 경우 `loadJsonTranslationsFrom()`을 사용합니다.

```php theme={null}
public function boot(): void
{
    $this->loadJsonTranslationsFrom(__DIR__.'/../lang');
}
```

## 명령어 등록

패키지의 Artisan 명령어는 `commands()` 메서드로 등록합니다. 콘솔 환경에서만 등록하는 것이 일반적입니다.

```php theme={null}
use Acme\Courier\Console\Commands\InstallCommand;
use Acme\Courier\Console\Commands\SyncCommand;

public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->commands([
            InstallCommand::class,
            SyncCommand::class,
        ]);
    }
}
```

### optimize 명령어에 통합

패키지가 자체 캐시를 가지고 있는 경우, `optimizes()` 메서드로 `php artisan optimize` 및 `php artisan optimize:clear`에 통합할 수 있습니다.

```php theme={null}
public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->optimizes(
            optimize: 'courier:cache',
            clear: 'courier:clear-cache',
        );
    }
}
```

### `about` 명령어에 정보 추가

`php artisan about`의 출력에 패키지 정보를 추가하려면 `AboutCommand::add()`를 사용합니다.

```php theme={null}
use Illuminate\Foundation\Console\AboutCommand;

public function boot(): void
{
    AboutCommand::add('Courier Package', fn () => ['Version' => '1.0.0']);
}
```

## 파사드 생성

파사드를 사용하면 서비스 컨테이너의 바인딩을 정적 메서드처럼 호출할 수 있습니다.

<Steps>
  <Step title="서비스 클래스 생성">
    ```php theme={null}
    <?php

    namespace Acme\Courier;

    class CourierManager
    {
        public function __construct(
            protected array $config,
        ) {}

        public function send(string $to, string $message): bool
        {
            // 메시지 발송 처리
            return true;
        }

        public function track(string $id): array
        {
            // 추적 정보 조회 처리
            return ['status' => 'delivered'];
        }
    }
    ```
  </Step>

  <Step title="파사드 클래스 생성">
    `Illuminate\Support\Facades\Facade`를 상속하고, `getFacadeAccessor()`에서 서비스 컨테이너의 바인딩 키를 반환합니다.

    ```php theme={null}
    <?php

    namespace Acme\Courier\Facades;

    use Illuminate\Support\Facades\Facade;

    /**
     * @method static bool send(string $to, string $message)
     * @method static array track(string $id)
     *
     * @see \Acme\Courier\CourierManager
     */
    class Courier extends Facade
    {
        protected static function getFacadeAccessor(): string
        {
            return \Acme\Courier\CourierManager::class;
        }
    }
    ```
  </Step>

  <Step title="서비스 프로바이더에서 바인딩">
    ```php theme={null}
    public function register(): void
    {
        $this->app->singleton(\Acme\Courier\CourierManager::class, function ($app) {
            return new \Acme\Courier\CourierManager($app['config']['courier']);
        });
    }
    ```
  </Step>

  <Step title="composer.json에 등록">
    ```json theme={null}
    "extra": {
        "laravel": {
            "providers": [
                "Acme\\Courier\\CourierServiceProvider"
            ],
            "aliases": {
                "Courier": "Acme\\Courier\\Facades\\Courier"
            }
        }
    }
    ```
  </Step>
</Steps>

파사드의 메서드에 PHPDoc의 `@method` 어노테이션을 붙이면 IDE 자동 완성을 활용할 수 있습니다.

```php theme={null}
// 파사드를 통해 서비스를 호출
use Acme\Courier\Facades\Courier;

Courier::send('user@example.com', '패키지가 도착했습니다');
$status = Courier::track('ABC-123');
```

## DeferrableProvider — 지연 로딩 구현

서비스 컨테이너에 대한 바인딩만 수행하는 프로바이더는 `DeferrableProvider` 인터페이스를 구현하여 지연 로딩을 구현할 수 있습니다. 서비스가 실제로 필요해질 때까지 프로바이더가 로드되지 않으므로 애플리케이션의 성능이 향상됩니다.

```php theme={null}
<?php

namespace Acme\Courier;

use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider implements DeferrableProvider
{
    public function register(): void
    {
        $this->app->singleton(CourierManager::class, function ($app) {
            return new CourierManager($app['config']['courier']);
        });
    }

    /**
     * 이 프로바이더가 제공하는 서비스 목록을 반환
     *
     * @return array<int, string>
     */
    public function provides(): array
    {
        return [CourierManager::class];
    }
}
```

Laravel은 지연 프로바이더가 제공하는 서비스 목록을 컴파일하여 저장합니다. `provides()`에 나열된 서비스가 해석될 때만 프로바이더가 로드됩니다.

<Warning>
  리소스 등록(뷰, 라우트, 이벤트 리스너 등)이 필요한 프로바이더에는 `DeferrableProvider`를 사용하지 마세요. 지연 로딩될 경우 해당 리소스가 등록되지 않은 상태로 남게 됩니다.
</Warning>

## 패키지 테스트

패키지 단독으로 테스트할 때는 [Orchestra Testbench](https://github.com/orchestral/testbench)를 사용합니다. 일반적인 Laravel 애플리케이션 내에 있는 것처럼 패키지의 테스트를 작성할 수 있습니다.

```shell theme={null}
composer require --dev orchestra/testbench
```

테스트 케이스에서 `getPackageProviders()`를 오버라이드하여 패키지의 서비스 프로바이더를 등록합니다.

```php theme={null}
<?php

namespace Acme\Courier\Tests;

use Acme\Courier\CourierServiceProvider;
use Orchestra\Testbench\TestCase as BaseTestCase;

class TestCase extends BaseTestCase
{
    /**
     * 패키지의 서비스 프로바이더를 등록
     */
    protected function getPackageProviders($app): array
    {
        return [
            CourierServiceProvider::class,
        ];
    }

    /**
     * 패키지의 파사드 별칭을 등록
     */
    protected function getPackageAliases($app): array
    {
        return [
            'Courier' => \Acme\Courier\Facades\Courier::class,
        ];
    }

    /**
     * 테스트용 환경 설정
     */
    protected function defineEnvironment($app): void
    {
        $app['config']->set('courier.api_key', 'test-key');
    }
}
```

```php theme={null}
<?php

namespace Acme\Courier\Tests\Feature;

use Acme\Courier\Facades\Courier;
use Acme\Courier\Tests\TestCase;

class CourierTest extends TestCase
{
    public function test_can_send_message(): void
    {
        $result = Courier::send('user@example.com', '테스트 메시지');

        $this->assertTrue($result);
    }
}
```

## Composer에 배포

패키지를 [Packagist](https://packagist.org/)에 배포하기 위한 모범 사례입니다.

**`composer.json`의 기본 설정**

```json theme={null}
{
    "name": "acme/courier",
    "description": "A Laravel courier package",
    "type": "library",
    "license": "MIT",
    "require": {
        "php": "^8.2",
        "illuminate/support": "^11.0||^12.0||^13.0"
    },
    "require-dev": {
        "orchestra/testbench": "^9.0||^10.0",
        "phpunit/phpunit": "^11.0"
    },
    "autoload": {
        "psr-4": {
            "Acme\\Courier\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Acme\\Courier\\Tests\\": "tests/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "Acme\\Courier\\CourierServiceProvider"
            ],
            "aliases": {
                "Courier": "Acme\\Courier\\Facades\\Courier"
            }
        }
    },
    "minimum-stability": "stable",
    "prefer-stable": true
}
```

<Tip>
  `illuminate/support`에 의존함으로써 `illuminate/framework` 전체가 아닌 Laravel의 필요한 컴포넌트만 의존성에 포함할 수 있습니다. 패키지의 의존성 트리를 작게 유지합시다.
</Tip>

**디렉터리 구조 예시**

```
acme/courier/
├── config/
│   └── courier.php
├── database/
│   └── migrations/
│       └── 2024_01_01_000000_create_courier_logs_table.php
├── lang/
│   └── ko/
│       └── messages.php
├── resources/
│   └── views/
│       └── dashboard.blade.php
├── routes/
│   └── web.php
├── src/
│   ├── Console/
│   │   └── Commands/
│   │       └── InstallCommand.php
│   ├── Facades/
│   │   └── Courier.php
│   ├── Http/
│   │   └── Controllers/
│   │       └── TrackingController.php
│   ├── CourierManager.php
│   └── CourierServiceProvider.php
├── tests/
│   ├── Feature/
│   └── TestCase.php
├── composer.json
└── README.md
```

## 관련 페이지

<Columns cols={2}>
  <Card title="서비스 프로바이더" icon="plug" href="/ko/service-providers">
    서비스 프로바이더의 `register`와 `boot` 메서드, 지연 프로바이더의 세부 사항을 확인합니다.
  </Card>

  <Card title="버전 호환성 관리" icon="git-branch" href="/ko/advanced/package-versioning">
    Laravel과 PHP의 메이저 버전 업에 대응하는 전략과 GitHub Actions의 테스트 매트릭스 설정을 설명합니다.
  </Card>
</Columns>


## Related topics

- [Testbench Workbench로 패키지 개발 진행하기](/ko/advanced/package-workbench.md)
- [Orchestra Testbench로 Laravel 패키지를 테스트하기](/ko/advanced/package-testing.md)
- [Laravel Package Skeleton — 공식 패키지용 스타터 템플릿](/ko/blog/package-skeleton-introduction.md)
- [Laravel Agent Detector — AI 에이전트 감지 패키지](/ko/blog/agent-detector-introduction.md)
- [InteractsWithData 트레이트](/ko/advanced/interacts-with-data.md)
