메인 콘텐츠로 건너뛰기

패키지란

Laravel에서 말하는 패키지는 애플리케이션에 기능을 추가하는 Composer 패키지입니다. 패키지는 크게 두 종류로 나뉩니다.
  • 스탠드얼론 패키지 — Laravel에 의존하지 않는 범용 PHP 라이브러리(예: Carbon, Pest)
  • Laravel 패키지 — 라우트, 컨트롤러, 뷰, 설정 등 Laravel과 통합된 기능을 가진 패키지
이 가이드에서는 후자, 즉 Laravel 전용 패키지 개발을 다룹니다. 패키지 개발에는 서비스 프로바이더, 파사드, 설정 파일 공개 등 Laravel의 내부 구조에 대한 깊이 있는 이해가 필요합니다.
패키지의 테스트를 작성할 때는 Orchestra Testbench를 사용합니다. 일반적인 Laravel 애플리케이션과 동일하게 패키지의 테스트를 작성할 수 있습니다.

패키지 자동 감지

Laravel은 패키지를 설치할 때 composer.jsonextra.laravel 섹션을 읽어 서비스 프로바이더와 파사드를 자동 등록합니다.
"extra": {
    "laravel": {
        "providers": [
            "Acme\\Courier\\CourierServiceProvider"
        ],
        "aliases": {
            "Courier": "Acme\\Courier\\Facades\\Courier"
        }
    }
}
이 설정을 추가하면 사용자는 bootstrap/providers.php를 수동으로 편집하지 않아도 패키지가 자동으로 로드됩니다.

자동 감지 비활성화

사용자 측에서 특정 패키지의 자동 감지를 비활성화하고 싶은 경우, 애플리케이션의 composer.json에 설정합니다.
"extra": {
    "laravel": {
        "dont-discover": [
            "acme/courier"
        ]
    }
}

서비스 프로바이더의 역할

서비스 프로바이더는 패키지의 진입점입니다. 뷰, 설정, 마이그레이션, 라우트 등의 리소스를 Laravel에 등록하는 처리를 여기에 집약합니다. 서비스 프로바이더는 Illuminate\Support\ServiceProvider를 상속하며, registerboot의 두 메서드를 가집니다.
<?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');
    }
}
register 메서드 내부에서 이벤트 리스너, 라우트, 뷰 등을 등록하지 마세요. 아직 로드되지 않은 다른 서비스 프로바이더의 서비스를 잘못 사용할 가능성이 있습니다. 바인딩 이외의 처리는 반드시 boot 메서드에서 수행합니다.

설정 파일 Publish

publishes() — 파일을 공개

boot 메서드에서 publishes()를 호출하면 사용자가 vendor:publish 명령어로 설정 파일을 자신의 애플리케이션으로 복사할 수 있습니다.
public function boot(): void
{
    $this->publishes([
        __DIR__.'/../config/courier.php' => config_path('courier.php'),
    ]);
}
공개 후 설정값은 일반적인 config 접근과 동일한 방법으로 가져올 수 있습니다.
$value = config('courier.option');

mergeConfigFrom() — 기본값과 병합

register 메서드에서 mergeConfigFrom()을 사용하면 사용자가 설정 파일을 공개하지 않은 경우에도 패키지의 기본값이 사용됩니다.
public function register(): void
{
    $this->mergeConfigFrom(
        __DIR__.'/../config/courier.php', 'courier'
    );
}
mergeConfigFrom()은 중첩된 배열의 깊은 레벨까지는 병합하지 않습니다. 다차원 배열을 가진 설정에서는 사용자가 일부만 정의한 경우 나머지 옵션이 병합되지 않을 수 있습니다.

태그로 공개 그룹 나누기

publishes()의 두 번째 인자로 태그를 지정하면, 사용자가 필요한 리소스만 선택해서 공개할 수 있습니다.
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');
}
# 설정 파일만 공개
php artisan vendor:publish --tag=courier-config

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

라우트 등록

loadRoutesFrom()을 사용하여 라우트 파일을 로드합니다. 애플리케이션의 라우트 캐시가 활성화되어 있는 경우 자동으로 건너뜁니다.
public function boot(): void
{
    $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
}
라우트 파일에서는 패키지의 컨트롤러를 지정합니다.
// 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이 자동으로 타임스탬프를 갱신합니다.
public function boot(): void
{
    $this->publishesMigrations([
        __DIR__.'/../database/migrations' => database_path('migrations'),
    ]);
}

뷰 Publish

loadViewsFrom() — 뷰 등록

loadViewsFrom()으로 뷰 디렉터리를 등록합니다. 두 번째 인자의 네임스페이스를 사용하여 package::view 형식으로 뷰를 참조합니다.
public function boot(): void
{
    $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');
}
등록 후 뷰는 패키지 네임스페이스로 참조합니다.
Route::get('/dashboard', function () {
    return view('courier::dashboard');
});
Laravel은 뷰를 두 곳에서 찾습니다. 먼저 애플리케이션의 resources/views/vendor/courier 디렉터리를 확인하고, 없으면 패키지의 뷰 디렉터리를 사용합니다. 이를 통해 사용자가 뷰를 커스터마이즈할 수 있습니다.

뷰 공개

public function boot(): void
{
    $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');

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

Blade 컴포넌트 등록

컴포넌트를 패키지에 포함시키는 경우, boot 메서드에서 등록합니다.
use Illuminate\Support\Facades\Blade;
use Acme\Courier\View\Components\AlertComponent;

public function boot(): void
{
    Blade::component('courier-alert', AlertComponent::class);
}
컴포넌트 네임스페이스를 사용하여 일괄 등록할 수도 있습니다.
use Illuminate\Support\Facades\Blade;

public function boot(): void
{
    Blade::componentNamespace('Acme\\Courier\\View\\Components', 'courier');
}
{{-- 개별 등록의 경우 --}}
<x-courier-alert />

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

번역 파일 Publish

loadTranslationsFrom()으로 번역 파일을 등록합니다. 번역은 package::file.key 형식으로 참조합니다.
public function boot(): void
{
    $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier');

    $this->publishes([
        __DIR__.'/../lang' => $this->app->langPath('vendor/courier'),
    ]);
}
// 번역 사용
echo trans('courier::messages.welcome');
JSON 번역 파일을 사용할 경우 loadJsonTranslationsFrom()을 사용합니다.
public function boot(): void
{
    $this->loadJsonTranslationsFrom(__DIR__.'/../lang');
}

명령어 등록

패키지의 Artisan 명령어는 commands() 메서드로 등록합니다. 콘솔 환경에서만 등록하는 것이 일반적입니다.
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 optimizephp artisan optimize:clear에 통합할 수 있습니다.
public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->optimizes(
            optimize: 'courier:cache',
            clear: 'courier:clear-cache',
        );
    }
}

about 명령어에 정보 추가

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

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

파사드 생성

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

서비스 클래스 생성

<?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'];
    }
}
2

파사드 클래스 생성

Illuminate\Support\Facades\Facade를 상속하고, getFacadeAccessor()에서 서비스 컨테이너의 바인딩 키를 반환합니다.
<?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;
    }
}
3

서비스 프로바이더에서 바인딩

public function register(): void
{
    $this->app->singleton(\Acme\Courier\CourierManager::class, function ($app) {
        return new \Acme\Courier\CourierManager($app['config']['courier']);
    });
}
4

composer.json에 등록

"extra": {
    "laravel": {
        "providers": [
            "Acme\\Courier\\CourierServiceProvider"
        ],
        "aliases": {
            "Courier": "Acme\\Courier\\Facades\\Courier"
        }
    }
}
파사드의 메서드에 PHPDoc의 @method 어노테이션을 붙이면 IDE 자동 완성을 활용할 수 있습니다.
// 파사드를 통해 서비스를 호출
use Acme\Courier\Facades\Courier;

Courier::send('[email protected]', '패키지가 도착했습니다');
$status = Courier::track('ABC-123');

DeferrableProvider — 지연 로딩 구현

서비스 컨테이너에 대한 바인딩만 수행하는 프로바이더는 DeferrableProvider 인터페이스를 구현하여 지연 로딩을 구현할 수 있습니다. 서비스가 실제로 필요해질 때까지 프로바이더가 로드되지 않으므로 애플리케이션의 성능이 향상됩니다.
<?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()에 나열된 서비스가 해석될 때만 프로바이더가 로드됩니다.
리소스 등록(뷰, 라우트, 이벤트 리스너 등)이 필요한 프로바이더에는 DeferrableProvider를 사용하지 마세요. 지연 로딩될 경우 해당 리소스가 등록되지 않은 상태로 남게 됩니다.

패키지 테스트

패키지 단독으로 테스트할 때는 Orchestra Testbench를 사용합니다. 일반적인 Laravel 애플리케이션 내에 있는 것처럼 패키지의 테스트를 작성할 수 있습니다.
composer require --dev orchestra/testbench
테스트 케이스에서 getPackageProviders()를 오버라이드하여 패키지의 서비스 프로바이더를 등록합니다.
<?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

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('[email protected]', '테스트 메시지');

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

Composer에 배포

패키지를 Packagist에 배포하기 위한 모범 사례입니다. composer.json의 기본 설정
{
    "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
}
illuminate/support에 의존함으로써 illuminate/framework 전체가 아닌 Laravel의 필요한 컴포넌트만 의존성에 포함할 수 있습니다. 패키지의 의존성 트리를 작게 유지합시다.
디렉터리 구조 예시
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

관련 페이지

서비스 프로바이더

서비스 프로바이더의 registerboot 메서드, 지연 프로바이더의 세부 사항을 확인합니다.

버전 호환성 관리

Laravel과 PHP의 메이저 버전 업에 대응하는 전략과 GitHub Actions의 테스트 매트릭스 설정을 설명합니다.
마지막 수정일 2026년 7월 13일