1. 개요 — 전체 데이터 흐름 조감도
DX CMS의 모든 요청은 단일 진입점 index.php를 통해 처리됩니다. HTTP 요청이 수신된 순간부터 HTML 응답이 전송되기까지, 데이터는 다음 6단계 파이프라인을 직렬로 통과합니다.
flowchart TD
A["HTTP 요청 (GET / POST / PUT / DELETE)"]
B["PHASE 1: 전처리
- ob_start()
- URL 정규화 (이중슬래시 301)
- PHP 버전체크
- 상수 정의 (DX_CMS, DX_ROOT)
- 설치 여부 확인"]
C["PHASE 2: 보안 초기화
- Secure::initSession()
- session_start()
- 보안 헤더 발행
- CSRF 토큰 발급
- 동적 세션키 (DX_SECRET_KEY)"]
D["PHASE 3: 서비스 초기화
- config.php (DB 연결)
- HookManager
- PluginRegistry / load_plugins()
- DxSite (멀티사이트)
- DxTheme (테마 확정)
- Auth (세션/쿠키 인증)
- DxContainer
- extend/top 실행"]
E["PHASE 4: 라우팅
- DxRouter::dispatch()
→ 성공: Controller 실행
→ 실패: Router::resolve()
- URI 파싱
- DB 조회 (게시판/페이지)
- route 배열 확정
- extend/middle 실행
- 접근권한 체크
- 핸들러 결정"]
F["PHASE 5: 비즈니스 로직 + 렌더링
- handler.php
- 입력 검증 / CSRF 확인
- DB 조회/쓰기
- 데이터 준비
- DxTheme::resolveBoardSkin()
- 스킨 include
- ob_start(스킨)
- layout/main.php 렌더링"]
G["PHASE 6: 후처리 + 응답
- extend/bottom 실행
- ob_end_flush()
- HTML 응답 전송"]
A --> B --> C --> D --> E --> F --> G
핵심 설계 원칙
모든 데이터는 단방향(요청→처리→응답)으로 흐르며, 각 Phase는 이전 Phase의 결과에만 의존합니다.
싱글턴 패턴으로 관리되는 서비스(Database, Auth, DxTheme 등)는 Phase 3에서 한 번만 초기화되고,
이후 어떤 파일에서든 getInstance()로 즉시 재사용됩니다.
2. PHASE 1 — 전처리
PHASE 1 전처리 단계
ob_start() • URL 정규화 • 상수 정의 • 설치 확인
2.1 출력 버퍼링 활성화
index.php의 첫 번째 실행 코드는 ob_start()입니다. 이 한 줄이 DX CMS의 신뢰성을 크게 높이는 역할을 합니다.
ob_start(); // index.php 최상단 — 가장 먼저 실행
| 역할 | 설명 |
|---|---|
| 헤더 충돌 방지 | PHP 출력 발생 후 header() 호출 시 나타나는 "headers already sent" 오류를 근본 차단 |
| IIS/CGI 호환 | IIS, CGI, 저가형 웹호스팅 환경에서 header() 동작을 안정적으로 보장 |
| 에러 복구 | Phase 진행 중 예상치 못한 출력 발생 시 ob_end_clean()으로 버퍼 초기화 가능 |
| 최종 flush | Phase 6의 ob_end_flush()가 모든 HTML을 한 번에 전송 — I/O 효율 향상 |
2.2 URL 정규화 (이중 슬래시 301)
REQUEST_URI에 이중 슬래시(//)가 포함된 경우 301 리다이렉트로 정규화합니다. 중복 URL로 인한 SEO 불이익과 라우팅 오작동을 예방합니다.
// 예: /board//free → /board/free (301 Redirect)
if (strpos($_dxRawPath, "//") !== false) {
header("Location: " . preg_replace("#/+#", "/", $_dxRawPath) . $_dxRawQuery, true, 301);
exit;
}
2.3 핵심 상수 정의
Phase 1에서 정의된 상수는 이후 모든 파일에서 경로 하드코딩 없이 사용됩니다.| 상수명 | 값 및 역할 |
|---|---|
| DX_CMS | true — 직접 접근 차단 체크에 사용 (if (!defined("DX_CMS")) exit) |
| DX_VERSION | "8.1.0" — 버전 문자열 |
| DX_ROOT | 설치 루트 절대 경로 (백슬래시→슬래시 정규화) |
| DX_CORE | DX_ROOT . "/core" — 핵심 클래스 디렉터리 |
| DX_DATA | DX_ROOT . "/data" — config.php, error.log 위치 |
| DX_THEMES | DX_ROOT . "/themes" — 테마 디렉터리 |
| DX_PLUGINS | DX_ROOT . "/plugins" — 플러그인 디렉터리 |
| DX_BOARDS | DX_ROOT . "/boards" — 게시판 핸들러 위치 |
| DX_PAGES | DX_ROOT . "/pages" — 페이지 파일 위치 |
| DX_EXTEND | DX_ROOT . "/extend" — 확장 스크립트 위치 |
| DX_START | microtime(true) — 성능 측정용 시작 시각 |
2.4 설치 여부 확인 및 분기
data/config.php 파일이 존재하는지 확인합니다. 파일이 없으면 설치 화면으로, 있으면 정상 부팅 경로로 진행합니다.
$_dxConfigFile = DX_ROOT . "/data/config.php";
$_dxInstalled = file_exists($_dxConfigFile);
if (!$_dxInstalled) {
// install/ 디렉터리로 301 리다이렉트
header("Location: " . $scheme . "://" . $host . "/install/");
exit;
}
💡 에러 출력 전략
미설치 환경: display_errors=1 (설치 디버깅을 위해 화면에 오류 표시)
설치 후 운영: display_errors=0, log_errors=1 → data/error.log에 기록
이 분기는 하나의 코드베이스가 설치 전/후를 모두 처리할 수 있게 합니다.
3. PHASE 2 — 보안 초기화
PHASE 2 보안 초기화
Secure 클래스 • 세션 • CSRF • 보안 헤더보안 관련 모든 처리는 Secure 클래스 하나에 집중됩니다. 이 클래스는 설치 시 core/security/{16자리 해시}/ 에 복사되어 공격자가 예측할 수 없는 경로에 위치합니다.
3.1 보안 경로 선취 파싱
Secure.php 로드 이전에 config.php에서 DX_SECURITY_PATH 상수만 정규식으로 먼저 추출합니다. DB 연결 없이 파일 텍스트만 파싱하는 이유는, 이 단계에서 아직 PDO가 초기화되지 않았기 때문입니다.
// config.php 텍스트에서 DX_SECURITY_PATH만 선취 파싱
$_dxCfgRaw = @file_get_contents($_dxConfigFile);
preg_match("/define.*DX_SECURITY_PATH.*([a-f0-9]{16}).*/", $_dxCfgRaw, $_dxSpM);
define("DX_SECURITY_PATH", $_dxSpM[1]); // "a3f1c8e2d94b5071" 형태
// Secure.php 로드 경로: core/security/{hash}/Secure.php
$_dxSecurePath = DX_CORE . "/security/" . DX_SECURITY_PATH . "/Secure.php";
3.2 세션 초기화 흐름
Secure::initSession()은 session_start() 전에 세션 설정을 완료합니다. 또한 GET 요청이고 세션 쿠키가 없는 경우 세션 시작을 건너뛰어 불필요한 파일 락을 제거합니다.
// 세션 필요 여부 판단
$_dxNeedSession = true;
if ($_SERVER["REQUEST_METHOD"] === "GET" && empty($_COOKIE[session_name()])) {
// admin, auth, view, api, write, edit, reply 는 세션 필요
// 단순 목록•페이지 조회는 세션 불필요 → 파일락 제거, 성능 향상
$_dxNeedSession = false;
}
if ($_dxNeedSession) { Secure::getInstance()->startSession(); }
3.3 DX_SECRET_KEY 기반 동적 키
사이트마다 다른 64자리 랜덤 secret_key를 DX_SECRET_KEY 상수로 정의합니다. 이 키를 기반으로 세션 키 이름, CSRF 토큰 이름을 동적으로 도출합니다.
// secret_key 로드 (DB config에서)
$_dxSecretVal = dx_config("secret_key", "");
define("DX_SECRET_KEY", $_dxSecretVal);
// Secure::initSecretKeys() 내부에서
// SESSION_KEY = hash_hmac("sha256", "session", secret_key)의 앞 16자리
// CSRF_KEY = hash_hmac("sha256", "csrf", secret_key)의 앞 16자리
// 사이트마다 키 이름이 달라 소스코드 공개로도 예측 불가
3.4 보안 헤더 발행
Secure::sendSecurityHeaders()는 .htaccess나 web.config 없는 공유 호스팅에서도 브라우저 수준의 보안을 보장합니다.| 헤더 | 역할 |
|---|---|
| X-Frame-Options: SAMEORIGIN | Clickjacking 방어 — 외부 iframe 삽입 차단 |
| X-Content-Type-Options: nosniff | MIME 타입 스니핑 공격 방어 |
| X-XSS-Protection: 1; | mode=block 구형 브라우저 XSS 필터 활성화 |
| Referrer-Policy: strict-origin... | 외부 링크 클릭 시 민감한 URL 정보 유출 방지 |
| Content-Security-Policy | 스크립트•스타일 출처 제한 (설정 기반 동적 구성) |
3.5 CSRF 토큰 흐름
세션이 있는 요청에서만 CSRF 토큰을 발급합니다. POST 요청 처리 시 handler.php에서 dx_csrf_verify()로 검증합니다.| 단계 | 위치 | 동작 |
|---|---|---|
| 토큰 발급 | Phase 2 — index.php | Secure::csrfToken() → 세션에 토큰 저장 |
| 토큰 출력 | 폼 템플릿 — 스킨 파일 | dx_csrf_field() → <input type="hidden" value="토큰"> |
| 토큰 검증 | Phase 5 — handler.php | dx_csrf_verify() → 세션 값과 POST 값 비교 |
| 실패 처리 | DxRouter 미들웨어 | 403 + JSON 오류 응답 즉시 반환 |
4. PHASE 3 — 서비스 초기화
PHASE 3 서비스 초기화
DB • 플러그인 • 멀티사이트 • 테마 • 인증Phase 3는 CMS가 실제로 "살아나는" 단계입니다. DB 연결이 완료되고, 모든 서비스가 순서대로 초기화됩니다. 초기화 순서가 중요합니다 — 각 서비스는 앞서 초기화된 서비스에 의존합니다.
4.1 DB 연결 — config.php
require_once $_dxConfigFile 한 줄로 DB 연결이 완료됩니다. config.php 내부에서 Database::getInstance()->connect()가 호출되며, PDO 옵션•접두사•캐릭터셋이 설정됩니다
// data/config.php 내부 구조 (예시)
define("DX_SECRET_KEY", "abcdef...64자리...123456");
define("DX_SECURITY_PATH", "a3f1c8e2d94b5071");
$db = Database::getInstance();
$db->connect($host, $dbName, $dbUser, $dbPass, "utf8mb4", "dx_");
// DB에서 전역 설정($dx_config) 로드
$settings = $db->rows("SELECT * FROM `dx_settings`");
foreach ($settings as $s) { $dx_config[$s["key"]] = $s["value"]; }
4.2 서비스 초기화 순서와 의존성
| 순서 | 서비스 | 의존 서비스 및 역할 |
|---|---|---|
| ① | HookManager | 의존: 없음 — 이벤트 버스, 가장 먼저 초기화 |
| ② | PluginRegistry | 의존: 없음 — 플러그인 타입 저장소 |
| ③ | load_plugins() | 의존: HookManager + PluginRegistry — plugins/**/plugin.php 자동 로드 |
| ④ | DxSite | 의존: Database — 도메인별 $dx_config 오버라이드 (멀티사이트) |
| ⑤ | DxTheme | 의존: DxSite 완료 후 (테마명 확정 후) — 테마 파일 해석기 초기화 |
| ⑥ | Auth | 의존: Database + Secure(세션) — 세션/쿠키로 사용자 로드 |
| ⑦ | DxContainer | 의존: 모든 서비스 — DI 컨테이너에 핵심 서비스 등록 |
| ⑧ | extend/top/ | 실행 의존: 모든 서비스 초기화 완료 — 사용자 정의 초기화 스크립트 |
4.3 DxSite — 멀티사이트 설정 오버라이드
DxSite::getInstance()는 현재 요청의 HTTP_HOST를 감지하고, dx_sites 테이블에서 도메인 매핑을 조회합니다. 매핑이 있으면 해당 도메인 설정이 전역 $dx_config를 덮어씁니다.
// DxSite 내부 흐름
$host = preg_replace("/:\d+$/", "", $_SERVER["HTTP_HOST"]); // 포트 제거
$site = $db->row("SELECT * FROM `dx_sites` WHERE domain=? AND status=1", [$host]);
if ($site) {
// site_name, theme, language, timezone 등 오버라이드
foreach ($map as $field => $configKey) {
if (!empty($site[$field])) dx_config_set($configKey, $site[$field]);
}
// extra_config (JSON) 추가 설정
$extra = json_decode($site["extra_config"], true);
foreach ($extra as $k => $v) dx_config_set($k, $v);
}
4.4 DxTheme — 테마 폴백 체인 초기화
DxSite 완료 후 테마명이 확정되면 DxTheme이 초기화됩니다. 테마 디렉터리가 없으면 자동으로 "default"로 폴백합니다.
// DxTheme 파일 해석 우선순위 (resolveBoardSkin 예시)
// 1. themes/{활성테마}/board/{스킨}/list.php
// 2. themes/{활성테마}/board/list.php
// 3. themes/default/board/{스킨}/list.php
// 4. themes/default/board/list.php
// → null이면 404
4.5 Auth — 세션•쿠키 기반 인증
Auth::getInstance() 호출 시 loadSession()이 실행되어 현재 요청자의 인증 상태가 결정됩니다.
// Auth::loadSession() 흐름
if (!isset($_SESSION[$sessionKey])) {
$this->tryRememberMe(); // Remember Me 쿠키로 자동 로그인 시도
return;
}
$userId = (int)$sessionData["id"];
$user = $db->find("members", ["id"=>$userId, "status"=>1]);
// 세션 토큰 검증 (hash_hmac SHA-256: id + join_date + secret_key)
if ($sessionData["token"] !== $this->makeToken($user)) {
$this->logout(); // 토큰 불일치 → 강제 로그아웃
}
| 인증 경로 | 트리거 | 처리 내용 |
|---|---|---|
| 세션 로그인 | 세션 쿠키 존재 | $_SESSION에서 user_id 읽기 → DB 조회 → 토큰 검증 |
| Remember Me | dx_remember 쿠키 | DB remember_token 비교 → 성공 시 토큰 롤링 갱신(30일) |
| 비로그인 | 세션•쿠키 없음 | $this->user = null → dx_is_login() = false |
| 토큰 불일치 | 세션 위변조 시도 | logout() → 세션 삭제 + 쿠키 만료 |
5. PHASE 4 — 라우팅
PHASE 4 라우팅
DxRouter • Router::resolve() • Dispatcher라우팅은 URI를 분석하여 어떤 핸들러가 요청을 처리할지 결정하는 단계입니다. DX CMS는 두 개의 라우팅 레이어를 직렬로 실행합니다.
5.1 DxRouter — 코드 기반 라우팅 (우선)
routes/ 폴더의 PHP 파일에 등록된 라우트를 먼저 처리합니다. URI 패턴과 HTTP 메서드가 일치하면 미들웨어를 실행하고 액션을 호출합니다.
// DxRouter::dispatch() 내부 흐름
$method = $_SERVER["REQUEST_METHOD"]; // GET / POST / PUT …
$uri = self::currentUri(); // "/board/free/view/123"
foreach (self::$routes as $route) {
if ($route["method"] !== $method) continue;
if (!self::matchUri($route["uri"], $uri, $params)) continue;
// URI 패턴 매칭: {id} → (?P<id>[^/]+) 정규식 변환
// 미들웨어 순차 실행
foreach ($route["middleware"] as $mw) {
$result = self::runMiddleware($mw, $params);
if ($result === false) return true;
}
self::runAction($route["action"], $params);
return true; // 매칭 성공
}
return false; // 매칭 실패 → Dispatcher 폴백
5.2 내장 미들웨어 종류
| 미들웨어 | 동작 |
|---|---|
| auth | 미로그인 시 auth/login?redirect=... 으로 리다이렉트 |
| admin | 관리자가 아닌 경우 HTTP 403 즉시 반환 |
| guest | 이미 로그인한 경우 홈으로 리다이렉트 (로그인 페이지 보호) |
| csrf | dx_csrf_verify() 실패 시 403 + JSON 오류 응답 |
| json | Content-Type: application/json 헤더 자동 설정 |
| throttle | 분당 요청 수 제한 (기본값 60회/분, 향후 확장 예정) |
| 커스텀 | 클로저 또는 ClassName@handle 형태로 직접 구현 가능 |
5.3 Router::resolve() — 파일 기반 라우팅 (폴백)
DxRouter에서 매칭되지 않으면 Dispatcher → Router::resolve()가 실행됩니다. URI 세그먼트를 분석하고 DB를 조회하여 route 배열을 확정합니다.
// URI: /free/view/1745123456789012
$segments = ["free", "view", "1745123456789012"];
// $segments[0] = "free" → boards 테이블에서 board_key="free" 조회
// $segments[1] = "view" → boardActions 목록에 있음 → TYPE_BOARD
// $segments[2] = "1745…" → BIGINT ID (ctype_digit 검증 후 문자열 유지)
| URI 패턴 | 타입 | 처리 파일 |
|---|---|---|
| / (루트) | TYPE_HOME | 테마/page/home.php → pages/home.php |
| /admin/… | TYPE_ADMIN | admin/index.php (관리자 전용) |
| /auth/login | TYPE_AUTH | core/auth/login.php |
| /api/… | TYPE_API | core/api/{action}.php |
| /search | TYPE_SEARCH | core/search/handler.php |
| /{board_key} | TYPE_BOARD | boards/handler.php (DB 조회 필요) |
| /{slug} | TYPE_PAGE | pages/{slug}.php (DB 조회 필요) |
| 매칭 없음 | TYPE_404 | 테마/page/404.php |
5.4 라우팅 데이터 흐름 — route 배열
Router::resolve()의 결과는 route 배열로 반환되어 $GLOBALS["dx_route"]에 저장됩니다. 이 배열이 Phase 5 핸들러로 전달되는 핵심 데이터 컨텍스트입니다.
// route 배열 구조 (게시판 view 예시)
$route = array(
"type" => "board",
"uri" => "/free/view/1745123456789012",
"segments" => ["free", "view", "1745123456789012"],
"slug" => "free",
"action" => "view",
"id" => "1745123456789012", // 문자열 — 32bit 오버플로우 방지
"board" => [/* dx_boards row */],
"page" => null,
);
$GLOBALS["dx_route"] = $route;
5.5 extend/middle/ — 라우트 확정 직후 실행
Dispatcher::dispatch()에서 route 배열이 확정된 직후, 핸들러 실행 전에 extend/middle/ 폴더의 스크립트가 자동 실행됩니다. 이 시점에 $GLOBALS["dx_route"]를 통해 라우트 정보를 읽을 수 있습니다.
// Dispatcher::dispatch() 내부
$this->route = $this->router->resolve();
$GLOBALS["dx_route"] = $this->route;
// extend/middle/ 실행 — 방문자 통계, A/B테스트, 접근제어 등
DxExtend::getInstance()->runMiddle(array(
"type" => $this->route["type"],
"route" => $this->route,
));
// 이후 switch($type) → 핸들러 실행
6. PHASE 5 — 비즈니스 로직 + 렌더링
PHASE 5 비즈니스 로직 + 렌더링
handler.php • DxTheme 스킨 • 레이아웃Phase 5는 실제 CMS 기능이 실행되는 핵심 단계입니다. 입력 데이터 검증, DB 조회•쓰기, 출력 데이터 준비, 테마 렌더링이 순차적으로 이루어집니다.
6.1 게시판 핸들러 — boards/handler.php
TYPE_BOARD 라우트는 boards/handler.php로 디스패치됩니다. 핸들러는 action에 따라 분기하여 각각의 로직을 실행합니다.
// Dispatcher::dispatchBoard() → boards/handler.php
$GLOBALS["dx_board"] = $board; // boards 테이블 row
$GLOBALS["dx_action"] = $action; // list / view / write / edit / delete …
$GLOBALS["dx_board_skin"] = $board["skin"] ?: "default";
require DX_BOARDS . "/handler.php";
6.1.1 게시판 액션별 데이터 흐름
| 액션 | 입력 데이터 | DB 조작 | 출력 데이터 |
|---|---|---|---|
| list | page, sort, keyword (GET) | 게시물 목록+COUNT SELECT | 게시물 배열, 페이지네이션 |
| view | id (URL 세그먼트) | 게시물 SELECT + view_count UPDATE | 게시물 row, 댓글 배열 |
| write POST | title, content, files (POST) | insertWithMicrotimeId + 파일 INSERT | 신규 게시물 ID, 리다이렉트 |
| edit POST | id, title, content (POST) | UPDATE posts + 파일 처리 | 수정된 게시물 ID, 리다이렉트 |
| delete | id (POST + CSRF) | DELETE posts + 파일 삭제 | 목록 리다이렉트 |
| reply | id, content (POST) | INSERT comments + hook | 댓글 추가, AJAX JSON 응답 |
6.1.2 list 액션 데이터 흐름 상세
1. URL 파라미터 읽기: $_GET["page"], $_GET["sort"], $_GET["keyword"]2. Board 설정 로드: $GLOBALS["dx_board"]에서 per_page, read_level, write_level 읽기
3. 접근 권한 체크: read_level에 따라 비로그인자 차단 여부 결정
4. 검색 조건 조립: keyword가 있으면 WHERE title LIKE ? OR content LIKE ?
5. COUNT 쿼리: Database::value()로 전체 게시물 수 조회 → 페이지네이션 계산
6. 목록 쿼리: Database::rows()로 현재 페이지 게시물 배열 조회
7. 스킨 파일 결정: DxTheme::resolveBoardSkin(skin, "list") → 폴백 체인 실행
8. 렌더링: ob_start() → 스킨 include → $dx_content → layout/main.php include
6.1.3 write POST 데이터 흐름 상세
1. CSRF 검증: dx_csrf_verify() — 실패 시 즉시 403 반환2. 로그인 확인: Auth::isLoggedIn() — write_level에 따라 차단
3. 입력 정제: DxSanitizer::clean() — XSS 방어 (허용 태그 화이트리스트)
4. 파일 업로드 처리: MIME 검증 → 확장자 검증 → 랜덤 파일명 → uploads/ 저장
5. 트랜잭션 시작: $db->begin()
6. 게시물 INSERT: $db->insertWithMicrotimeId("posts", $data)
7. 파일 메타 INSERT: $db->insertRow("post_files", $fileData)
8. 트랜잭션 커밋: $db->commit() — 실패 시 rollback()
9. 훅 실행: dx_run_hook("dx_after_post_write", [...]) — 포인트 지급 등
10. 응답: dx_redirect(게시물 view URL)
6.2 렌더링 파이프라인 — ob_start 중첩
DX CMS의 렌더링은 ob_start()를 중첩 사용하여 스킨 출력을 버퍼에 포착하고, 이를 레이아웃에 $dx_content로 주입하는 방식으로 동작합니다.
// 1. index.php의 ob_start()가 전체 버퍼를 감쌈
// 2. handler.php의 _brd_render() 내부
ob_start();
extract($vars); // 스킨 변수 주입
require $skinFile; // 스킨 파일 실행
$dx_content = ob_get_clean(); // 스킨 출력 포착
// 3. 레이아웃에 주입
$layoutFile = DxTheme::getInstance()->resolve("layout/main.php");
extract($context);
require $layoutFile; // $dx_content 사용
// layout/main.php 예:
// <?php include header.php; ?>
// <main><?php echo $dx_content; ?></main>
// <?php include footer.php; ?>
6.3 페이지 렌더링 — 에러 격리
페이지 파일(pages/*.php)은 set_error_handler + try/catch로 격리되어 실행됩니다. 페이지 파일에서 오류가 발생해도 레이아웃(헤더/푸터)은 유지됩니다.
// renderPageWithLayout 내부 에러 격리 패턴
set_error_handler(function($errno, $errstr, $errfile, $errline) {
dx_log("[Page][{$errfile}:{$errline}] " . $errstr, "error");
return true; // PHP 기본 에러 핸들러 비활성화
});
ob_start();
try {
extract($context, EXTR_SKIP);
include $contentFile; // 페이지 파일 실행
} catch (Exception $e) {
echo "<div>페이지 오류: " . htmlspecialchars($e->getMessage()) . "</div>";
}
$dx_content = ob_get_clean();
restore_error_handler();
require $layoutFile; // 레이아웃은 정상 출력
6.4 API 요청 데이터 흐름
TYPE_API 라우트는 core/api/{action}.php로 디스패치됩니다. Content-Type: application/json 헤더가 자동 설정되며, 각 파일은 JSON 응답을 직접 출력합니다.
// 예: /api/post_like POST 요청 흐름
// 1. Dispatcher::dispatchApi() → core/api/post_like.php
// 2. CSRF 검증 (POST 요청)
// 3. 로그인 확인
// 4. $db->exists("post_likes", ["post_id"=>$id, "member_id"=>$uid])
// 5. INSERT 또는 DELETE (토글)
// 6. $db->value("SELECT COUNT(*) FROM dx_post_likes WHERE post_id=?", [$id])
// 7. echo json_encode(["success"=>true, "count"=>$cnt])
6.5 인증 요청 데이터 흐름 (로그인)
| 단계 | 위치 | 데이터 흐름 |
|---|---|---|
| 1. GET /auth/login | Dispatcher::dispatchAuth() | 로그인 폼 HTML 출력 (토큰 포함) |
| 2. POST 입력 수신 | core/auth/login.php | $_POST["login_id"], $_POST["password"] |
| 3. CSRF 검증 | dx_csrf_verify() | 세션 토큰 vs POST 토큰 비교 |
| 4. DB 조회 | Auth::login() | members WHERE login_id=? OR email=? |
| 5. 비밀번호 검증 | Auth::verifyPassword() | password_verify() bcrypt 검증 |
| 6. 세션 생성 | $_SESSION[$sessionKey] | ["id"=>$uid, "token"=>hash_hmac(...)] |
| 7. Remember Me | setcookie("dx_remember") | userId:랜덤토큰 (30일, httpOnly) |
| 8. 훅 실행 | dx_run_hook("dx_after_login") | 회원 모니터링, 포인트, 알림 등 |
| 9. 리다이렉트 | header("Location: ...") | redirect 파라미터 또는 홈으로 |
7. PHASE 6 — 후처리 + 응답
PHASE 6 후처리 + 응답 전송
extend/bottom/ • ob_end_flush()렌더링이 완료된 후 extend/bottom/ 스크립트가 실행되고, 최종 버퍼가 브라우저로 전송됩니다.
7.1 extend/bottom/ 실행
DxExtend::getInstance()->runBottom(array(
"elapsed" => round((microtime(true) - DX_START) * 1000, 2),
));
| 활용 사례 | 설명 |
|---|---|
| 성능 로그 | elapsed ms + DB 쿼리 수를 로그에 기록 |
| 캐시 저장 | 렌더링 완료된 HTML을 파일 캐시에 저장 |
| 통계 집계 | 페이지뷰 카운터 비동기 업데이트 |
| 정리 작업 | 임시 파일 삭제, 메모리 해제 |
| 모니터링 | 외부 APM 서비스에 요청 완료 신호 전송 |
7.2 최종 버퍼 flush
// ob_get_level() > 0 체크 — 이중 flush 방지
if (ob_get_level() > 0) {
ob_end_flush(); // 전체 HTML을 브라우저로 한 번에 전송
}
💡 ob_end_flush() 의미
index.php 최상단 ob_start()부터 쌓인 모든 출력 버퍼를 한 번에 전송합니다.
단일 TCP 패킷으로 전송되어 IIS•CGI 환경에서도 안정적으로 동작합니다.
이미 headers_sent 상태가 되어도 버퍼 내용은 정상 전송됩니다.
8. 주요 데이터 변환 지점
각 Phase의 경계에서 데이터가 어떻게 변환•전달되는지 정리합니다.
8.1 입력 데이터 정제 흐름
각 Phase의 경계에서 데이터가 어떻게 변환•전달되는지 정리합니다.
8.1 입력 데이터 정제 흐름
| 입력 소스 | 정제 방법 | 전달 대상 |
|---|---|---|
| URL 세그먼트 | preg_replace(/[^a-zA-Z0-9\-_]/,"") | Router::parseSegments → route 배열 |
| GET 파라미터 | htmlspecialchars / (int) 캐스팅 | handler.php 내 직접 사용 |
| POST 본문 | 본문 DxSanitizer::clean() — XSS 필터링 | DB INSERT/UPDATE 값 |
| 파일 업로드 | MIME 검증 + 확장자 화이트리스트 | uploads/ 디렉터리 저장 |
| BIGINT ID | ctype_digit() 검증 후 문자열 유지 | DB WHERE id=? 바인딩 |
| DB 조회값 | 바인딩 결과 — 이미 안전 | 스킨 파일 변수로 extract() |
8.2 전역 상태 흐름
$dx_config, $GLOBALS["dx_route"] 등 전역 변수가 Phase 간 데이터를 전달하는 중요한 매개체입니다.| 전역 변수 | 설정 위치 | 참조 위치 |
|---|---|---|
| $dx_config | config.php (DB settings) + DxSite::load() | 전체 파일 — dx_config()로 접근 |
| $GLOBALS["dx_route"] | Dispatcher::dispatch() | extend/middle/, handler.php, 테마 파일 |
| $GLOBALS["dx_board"] | Dispatcher::dispatchBoard() | boards/handler.php |
| $GLOBALS["dx_action"] | Dispatcher::dispatchBoard() | boards/handler.php |
| $_SESSION[$key] | Auth::login() + Auth::loadSession() | Auth::user(), dx_is_login() |
| DX_START_TIME | index.php define() | extend/bottom/, 성능 위젯 |
8.3 훅이 데이터 흐름에 개입하는 지점
| 훅 포인트 | Phase | 개입 내용 |
|---|---|---|
| dx_after_login | Phase 3 → Auth::login() | 포인트 지급, 회원 모니터링, 알림 발송 |
| dx_after_register | Phase 5 → Auth::register() | 가입 환영 이메일, 기본 포인트 지급 |
| dx_after_post_write | Phase 5 → handler.php | 포인트 지급, 알림, 검색 색인 |
| dx_editor_render | Phase 5 → 스킨 파일 | 활성 에디터 HTML 출력 (플러그인 교체) |
| dx_body_bottom | Phase 5 → 레이아웃 | 팝업, 소켓 스크립트, 통계 코드 삽입 |
| dx_top / dx_bottom | Phase 5 → 테마 파일 | 광고•위젯 삽입, 추가 메타태그 |
| dx_content_before_save | Phase 5 → handler.php | 본문 필터링, 금칙어 처리 (Filter 훅) |
9. 전체 데이터 흐름 요약
HTTP 요청 → HTML 응답까지 한 눈에 보는 전체 데이터 흐름도입니다.
HTTP 요청
│
▼ [PHASE 1: index.php 전처리]
│ ob_start() → URL 정규화 → 상수 정의 → 설치 확인
│
▼ [PHASE 2: 보안 초기화]
│ Secure.php 로드 → initSession() → startSession()
│ sendSecurityHeaders() → csrfToken() → secret_key 주입
│
▼ [PHASE 3: 서비스 초기화]
│ config.php(DB연결 + $dx_config 로드)
│ HookManager → PluginRegistry → load_plugins()
│ DxSite($dx_config 도메인별 오버라이드)
│ DxTheme(테마명 확정 + 폴백 체인)
│ Auth(세션/쿠키 → $this->user 로드)
│ DxContainer → extend/top/ 실행
│
▼ [PHASE 4: 라우팅]
│ routes/*.php 로드
│ DxRouter::dispatch() ─ 매칭성공 → 미들웨어 → Controller@action
│ └ 매칭실패 → Dispatcher → Router::resolve()
│ $GLOBALS["dx_route"] 확정
│ extend/middle/ 실행
│ 접근 권한 체크(read_level, write_level, access_level)
│
▼ [PHASE 5: 비즈니스 로직 + 렌더링]
│ handler.php(게시판) / pages/*.php / auth/*.php / api/*.php
│ 입력 정제(DxSanitizer) → CSRF 검증 → DB 조회/쓰기
│ 트랜잭션(begin/commit/rollback)
│ 훅 실행(dx_after_post_write 등)
│ ob_start() → 스킨 파일 → $dx_content → layout/main.php
│
▼ [PHASE 6: 후처리 + 응답]
│ extend/bottom/ 실행(성능 로그, 캐시 저장)
│ ob_end_flush() → HTML 전송
▼
브라우저 렌더링
⚠️ 데이터 흐름 설계 시 주의 사항
라우팅(Phase 4) 이전에는 $GLOBALS["dx_route"]가 설정되지 않습니다. extend/top/에서 라우트 정보를 사용하면 안 됩니다.
Auth(Phase 3)는 DB 연결 이후에만 초기화됩니다. config.php 로드 전에 Auth를 사용할 수 없습니다.
BIGINT ID는 (int) 캐스팅을 절대 사용하지 않습니다. 32bit PHP에서 오버플로우가 발생합니다.
ob_start()가 여러 겹으로 중첩될 수 있습니다. ob_end_flush() 전에 ob_get_level() > 0 체크가 필수입니다.