01. 한눈에 보는 총평표
9개 영역 빠른 비교 — 자세한 내용은 각 섹션에서
| 비교 항목 | DXCMS | Laravel | 핵심 요약 |
|---|---|---|---|
| PHP 버전 지원 | ✅ 5.6+ | △ 8.2+ | DXCMS가 압도적으로 넓음. 저가 호스팅 완전 호환 |
| Composer 의존성 | ✅ 없음 | ❌ 필수 | DXCMS는 FTP 업로드만으로 배포 가능 |
| 라우팅 API | ✅ 95% | ✅ 100% | 문법 거의 동일. named route만 없음 |
| QueryBuilder | ✅ 90% | ✅ 100% | 문법 거의 동일. Model 클래스만 없음 |
| DI 컨테이너 | ✅ 85% | ✅ 100% | bind/singleton/make 동일. 타입힌트 자동해결 없음 |
| 미들웨어 | ✅ 80% | ✅ 100% | 내장 6종 동일. 미들웨어 클래스 구조 없음 |
| 이벤트/훅 | ✅ 75% | ✅ 100% | Action/Filter 구조 동일. 이벤트 클래스 없음 |
| ORM Active Record | ❌ 없음 | ✅ Eloquent | DXCMS 최대 격차. DxModel 추가 필요 |
| 템플릿 엔진 | △ PHP 직접 | ✅ Blade | 기능은 되나 Blade의 편의성 없음 |
| 보안 시스템 | ✅ 우수 | ✅ 우수 | Secure.php 집중화 vs 분산 레이어. 방식 차이만 |
| CMS 기능 | ✅ 압도 | ❌ 없음 | 게시판/회원/포인트/샵/관리자 내장. 라라벨은 직접 구현 |
| 생태계/패키지 | △ 초기 | ✅ 방대 | Packagist vs DX 마켓. 격차 존재 |
| 국내 특화 기능 | ✅ 내장 | ❌ 별도 | 카카오/네이버/토스페이 내장 |
| 저가 호스팅 호환 | ✅ 최고 | ❌ 어려움 | DXCMS의 핵심 강점 영역 |
| 테스트 인프라 | ❌ 없음 | ✅ 완비 | PHPUnit•Feature Test 없음 |
| 공식 문서/커뮤니티 | △ 초기 | ✅ 방대 | 한국어 메뉴얼 있음. 영문/글로벌 없음 |
02. 포지셔닝 — 두 도구의 목적과 철학
무엇을 위해 만들어진 도구인가| DXCMS Board | Laravel |
|---|---|
| PHP CMS + 미니 프레임워크 | 풀스택 PHP 프레임워크 |
| 올인원: 게시판•회원•포인트•샵•관리자 내장 | 기능 없음: 모든 것을 직접 구현 |
| PHP 5.6~8.x 단일 코드베이스 | PHP 8.2+ 전용 |
| Composer 없음 — FTP 업로드만으로 배포 | Composer 필수 — SSH/CLI 환경 필요 |
| 국내 저가 호스팅 완전 최적화 | 국제 표준 호스팅 환경 타겟 |
| 카카오•네이버•토스페이 내장 | 별도 패키지 설치 필요 |
| 설치 마법사 (브라우저 5분 설치) | Artisan CLI (php artisan migrate) |
| 타겟: 커뮤니티•쇼핑몰•기업 홈페이지 | 타겟: API 서버•SaaS•대형 앱 |
💡 핵심: 두 도구는 경쟁 관계가 아니라 "다른 문제를 푸는 다른 도구"입니다.
라라벨이 "빈 캔버스 위에 무엇이든 그릴 수 있는 붓"이라면,
DXCMS는 "한국형 웹사이트에 필요한 것이 이미 그려진 완성형 패키지"입니다.
03. 요청 라이프사이클 비교
HTTP Request가 들어와서 Response가 나가기까지|
|
DXCMS | Laravel |
|---|---|---|
|
1
|
ob_start() + 상수 정의 (DX_ROOT•DX_CMS 등) | Composer Autoloader + Application 인스턴스 생성 |
|
2
|
require_once 20+ 클래스 수동 로드 | HTTP Kernel 부트스트랩 |
|
3
|
Secure::getInstance() — 보안 초기화 | ServiceProvider 자동 등록 (config•route•view 등) |
|
4
|
DB 연결 (data/config.php) | 미들웨어 파이프라인 구성 |
|
5
|
HookManager + PluginRegistry 초기화 | Router 매칭 |
|
6
|
DxSite → DxTheme → Auth 순서 초기화 | 컨트롤러 의존성 자동 주입 (ReflectionClass) |
|
7
|
DxContainer::registerCoreServices() | Response 생성 + 반환 |
|
8
|
extend/top/ 실행 (파일 기반 주입 슬롯) | (라라벨에 없는 개념) |
|
9
|
routes/*.php 자동 로드 | — |
| 10 | DxRouter::dispatch() → 매칭 없으면 폴백 | — |
| 11 | 파일 기반 Dispatcher 폴백 실행 | — |
| 12 | extend/bottom/ 실행 → ob_end_flush() | — |
→ DXCMS의 extend/top/middle/bottom 3점 주입 구조는 라라벨에 없는 독자적 강점입니다.
→ 라라벨의 ServiceProvider 자동 등록이 없어 require_once를 수동 나열하는 것이 가장 큰 구조적 차이입니다.
→ 두 방식 모두 단일 진입점(index.php) → 초기화 → 라우팅 → 디스패치의 흐름은 동일합니다.
04. 디렉토리 구조 비교
프로젝트 폴더 1:1 대응표
| 역할 | DXCMS | Laravel |
|---|---|---|
| 단일 진입점 | index.php | public/index.php |
| 핵심 엔진 | core/ | vendor/laravel/framework/src/ |
| 라우트 정의 | routes/*.php (자동 로드) | routes/web.php, api.php |
| 컨트롤러 | controllers/ | app/Http/Controllers/ |
| 모델 (ORM) | 없음 → QueryBuilder 직접 | app/Models/ |
| 뷰 (템플릿) | themes/{name}/*.php | resources/views/*.blade.php |
| 설정 | data/config.php | config/*.php + .env |
| 공개 에셋 | assets/ | public/ |
| 업로드/캐시 | data/ | storage/ |
| 플러그인/패키지 | plugins/ | vendor/ (Composer) |
| 관리자 패널 | admin/ (완전 내장) | 없음 (Filament 등 설치) |
| 설치 마법사 | install/ | 없음 (Artisan) |
| 확장 자동실행 | extend/ — 독자 강점 | 없음 |
| 미들웨어 클래스 | 없음 → DxRouter 인라인 | app/Http/Middleware/ |
| ServiceProvider | 없음 → plugin.php | app/Providers/ |
| 게시판 스킨 | boards/skins/{skin}/ | 없음 (직접 구현) |
| 테마 | themes/{name}/ | 없음 (직접 구현) |
| 페이지 | pages/ | 없음 (직접 구현) |
05. 라우팅 시스템 — 문법 정밀 비교
DxRouter.php 소스코드 직접 기반
5-1. 기본 라우트 등록
| DXCMS | Laravel |
|---|---|
| // routes/web.php DxRouter::get('/posts','PostController@index'); DxRouter::post('/posts','PostController@store'); DxRouter::put('/posts/{id}','PostController@update'); DxRouter::patch('/posts/{id}','PostController@update'); DxRouter::delete('/posts/{id}','PostController@destroy'); DxRouter::any('/search','SearchController@index'); // 클로저 라우트 DxRouter::get('/health', function() { dx_json(array('status' => 'ok')); exit; }); |
// routes/web.php Route::get('/posts',[PostController::class,'index']); Route::post('/posts',[PostController::class,'store']); Route::put('/posts/{id}',[PostController::class,'update']); Route::patch('/posts/{id}',[PostController::class,'update']); Route::delete('/posts/{id}',[PostController::class,'destroy']); Route::any('/search',[SearchController::class,'index']); // 클로저 라우트 Route::get('/health', function() { return response()->json(['status' => 'ok']); }); |
5-2. 파라미터 & 미들웨어 체이닝
| DXCMS | Laravel |
|---|---|
| DXCMS | Laravel |
|---|---|
| // 파라미터 캡처 + 미들웨어 DxRouter::get('/posts/{id}', 'PostController@show') ->middleware('auth'); // 복수 미들웨어 (PHP 5.6 array()) DxRouter::post('/admin/posts', 'PostController@store') ->middleware(array('admin','csrf')); // 내장 미들웨어 6종: // auth — 로그인 필수 (→ /auth/login) // admin — 관리자 필수 (→ 403) // guest — 비로그인 전용 // csrf — CSRF 토큰 검증 // json — JSON 헤더 자동 // throttle — 요청 제한 |
// 파라미터 캡처 + 미들웨어 Route::get('/posts/{id}', [PostController::class,'show']) ->middleware('auth'); // 복수 미들웨어 Route::post('/admin/posts', [PostController::class,'store']) ->middleware(['auth','verified']); // 커스텀 미들웨어 클래스 // app/Http/Middleware/MyMiddleware.php Route::get('/','HomeController@index') ->middleware(MyMiddleware::class); // named route Route::get('/posts/{id}', ...) ->name('posts.show'); |
5-3. 라우트 그룹
| DXCMS | Laravel |
|---|---|
| DXCMS | Laravel |
|---|---|
| // 그룹 (prefix + middleware) DxRouter::group( array( 'prefix' => '/admin', 'middleware' => 'admin' ), function() { DxRouter::get('/dashboard', 'AdminController@dashboard'); DxRouter::get('/users', 'AdminController@users'); DxRouter::post('/users', 'AdminController@store') ->middleware('csrf'); } ); |
// 그룹 (prefix + middleware) Route::group([ 'prefix' => 'admin', 'middleware' => 'auth:admin', ], function () { Route::get('/dashboard', [AdminController::class,'dashboard']); Route::get('/users', [AdminController::class,'users']); Route::post('/users', [AdminController::class,'store']); }); // 체이닝 스타일도 가능 Route::prefix('admin') ->middleware('auth:admin') ->group(function () { ... }); |
5-4. Resource 라우트
| DXCMS | Laravel |
|---|---|
| DXCMS | Laravel |
|---|---|
| // resource() — 6개 자동 등록 DxRouter::resource('/posts','PostController'); // GET /posts → index // POST /posts → store // GET /posts/{id} → show // PUT /posts/{id} → update // PATCH /posts/{id} → update // DELETE /posts/{id} → destroy // 일부만 등록 DxRouter::resource('/posts', 'PostController', array('index','show')); |
// resource() — 7개 자동 등록 Route::resource('posts', PostController::class); // GET /posts → index // GET /posts/create → create ← 추가 // POST /posts → store // GET /posts/{post} → show // GET /posts/{post}/edit → edit ← 추가 // PUT /posts/{post} → update // DELETE /posts/{post} → destroy // 일부만 등록 Route::resource('posts', PostController::class) ->only(['index','show']); |
5-5. 라우팅 기능 완성도
| 기능 | DXCMS | Laravel | 비고 |
|---|---|---|---|
| GET/POST/PUT/PATCH/DELETE | ✅ | ✅ | 완전 동일 |
| 라우트 파라미터 {id} | ✅ | ✅ | 완전 동일 |
| 미들웨어 체이닝 ->middleware() | ✅ | ✅ | 문법 동일 |
| 라우트 그룹 group() | ✅ | ✅ | 거의 동일 |
| Resource RESTful | ✅ 6종 | ✅ 7종 | create/edit 뷰 라우트 없음 |
| 클로저 라우트 | ✅ | ✅ | 완전 동일 |
| named route ->name() | ❌ | ✅ | dx_route() 헬퍼 없음 |
| 라우트 캐싱 | ❌ | ✅ | route:cache Artisan 없음 |
| 암묵적 모델 바인딩 | ❌ | ✅ | Eloquent Model 없어서 불가 |
| 정규식 제약 ->where() | ❌ | ✅ | 향후 추가 가능 |
| 커스텀 미들웨어 클래스 | ❌ | ✅ | 클로저만 지원, 클래스@handle 없음 |
| 파일 기반 라우팅 폴백 | ✅ 독자강점 | 없음 | 기존 코드 100% 호환 유지 |
06. ORM / QueryBuilder 비교
QueryBuilder.php 소스코드 직접 기반
6-1. 기본 조회
| DXCMS | Laravel |
|---|---|
| DXCMS | Laravel |
|---|---|
| // dx_db() 전역 헬퍼 함수 $posts = dx_db('posts') ->where('status', 1) ->orderBy('id', 'desc') ->limit(10) ->get(); // array[] 반환 $post = dx_db('posts') ->where('id', $id) ->first(); // array|null $title = dx_db('posts') ->where('id', $id) ->value('title'); // 단일값 $titles = dx_db('posts') ->pluck('title'); // 컬럼 배열 |
// DB 파사드 (QueryBuilder) $posts = DB::table('posts') ->where('status', 1) ->orderBy('id', 'desc') ->limit(10) ->get(); // Collection 반환 $post = DB::table('posts') ->where('id', $id) ->first(); // stdClass|null $title = DB::table('posts') ->where('id', $id) ->value('title'); $titles = DB::table('posts') ->pluck('title'); // Collection |
6-2. WHERE 조건 — 전체 비교
| DXCMS | Laravel |
|---|---|
| DXCMS | Laravel |
|---|---|
| dx_db('posts') ->where('status', 1) // = ->where('views', '>=', 100) // >= ->orWhere('featured', 1) // OR ->whereIn('cat_id',[1,2,3]) // IN ->whereNotIn('id',[4,5]) // NOT IN ->whereNull('deleted_at') // IS NULL ->whereNotNull('published_at') // NOT NULL ->whereBetween('views',10,100) // BETWEEN ->whereRaw( 'YEAR(created_at)=?', array(2025)) ->get(); |
DB::table('posts') ->where('status', 1) ->where('views', '>=', 100) ->orWhere('featured', 1) ->whereIn('cat_id',[1,2,3]) ->whereNotIn('id',[4,5]) ->whereNull('deleted_at') ->whereNotNull('published_at') ->whereBetween('views',[10,100]) ->whereRaw( 'YEAR(created_at)=?', [2025]) ->get(); |
6-3. JOIN • GROUP • ORDER
| DXCMS | Laravel |
|---|---|
| DXCMS | Laravel |
|---|---|
| dx_db('posts') ->leftJoin('members', 'posts.member_id','=', 'members.id') ->select(array( 'posts.*', 'members.nickname')) ->groupBy('category_id') ->orderBy('id','desc') ->orderBy('views','desc') ->get(); |
DB::table('posts') ->leftJoin('members', 'posts.member_id','=', 'members.id') ->select( 'posts.*', 'members.nickname') ->groupBy('category_id') ->orderBy('id','desc') ->orderBy('views','desc') ->get(); |
6-4. CRUD — INSERT / UPDATE / DELETE
| DXCMS | Laravel |
|---|---|
| DXCMS | Laravel |
|---|---|
| // INSERT $id = dx_db('posts')->insert(array( 'title' => '제목', 'content' => '내용', 'member_id' => $userId, )); // UPDATE (WHERE 없으면 자동 차단) dx_db('posts') ->where('id', $id) ->update(array('title' => '수정')); // DELETE (WHERE 없으면 자동 차단) dx_db('posts') ->where('id', $id) ->delete(); // UPSERT dx_db('stats')->upsert( array('user_id'=>1,'count'=>1), array('count' => 2) ); // INCREMENT / DECREMENT dx_db('posts') ->where('id',$id)->increment('views'); |
// INSERT $id = DB::table('posts') ->insertGetId([ 'title' => '제목', 'content' => '내용', 'member_id' => $userId, ]); // UPDATE DB::table('posts') ->where('id', $id) ->update(['title' => '수정']); // DELETE DB::table('posts') ->where('id', $id) ->delete(); // UPSERT (Laravel 8+) DB::table('stats')->upsert( ['user_id'=>1,'count'=>1], ['user_id'],['count'] ); // INCREMENT / DECREMENT DB::table('posts') ->where('id',$id)->increment('views'); |
6-5. 페이지네이션 & 집계
| DXCMS | Laravel |
|---|---|
| DXCMS | Laravel |
|---|---|
| // 페이지네이션 $result = dx_db('posts') ->where('status',1) ->orderBy('id','desc') ->paginate(20); // 반환: array( // 'data' => [...], // 'total' => 100, // 'per_page' => 20, // 'current_page' => 1, // 'last_page' => 5, // 'from' => 1, // 'to' => 20, // ) // 집계 dx_db('posts')->count(); dx_db('posts')->max('views'); dx_db('posts')->sum('views'); dx_db('posts')->avg('views'); dx_db('posts')->where('id',1)->exists(); // SQL 디버그 list($sql,$binds) = dx_db('posts') ->where('status',1)->toSql(); |
// 페이지네이션 $result = DB::table('posts') ->where('status',1) ->orderBy('id','desc') ->paginate(20); // 반환: LengthAwarePaginator // $result->items() // $result->total() // $result->perPage() // $result->currentPage() // $result->lastPage() // $result->from() // $result->to() // 집계 DB::table('posts')->count(); DB::table('posts')->max('views'); DB::table('posts')->sum('views'); DB::table('posts')->avg('views'); DB::table('posts')->where('id',1)->exists(); // SQL 디버그 DB::table('posts') ->where('status',1)->toSql(); |
6-6. Eloquent Model — DXCMS 최대 격차
⚠ DXCMS에는 Eloquent에 해당하는 Model 클래스가 없습니다.
QueryBuilder로 모든 DB 작업이 가능하지만, Active Record 패턴(관계•이벤트•스코프)은 지원하지 않습니다.
→ 로드맵: DxModel 기본 클래스 추가 (PHP 5.6 호환, QueryBuilder 위에 얇은 레이어)
| DXCMS | Laravel |
|---|---|
| DXCMS | Laravel |
|---|---|
| // DXCMS — QueryBuilder 직접 사용 $post = dx_db('posts') ->where('id',$id)->first(); // 관계 — JOIN으로 수동 처리 $post = dx_db('posts') ->leftJoin('members', 'posts.member_id' ,'=','members.id') ->where('posts.id',$id) ->first(); // 사용: $post['nickname'] (배열) // ⚠ 없는 것들: // - hasMany/belongsTo 관계 // - creating/updated 이벤트 // - scopePublished() 스코프 // - $timestamps 자동 관리 // - $fillable/$guarded 보호 |
// Eloquent — Active Record class Post extends Model { protected $table = 'posts'; // 관계 자동 해결 public function author() { return $this ->belongsTo(Member::class); } public function comments() { return $this->hasMany(Comment::class); } // 스코프 public function scopePublished($q){ return $q->where('status',1); } } // 사용 $post = Post::with('author') ->published()->find($id); $post->author->nickname; // 객체 |
07 DI 컨테이너 비교
DxContainer.php 소스코드 직접 기반
7-1. 바인딩 & 해석
| DXCMS | Laravel |
|---|---|
| DXCMS | Laravel |
|---|---|
| // 팩토리 바인딩 dx_app()->bind('mailer', function() { return new MyMailer( dx_config('smtp_host') ); }); // 싱글턴 바인딩 dx_app()->singleton('sms', function() { return new AlimtalkSMS( dx_config('alimtalk_key') ); }); // 인스턴스 직접 등록 dx_app()->instance('redis', $redis); // 별칭 dx_app()->alias('db', 'database'); // 바인딩 여부 확인 dx_app()->bound('mailer'); // bool // 해제 dx_app()->forget('mailer'); // 꺼내 쓰기 $m = dx_app()->make('mailer'); $m = dx_make('mailer'); // 단축함수 |
// 팩토리 바인딩 app()->bind(Mailer::class, function($app) { return new MyMailer( config('mail.smtp_host') ); }); // 싱글턴 바인딩 app()->singleton(SMS::class, function($app) { return new AlimtalkSMS( config('services.alimtalk_key') ); }); // 인스턴스 직접 등록 app()->instance('redis', $redis); // 별칭 app()->alias(Mailer::class, 'mailer'); // 바인딩 여부 확인 app()->bound(Mailer::class); // bool // 꺼내 쓰기 $m = app()->make(Mailer::class); $m = app(Mailer::class); // 단축함수 |
7-2. 컨트롤러 자동 의존성 주입
| DXCMS | Laravel |
|---|---|
| DXCMS | Laravel |
|---|---|
| // DxContainer::call()로 실행 dx_app()->call( 'BoardController@index', array('board_key' => 'free') ); // 생성자 파라미터 자동 주입 // (바인딩된 서비스만 주입 가능) class BoardController { private $db; public function __construct( Database $db // 바인딩 필요 ) { $this->db = $db; } public function index($params) { $key = $params['board_key']; // ... } } // 컨트롤러 자동 로드 순서: // 1. controllers/{Class}.php // 2. plugins/*/controllers/{Class}.php |
// 타입힌트만으로 자동 해결 // 컨트롤러 메서드에서 바로 사용 // 생성자 + 메서드 모두 자동 주입 // 미등록 클래스도 Reflection으로 해결 class BoardController extends Controller { private $db; public function __construct( Database $db // 타입힌트만으로 OK ) { $this->db = $db; } public function index( Request $request // 메서드도 자동 ) { // ... } } |
7-3. 핵심 차이 요약
| 기능 | DXCMS | Laravel | 비고 |
|---|---|---|---|
| bind() / singleton() / instance() | ✅ | ✅ | 완전 동일 |
| make() / alias() / bound() | ✅ | ✅ | 완전 동일 |
| 생성자 타입힌트 자동 주입 | △ 바인딩된 것만 | ✅ 모든 클래스 | 라라벨이 더 스마트 |
| 메서드 파라미터 자동 주입 | ❌ | ✅ | 라라벨만 지원 |
| Contextual Binding | ❌ | ✅ | 인터페이스별 다른 구현체 |
| 태깅 (tag) | ❌ | ✅ | |
| ServiceProvider 패턴 | ❌ → plugin.php | ✅ | 개념은 동일, 구조 차이 |
08 미들웨어 비교
DxRouter.php의 runMiddleware() 소스코드 기반
8-1. 내장 미들웨어 비교
| 이름 | DXCMS 동작 | Laravel 동작 |
|---|---|---|
| auth | 미로그인 시 /auth/login?redirect=... 으로 리다이렉트 | Authenticate 미들웨어 — /login 리다이렉트 |
| admin | 비관리자 시 HTTP 403 반환 | auth:admin — Gate 기반 |
| guest | 로그인 상태면 홈으로 리다이렉트 | RedirectIfAuthenticated |
| csrf | dx_csrf_verify() — 실패 시 403 + JSON 에러 | VerifyCsrfToken — 자동 적용 (Web 라우트) |
| json | Content-Type: application/json 헤더 자동 | AcceptsJson + JSON 응답 |
| throttle | 기본 정의됨 (향후 확장) | ThrottleRequests — 분당 횟수 설정 |
8-2. 커스텀 미들웨어
| DXCMS | Laravel |
|---|---|
| DXCMS | Laravel |
|---|---|
| // DXCMS — 클로저 방식만 지원 DxRouter::get('/admin','AdminController@index') ->middleware(function($params) { $auth = Auth::getInstance(); if (!$auth->isLoggedIn()) { header('Location: /auth/login'); exit; } if ($auth->get('level') < 5) { http_response_code(403); exit('권한 없음'); } }); // ⚠ 클래스@handle 구조 미지원 // → 로드맵: 미들웨어 클래스 추가 예정 |
// Laravel — 미들웨어 클래스 // app/Http/Middleware/CheckLevel.php class CheckLevel { public function handle( Request $request, Closure $next, $level = 1 ) { if (!auth()->check()) { return redirect('/login'); } if (auth()->user()->level < $level) { abort(403); } return $next($request); } } // 등록 후 사용 Route::get('/admin','AdminController@index') ->middleware('check.level:5'); |
8-3. extend/ — 미들웨어를 대체하는 독자 방식
| DXCMS | Laravel |
|---|---|
| DXCMS | Laravel |
|---|---|
| // extend/top/01_maintenance.php // 코드 등록 없이 파일만 넣으면 실행됨 $mode = dx_config('maintenance', 0); if ($mode) { $auth = Auth::getInstance(); if (!$auth->isAdmin()) { header('Content-Type: text/html'); echo '<h1>점검 중입니다. 잠시 후 접속하세요.</h1>'; exit; } } // extend/middle/01_access_log.php $uri = dx_request_uri(); $ip = dx_ip(); dx_db('access_logs')->insert(array( 'uri' => $uri, 'ip' => $ip, 'ua' => $_SERVER['HTTP_USER_AGENT'], 'at' => date('Y-m-d H:i:s'), )); |
// Laravel — 동등 구현: 미들웨어 클래스 // (파일 생성 + 등록 + Kernel 추가 필요) // app/Http/Middleware/Maintenance.php class Maintenance { public function handle($req, $next) { if (config('app.maintenance')) { if (!auth()->user()?->isAdmin()) { return response('점검 중', 503); } } return $next($req); } } // app/Http/Middleware/AccessLog.php class AccessLog { public function handle($req, $next) { $res = $next($req); DB::table('access_logs')->insert([..]); return $res; } } // + Kernel.php에 등록 필요 |
09 이벤트 / 훅 시스템 비교
HookManager.php 소스코드 기반
| DXCMS | Laravel |
|---|---|
| // ── Action 훅 (이벤트 발생/구독) ── // 훅 등록 (구독) dx_add_hook('dx_after_login', function($args) { $user = $args['user']; DxPoint::reward($user['id'], 10); DxMemberMonitor::onLogin($user['id']); }, 20 // 우선순위 (낮을수록 먼저) ); // 훅 실행 (이벤트 발생) dx_run_hook('dx_after_login', array('user' => $userData) ); // ── Filter 훅 (값 변환) ── // 필터 등록 dx_add_filter('post_content', function($content) { return nl2br($content); } ); // 필터 실행 — 등록된 콜백들이 순서대로 값 변환 $html = dx_apply_filter('post_content', $raw); // 훅 존재 확인 / 제거 dx_has_hook('dx_after_login'); // bool dx_remove_hook('dx_after_login', $cb); |
// ── Laravel Event (이벤트 발생/구독) ── // 리스너 등록 (EventServiceProvider) Event::listen( UserLoggedIn::class, function(UserLoggedIn $event) { $user = $event->user; Points::reward($user->id, 10); MemberMonitor::onLogin($user->id); } ); // 이벤트 발생 event(new UserLoggedIn($user)); // ── Filter 개념 없음 ── // Pipeline으로 직접 구현해야 함 // 리스너 클래스 방식도 지원 Event::listen( UserLoggedIn::class, SendWelcomeEmail::class ); |
표준 훅 포인트 목록
| 훅 이름 | 발생 시점 | 활용 예 |
|---|---|---|
| dx_top | 모든 페이지 최상단 (body 시작) | 광고 배너•공지•AB테스트 |
| dx_bottom | 모든 페이지 최하단 (body 끝) | 챗봇•스크립트 주입 |
| dx_body_bottom | ob_flush 직전 | DxPopup 팝업 자동 출력 |
| dx_{type}_top/bottom | 게시판/페이지 타입별 | board_top, page_bottom |
| dx_after_login | 로그인 성공 후 | 포인트 지급•로그•알림 |
| dx_after_logout | 로그아웃 후 | 세션 정리•접속 기록 |
| dx_extend_top/bottom | extend/ 실행 직후 | 추가 초기화•후처리 |
| post_content (Filter) | 게시글 본문 출력 시 | 마크다운•하이라이트 변환 |
| board_list_query (Filter) | 게시글 목록 쿼리 시 | 커스텀 필터 주입 |
| before_post_save | 게시글 저장 직전 | 유효성 검사•데이터 가공 |
| after_comment_save | 댓글 저장 완료 후 | 실시간 알림 발송 |
| member_register | 회원가입 완료 후 | 환영 포인트•이메일 |
10 템플릿 엔진 비교
PHP 직접 vs Blade
| DXCMS | Laravel |
|---|---|
| // ── DXCMS — PHP 직접 ── // themes/default/layout/main.php <!DOCTYPE html> <html> <head> <title><?php echo dx_esc($title); ?></title> <?php DxSeo::renderMeta(); ?> </head> <body> <?php dx_hook_top(); // 훅 포인트 ?> <!-- 메뉴 include --> <?php include DX_THEMES.'/default/parts/nav.php'; ?> <!-- 콘텐츠 영역 --> <main><?php echo $dx_content; ?></main> <?php dx_hook_bottom(); // 훅 포인트 ?> <?php dx_run_hook('dx_body_bottom'); ?> </body> </html> |
// ── Laravel — Blade ── // resources/views/layouts/app.blade.php <!DOCTYPE html> <html> <head> <title>{{ $title }}</title> @stack('head') </head> <body> @yield('header') <!-- 컴포넌트 --> @include('partials.nav') <!-- 콘텐츠 영역 --> <main>@yield('content')</main> @stack('scripts') </body> </html> |
순수 PHP이므로 IDE 자동완성이 완벽하게 동작하고, 별도 컴파일 과정이 없어 저가 호스팅에서도 동일하게 실행됩니다.
단, {{ }} 자동 이스케이프가 없으므로 출력 시 반드시 dx_esc()를 사용해야 합니다.
11 보안 시스템 비교
Secure.php 집중화 vs Laravel 분산 레이어
| 보안 항목 | DXCMS Secure.php | Laravel |
|---|---|---|
| CSRF 토큰 | csrfToken()/csrfCheck() TTL 24시간, 활동 시 자동 갱신 | VerifyCsrfToken 미들웨어 Web 라우트 자동 적용 |
| XSS 방어 | dx_esc() 수동 호출 (출력 이스케이프) | Blade {{ }} 자동 이스케이프 |
| 세션 보안 | HttpOnly•Secure•SameSite=Lax 설정 | Session 드라이버 + 암호화 |
| Rate Limit | Redis → 파일 폴백 자동전환 ateLimit($key,$max,$window) |
ThrottleRequests 미들웨어 Route::throttle() |
| 파일 업로드 | MIME+이중확장자 이중 검증 alidateUpload($file) |
Storage 드라이버 + MIME 검증 |
| SQL Injection | PDO 준비된 구문 일관 사용 (QueryBuilder 자동 처리) |
Eloquent PDO 바인딩 자동 |
| 보안 헤더 | sendSecurityHeaders() X-Frame-Options 등 자동 | TrustHosts + 별도 패키지 |
| WAF | POST 본문 SQL/XSS 패턴 탐지 | 없음 (서버/별도 패키지) |
| Bot 탐지 | 검색봇 허용, 의심봇 로그 기록 | 없음 (서버/별도 패키지) |
| 비밀번호 | bcrypt (PHP 5.6 폴백 포함) | Hash::make() bcrypt/argon2 |
| 보안 패치 | Secure.php 1파일만 교체 | 프레임워크 업데이트 |
core/Secure.php 한 파일에 모든 보안 코드가 집중되어 있습니다.
보안 패치 시 이 파일 하나만 교체하면 됩니다. 소수 팀에서 유지보수하기에 최적인 구조입니다.
→ WAF•Bot 탐지는 라라벨이 기본 제공하지 않는 영역에서 DXCMS가 앞서 있습니다.
12 플러그인 vs Service Provider
생태계 확장 패턴 비교
| DXCMS | Laravel |
|---|---|
| DXCMS | Laravel |
|---|---|
| DXCMS | Laravel |
|---|---|
| DXCMS | Laravel |
|---|---|
| DXCMS | Laravel |
|---|---|