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

# 패키지의 Blade 확장과 컴파일 캐시

> Laravel 13의 BladeCompiler를 분석하여, 커스텀 디렉티브와 조건문 등록, 컴파일 시점과 실행 시점의 분리, 패키지 업데이트 시 뷰 캐시 검증을 해설합니다.

패키지에서 Blade 문법을 제공하려면 **템플릿을 PHP로 변환하는 처리**와 **생성된 PHP를 실행하는 처리**를 분리합니다. 현재 설정값을 컴파일 시점에 포함시키면, 캐시된 뷰에서 오래된 값이 계속 표시될 수 있습니다.

이 페이지에서는 Laravel 공식 문서의 `13.x`와 Framework `v13.37.0`의 구현을 참조하여 [패키지 개발의 기초](/ko/advanced/package-development)를 더 깊이 살펴봅니다. 일반적인 템플릿 문법은 [Blade](/ko/blade)를, 뷰의 네임스페이스와 배포는 [패키지 뷰의 덮어쓰기와 업데이트](/ko/advanced/package-views)를 참조하세요.

## 등록·컴파일·실행을 구분한다

`Blade::directive()`에 전달하는 콜백은 표시할 값이 아니라 **생성할 PHP 코드 문자열**을 반환합니다. 인수인 `$expression`도 평가된 값이 아니라 템플릿에 작성된 PHP 표현식입니다.

```mermaid theme={null}
flowchart TD
    A["프로바이더의 boot에서 확장을 등록"] --> B["뷰를 렌더링"]
    B --> C{"컴파일이 필요?"}
    C -->|Yes| D["directive의 콜백으로<br>PHP 코드를 생성"]
    D --> E["컴파일 결과를 저장"]
    C -->|No| F["저장된 PHP를 사용"]
    E --> G["PHP를 실행하여 HTML을 생성"]
    F --> G
    G --> H["실행 시점의 설정과 인수를 참조"]
```

| API | 등록한 콜백의 역할 | 실행되는 시점 |
| - | - | - |
| `Blade::directive()` | PHP 코드 문자열을 생성한다 | 대상 디렉티브를 컴파일할 때 |
| `Blade::if()` | 조건을 판정한다 | 생성된 PHP가 `Blade::check()`를 호출할 때 |

`view:cache`에서도 컴파일이 수행됩니다. 디렉티브의 생성용 콜백에서는 현재 사용자 조회, DB 쓰기, 외부 API 호출 등을 수행하지 않도록 설계합니다.

## 패키지 고유의 이름으로 등록한다

예로서, 배송 코드 앞에 설정된 라벨을 표시하는 `@acmeCourierLabel($code)`를 제공합니다. Composer의 PSR-4 설정에서 `Acme\Courier\`를 `src/`에 대응시키고, 프로바이더를 [자동 검출](/ko/advanced/package-discovery) 또는 수동으로 등록하는 것을 전제로 합니다.

```php src/CourierServiceProvider.php theme={null}
<?php

namespace Acme\Courier;

use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Blade::directive('acmeCourierLabel', function (string $expression): string {
            return "<?php echo e((string) config('courier.label', 'Tracking').': '.({$expression})); ?>";
        });

        Blade::if('acmeCourierEnabled', function (): bool {
            return (bool) config('courier.enabled', false);
        });
    }
}
```

이 예에서는 `courier.label`은 문자열, `courier.enabled`는 boolean이라는 설정 계약을 둡니다. 설정 파일의 배포와 병합은 [패키지 설정 병합과 캐시](/ko/advanced/package-config-merging)를 참조하세요.

등록은 일반적인 프로바이더의 `boot()`에서 수행하며, `runningInConsole()` 조건으로 감싸지 않습니다. HTTP에서의 렌더링과 Artisan의 `view:cache` 양쪽에서 같은 확장이 필요합니다. Blade 확장만 등록하는 프로바이더를, 다른 서비스가 해석될 때까지 기동하지 않는 지연 프로바이더로 만드는 것도 피합니다.

### 인수는 값이 아니라 표현식으로 받는다

```blade theme={null}
@acmeCourierLabel($code)
```

이 템플릿에서 콜백이 받는 것은 문자열 `$code`입니다. 생성되는 PHP는 다음과 같은 형태가 됩니다.

```php theme={null}
<?php echo e((string) config('courier.label', 'Tracking').': '.($code)); ?>
```

`config()`와 `$code`가 평가되는 것은 이 PHP를 실행하는 시점입니다. 콜백 자체가 `config()`를 읽어 고정된 문자열을 반환하는 구현과는 다릅니다.

확인한 `callCustomDirective()`는 바깥쪽 괄호를 제거하고 표현식을 trim하여 콜백에 전달합니다. 인수를 PHP 값으로 변환하거나 개별 인수로 분할하지는 않습니다. 이 예는 **필수 표현식 하나를 전달하는 문법**으로 공개합니다.

여러 인수를 다루는 경우, 단순히 쉼표로 `explode()`하지 마세요. 배열, 메서드 호출, 문자열 리터럴 안에도 쉼표가 포함됩니다. 속성이나 복잡한 HTML을 다루고 싶다면 [Blade 컴포넌트](/ko/blade)로 제공하는 편이 책임을 분리하기 쉽습니다.

<Warning>
  템플릿의 표현식은 신뢰할 수 있는 개발자가 작성하는 PHP입니다. 사용자 입력을 템플릿 소스로 조립하지 마세요. 또한 커스텀 디렉티브의 `echo`에는 일반적인 `{{ }}`의 이스케이프가 자동으로 적용되지 않습니다. 이 예에서는 생성하는 PHP가 명시적으로 `e()`를 호출합니다.
</Warning>

### 이름 충돌도 호환성 문제가 된다

`directive()`는 이름을 키로 하여 핸들러를 배열에 저장합니다. 같은 이름이면 이전 등록을 덮어쓰고, `compileStatement()`는 내장 변환 메서드보다 커스텀 디렉티브를 먼저 선택합니다. 공식 테스트에도 커스텀 등록으로 `foreach`를 덮어쓰는 케이스가 있습니다.

`label`이나 `enabled` 같은 일반적인 이름이나 내장 이름을 피하고, 패키지 고유의 접두사를 붙입니다. 사용자가 템플릿에서 사용하는 이름과 인수는 공개 API로 취급하며, 변경 시에는 마이그레이션 절차를 제공합니다.

## 조건 분기는 Blade::if로 실행 시점에 판정한다

위의 프로바이더에서 등록한 조건은 다음과 같이 사용할 수 있습니다.

```blade theme={null}
@acmeCourierEnabled
    @acmeCourierLabel($code)
@else
    배송 추적이 비활성화되어 있습니다.
@endacmeCourierEnabled
```

`Blade::if()`는 조건 콜백을 저장한 뒤, 시작·`unless`·`else`·종료용 디렉티브를 등록합니다. 이 예의 시작 부분은 다음 PHP로 변환됩니다.

```php theme={null}
<?php if (\Illuminate\Support\Facades\Blade::check('acmeCourierEnabled')): ?>
```

설정을 확인하는 조건 콜백은 `check()`에서 실행 시점에 호출됩니다. 컴파일 시점에 true·false 결과를 고정하는 처리가 아닙니다. 컴파일된 뷰를 사용하는 HTTP 프로세스에서도 조건 콜백의 등록이 필요합니다.

이 문법은 표시 전환일 뿐, 배송 작업에 대한 인가가 아닙니다. 접근 제어는 라우트나 작업 측에서도 수행합니다.

## 패키지 업데이트로 뷰가 자동 재생성된다고는 할 수 없다

확인한 `Compiler::isExpired()`는 캐시의 존재 여부와, 설정에 따라 원본 뷰와 컴파일된 파일의 갱신 시각을 조사합니다. **프로바이더에 작성된 디렉티브의 변경을 의존 파일로 추적하는 처리가 아닙니다.**

| 변경 내용 | 컴파일된 PHP에 대한 영향 |
| - | - |
| `directive()`가 반환하는 코드를 변경 | 오래된 PHP가 남으므로 재컴파일이 필요 |
| `Blade::if()`의 조건 콜백 처리만 변경 | 생성된 `check()`에서 새 처리를 호출할 수 있지만, 새 코드의 로드가 필요 |
| 디렉티브 이름이나 인수 계약을 변경 | 사용자의 템플릿 수정과 재컴파일이 필요 |
| 뷰를 애플리케이션 측에 배포함 | 패키지 내 뷰 업데이트만으로는 배포된 사본이 업데이트되지 않음 |

공식 문서도 디렉티브 로직을 변경한 후에는 `view:clear`로 캐시를 삭제하도록 안내합니다. 사용하는 애플리케이션의 배포 공정에서, 업데이트한 패키지와 사용자의 뷰를 맞춘 후에 실행합니다.

```bash theme={null}
php artisan view:clear
php artisan view:cache
```

`view:cache`는 프로덕션에서 사전 컴파일하는 경우에 실행합니다. 패키지의 `boot()`에서 매번 캐시를 삭제하거나, 설치 시 사용하는 애플리케이션의 배포 공정을 임의로 변경하지 마세요. 설정 캐시 갱신이나 상주 프로세스 재로드도 사용 환경의 일반적인 배포 절차를 따릅니다.

## 컴파일 결과와 렌더링 결과를 따로 검증한다

먼저 위 프로바이더가 등록된 테스트용 Laravel 애플리케이션에서, 생성되는 PHP를 검증합니다. 다음은 `Tests\TestCase`를 상속하는 Feature 테스트에 추가하는 메서드입니다.

```php theme={null}
public function test_label_directive_compiles_to_runtime_lookup(): void
{
    $compiled = \Illuminate\Support\Facades\Blade::compileString(
        '@acmeCourierLabel($code)',
    );

    $this->assertSame(
        "<?php echo e((string) config('courier.label', 'Tracking').': '.(\$code)); ?>",
        $compiled,
    );
}
```

이 테스트로 PHP를 생성하는 계약은 확인할 수 있지만, 실행 시점의 이스케이프나 조건 분기까지는 검증할 수 없습니다. 실제 패키지에는 뷰 렌더링과 업데이트 시의 검증도 추가합니다.

| 확인 케이스 | 기대하는 결과 |
| - | - |
| 배송 코드에 HTML로 해석되는 문자를 전달 | 생성된 `e()`에 의해 이스케이프된다 |
| 같은 컴파일된 뷰를 사용하고, 렌더링 전에 라벨 설정을 변경 | 다음 렌더링에서 새 설정값이 표시된다 |
| `courier.enabled`를 true·false로 하여 렌더링 | 대응하는 분기만 표시된다 |
| `view:cache` 후 새 애플리케이션을 기동 | 확장이 등록되며, 사전 컴파일이 없는 경우와 같은 결과가 된다 |
| 디렉티브의 생성 코드를 변경하여 배포 | 캐시 재생성 후 새 PHP가 사용된다 |
| Laravel 지원 버전을 업데이트 | 생성 코드·렌더링·캐시 사용의 각 테스트가 통과한다 |

## 관련 페이지

<Columns cols={2}>
  <Card title="패키지 뷰의 덮어쓰기와 업데이트" icon="eye" href="/ko/advanced/package-views">
    네임스페이스의 검색 순서와, 배포된 템플릿을 유지보수하는 방법을 확인합니다.
  </Card>

  <Card title="패키지 캐시와 optimize 통합" icon="gears" href="/ko/advanced/package-optimization">
    자체 캐시의 생성·삭제를 배포 공정에 통합하는 방법을 확인합니다.
  </Card>
</Columns>

## 참고한 1차 자료

* [Laravel 공식 문서: Extending Blade·Custom if statements](https://github.com/laravel/docs/blob/12080c72699b99ec6573f7e79ff4834d9001b6bc/blade.md#extending-blade)
* [Laravel 공식 문서: 패키지의 뷰와 컴포넌트](https://github.com/laravel/docs/blob/12080c72699b99ec6573f7e79ff4834d9001b6bc/packages.md#views)
* [BladeCompiler: directive()·callCustomDirective()·compileStatement()·if()·check()](https://github.com/laravel/framework/blob/v13.37.0/src/Illuminate/View/Compilers/BladeCompiler.php)
* [Compiler: isExpired()에 의한 캐시 판정](https://github.com/laravel/framework/blob/v13.37.0/src/Illuminate/View/Compilers/Compiler.php)
* [BladeCustomTest: 커스텀 디렉티브·조건·내장 문법의 덮어쓰기](https://github.com/laravel/framework/blob/v13.37.0/tests/View/Blade/BladeCustomTest.php)


## Related topics

- [고급 주제](/ko/advanced/index.md)
- [패키지의 라우트 등록과 캐시](/ko/advanced/package-routes.md)
- [패키지 뷰의 덮어쓰기와 업데이트](/ko/advanced/package-views.md)
- [패키지의 이벤트 리스너 등록과 캐시](/ko/advanced/package-events.md)
- [패키지 자동 감지의 내부 구조](/ko/advanced/package-discovery.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.