이것만으로 Feature::active('billing-v2')가 자동으로 현재 팀을 대상으로 합니다. 팀 멤버가 누구든 같은 팀에 속하는 한 같은 결과가 반환되므로, UI의 일관성이 유지됩니다.팀 가입일에 따른 단계적 롤아웃 예시입니다.
use App\Models\Team;use Illuminate\Support\Carbon;use Illuminate\Support\Lottery;use Laravel\Pennant\Feature;Feature::define('new-billing', function (Team $team) { // 2024년 이후에 등록한 팀은 즉시 유효 if ($team->created_at->isAfter(new Carbon('2024-01-01'))) { return true; } // 2022~2023년 등록은 10%씩 롤아웃 if ($team->created_at->isAfter(new Carbon('2022-01-01'))) { return Lottery::odds(1 / 10); } // 오래된 팀은 신중하게 1%부터 return Lottery::odds(1 / 100);});
특정 팀만 활성화하고 싶은 경우(엔터프라이즈 고객에게 선행 제공 등):
// 배포 후 특정 팀만 수동으로 활성화Feature::for($earlyAccessTeam)->activate('new-billing');
프로덕션에서 버그가 발각되었을 때, 코드 롤백 없이 즉시 기능을 무효화할 수 있습니다. 클래스 기반 피처에 before 메서드를 추가하면 스토리지의 값보다 먼저 체크가 실행됩니다.
<?phpnamespace App\Features;use App\Models\User;use Illuminate\Support\Facades\Config;class NewCheckout{ /** * 프로덕션 버그 발생 시 config로 즉시 무효화할 수 있는 긴급 스위치 */ public function before(User $user): mixed { // config/features.php의 값, 또는 env로 제어 if (Config::boolean('features.new_checkout_disabled', false)) { return false; // 전원에게 즉시 무효화 } // 관리자는 항상 유효 (디버그를 위해) if ($user->isAdmin()) { return true; } return null; // null을 반환하면 일반적인 resolve()로 진행 } public function resolve(User $user): mixed { return $user->isPremium() || Lottery::odds(1 / 5); }}
환경 변수 FEATURES_NEW_CHECKOUT_DISABLED=true를 설정하는 것만으로, DB를 건드리지 않고 기능을 멈출 수 있습니다. 배포 불필요한 킬 스위치입니다.
before가 null을 반환하면 resolve()가 실행됩니다. false를 반환하면 즉시 비활성으로 처리됩니다. 긴급 시 이외에는 null을 반환하도록 하세요.
특정 일시에 자동으로 전 사용자에게 공개하고 싶은 케이스. before 메서드로 구현할 수 있습니다.
public function before(User $user): mixed{ $rolloutDate = Config::get('features.new_api.rollout_date'); if ($rolloutDate && Carbon::parse($rolloutDate)->isPast()) { return true; // 롤아웃 날짜를 지나면 전원에게 공개 } return null; // 아직인 경우는 일반적인 resolve()로}public function resolve(User $user): mixed{ // 롤아웃 전에는 사내 멤버만 return $user->isInternalTeamMember();}
# .envFEATURES_NEW_API_ROLLOUT_DATE=2025-04-01
이제 2025-04-01을 지나면 자동으로 전 사용자에게 공개됩니다. 배포 없이, DB 없이, Artisan 명령 없이 자동화할 수 있습니다.
공식 문서에는 FeatureRetrieved 이벤트에 대한 언급이 있지만, 실제 A/B 테스트 집계 패턴은 제시되어 있지 않습니다.FeatureResolved 이벤트는 피처의 값이 처음으로 해결되었을 때만 발화합니다. 이것을 사용하여 사용자의 배리언트 할당을 기록합니다.
use Laravel\Pennant\Events\FeatureResolved;use Illuminate\Support\Facades\Event;// AppServiceProvider나 EventServiceProvider에 등록Event::listen(function (FeatureResolved $event) { if ($event->feature !== 'purchase-button') { return; } // 사용자에게 배리언트 할당 시점에 기록 analytics()->identify($event->scope?->id, [ 'ab_purchase_button' => $event->value, ]);});
큐의 잡에서는 인증된 사용자가 존재하지 않으므로, 피처 체크가 예상과 다른 동작을 할 수 있습니다. 잡에 스코프를 명시적으로 가지게 하세요.
class SendWeeklyDigest implements ShouldQueue{ public function __construct( private readonly User $user ) {} public function handle(): void { // NG: 인증 사용자가 존재하지 않으므로 항상 false가 됨 // if (Feature::active('new-digest-layout')) { ... } // OK: 사용자를 명시적으로 전달 if (Feature::for($this->user)->active('new-digest-layout')) { $this->sendNewLayout($this->user); } else { $this->sendLegacyLayout($this->user); } }}
클래스 기반 피처를 리네임할 때, DB에 저장된 플래그 이름이 바뀌면 모든 사용자의 플래그가 리셋되어 버립니다. Name 어트리뷰트로 저장 이름을 고정합시다.
use Laravel\Pennant\Attributes\Name;// 이전 클래스명: CheckoutV2 → 새 클래스명: NewCheckoutExperience// DB에는 항상 'checkout-v2'로 저장됨#[Name('checkout-v2')]class NewCheckoutExperience{ public function resolve(User $user): mixed { return $user->isPremium(); }}