블로그워드프레스에서 주식 기술적 분석 대시보드 만들기 (5) — AJAX 연동과 프론트엔드 완성

워드프레스에서 주식 기술적 분석 대시보드 만들기 (5) — AJAX 연동과 프론트엔드 완성

4편까지 백엔드(API, 지표 계산, 스크리닝)와 차트를 만들었다. 마지막 편에서는 이 모든 것을 하나로 엮는 프론트엔드를 완성한다. 워드프레스 AJAX 구조, nonce 보안, 탭 전환, 검색 UX, localStorage 캐싱, 반응형 디자인, 다크모드까지 — 사용자가 실제로 만지는 부분 전부다.

WordPress AJAX의 작동 원리

워드프레스에서 AJAX를 쓰려면 정해진 규칙이 있다. 모든 AJAX 요청은 admin-ajax.php로 보내고, action 파라미터로 어떤 핸들러를 호출할지 지정한다.

서버 측 등록:

// 로그인 사용자 대상
add_action( 'wp_ajax_nalkkul_sa_search', 'nalkkul_sa_ajax_search' );
// 비로그인 사용자도 대상 (공개 기능이니까)
add_action( 'wp_ajax_nopriv_nalkkul_sa_search', 'nalkkul_sa_ajax_search' );

// 시장 동향 탭
add_action( 'wp_ajax_nalkkul_sa_market', 'nalkkul_sa_ajax_market' );
add_action( 'wp_ajax_nopriv_nalkkul_sa_market', 'nalkkul_sa_ajax_market' );

// 스크리닝 탭
add_action( 'wp_ajax_nalkkul_sa_screening', 'nalkkul_sa_ajax_screening' );
add_action( 'wp_ajax_nopriv_nalkkul_sa_screening', 'nalkkul_sa_ajax_screening' );

// 차트 데이터
add_action( 'wp_ajax_nalkkul_sa_chart_data', 'nalkkul_sa_ajax_chart_data' );
add_action( 'wp_ajax_nopriv_nalkkul_sa_chart_data', 'nalkkul_sa_ajax_chart_data' );

// AI 분석
add_action( 'wp_ajax_nalkkul_sa_ai', 'nalkkul_sa_ajax_ai' );
add_action( 'wp_ajax_nopriv_nalkkul_sa_ai', 'nalkkul_sa_ajax_ai' );

wp_ajax_wp_ajax_nopriv_ 두 개를 모두 등록해야 한다. nopriv가 빠지면 로그인하지 않은 방문자는 AJAX가 작동하지 않는다. 주식 대시보드는 공개 기능이므로 반드시 둘 다 등록한다.

클라이언트 측에서 URL과 nonce를 주입하는 부분:

// Shortcode 함수 안에서
$nonce    = wp_create_nonce( 'nalkkul_sa_nonce' );
$ajax_url = admin_url( 'admin-ajax.php' );
?>
<script>
(function(){
    var ajaxUrl = <?php echo wp_json_encode( $ajax_url ); ?>;
    var nonce   = <?php echo wp_json_encode( $nonce ); ?>;
    // ... 이 스코프 안에서 모든 AJAX 호출에 ajaxUrl, nonce 사용
})();
</script>

IIFE(즉시 실행 함수)로 감싸서 전역 오염을 막는다. ajaxUrlnonce는 이 클로저 안에서만 접근 가능하다.

Nonce 보안 — CSRF 방어

Nonce(Number used ONCE)는 워드프레스의 CSRF 방어 메커니즘이다. 서버에서 생성한 토큰을 클라이언트에 주고, 클라이언트가 AJAX 요청할 때 이 토큰을 같이 보낸다. 서버에서 토큰을 검증해서 “내가 렌더링한 페이지에서 보낸 요청이 맞는지” 확인한다.

생성:

$nonce = wp_create_nonce( 'nalkkul_sa_nonce' );

검증 (모든 AJAX 핸들러 첫 줄):

if ( ! check_ajax_referer( 'nalkkul_sa_nonce', 'nonce', false ) ) {
    wp_send_json_error( array( 'message' => '보안 검증에 실패했습니다. 페이지를 새로고침해주세요.' ), 403 );
}

세 번째 인자 false는 “실패 시 die()하지 말고 false를 반환하라”는 뜻이다. 직접 에러 응답을 보내야 하니까 die() 대신 우리가 메시지를 컨트롤한다.

클라이언트에서 nonce를 보내는 방법:

var fd = new FormData();
fd.append('action', 'nalkkul_sa_search');
fd.append('nonce', nonce);     // ← 여기
fd.append('itms_nm', val);

fetch(ajaxUrl, {method:'POST', body:fd, credentials:'same-origin'})
    .then(function(r){ return r.json(); })
    .then(function(res){
        // ...
    });

credentials: 'same-origin'은 쿠키를 같이 보내라는 뜻이다. 워드프레스 nonce 검증이 세션 쿠키에 의존하므로 반드시 필요하다.

4개 탭 전환 UI

대시보드는 4개 탭으로 구성된다: 기술적 분석, 시장 동향, 공시/뉴스, 스크리닝. 탭 전환 로직은 단순하다:

window.tgSaTab = function(tab) {
    // 모든 탭 버튼에서 active 제거
    document.querySelectorAll('.tg-sa-tab').forEach(function(t){
        t.classList.remove('active');
    });
    // 모든 탭 콘텐츠 숨김
    document.querySelectorAll('.tg-sa-tab-content').forEach(function(c){
        c.classList.remove('active');
    });

    // 선택한 탭만 활성화
    document.querySelector('.tg-sa-tab[data-tab="'+tab+'"]').classList.add('active');
    document.getElementById('tg-sa-tab-'+tab).classList.add('active');

    // 검색창은 기술적 분석 탭에서만 보이기
    var searchArea = document.getElementById('tg-sa-search-area');
    if (searchArea) {
        searchArea.style.display = tab === 'analysis' ? '' : 'none';
    }

    // Lazy loading: 해당 탭 데이터를 처음 열 때만 로드
    if (tab === 'market' && !marketLoaded) {
        loadMarket();
    }
    if (tab === 'screening' && !screeningLoaded) {
        loadScreening();
    }
};

핵심은 lazy loading이다. 시장 동향 탭과 스크리닝 탭은 API 호출이 필요한데, 사용자가 안 볼 수도 있는 탭을 페이지 로드 시점에 미리 호출하면 낭비다. marketLoaded, screeningLoaded 플래그로 첫 클릭 때만 데이터를 가져오고, 두 번째부터는 이미 렌더링된 결과를 그대로 보여준다.

검색 기능 — 인기 종목 태그

검색창 아래에 인기 종목 태그(삼성전자, SK하이닉스, 네이버 등)를 배치했다. 태그를 클릭하면 해당 종목명이 입력창에 들어가고 바로 검색된다:

<div class="tg-sa-tags">
    <a class="tg-sa-tag" onclick="tgSaQuick('삼성전자')">삼성전자</a>
    <a class="tg-sa-tag" onclick="tgSaQuick('SK하이닉스')">SK하이닉스</a>
    <a class="tg-sa-tag" onclick="tgSaQuick('네이버')">네이버</a>
    <a class="tg-sa-tag" onclick="tgSaQuick('카카오')">카카오</a>
    <a class="tg-sa-tag" onclick="tgSaQuick('현대차')">현대차</a>
    <a class="tg-sa-tag" onclick="tgSaQuick('LG에너지솔루션')">LG에너지솔루션</a>
</div>
window.tgSaQuick = function(name) {
    var inp = document.getElementById('tg-sa-input');
    if (inp) { inp.value = name; }
    tgSaSearch();
};

이 태그들이 있으면 사용자가 뭘 검색해야 할지 모를 때 바로 클릭할 수 있다. 처음 방문한 사용자의 “빈 화면” 문제를 해결하는 UX 패턴이다.

종목명 검색의 삽질: 한글 vs 영문

data.go.kr API에서 종목명(itmsNm)으로 검색할 때, “네이버”로 검색하면 결과가 안 나오고 “NAVER”로 해야 나오는 경우가 있다. 그래서 AJAX 핸들러에 3단계 검색 폴백을 넣었다:

// 1차: 입력 그대로 검색
$result = nalkkul_sa_fetch_stock( array( 'itmsNm' => $itms_nm, 'numOfRows' => 60 ) );

// 2차: 결과 없으면 매핑 테이블로 재시도
$name_map = array(
    '네이버' => 'NAVER',
    '포스코홀딩스' => 'POSCO홀딩스',
    '현대자동차' => '현대차',
    // ...
);
if ( empty( $result['items'] ) && isset( $name_map[ $itms_nm ] ) ) {
    $result = nalkkul_sa_fetch_stock( array( 'itmsNm' => $name_map[ $itms_nm ] ) );
}

// 3차: 대문자로 재시도
if ( empty( $result['items'] ) ) {
    $upper = mb_strtoupper( $itms_nm );
    if ( $upper !== $itms_nm ) {
        $result = nalkkul_sa_fetch_stock( array( 'itmsNm' => $upper ) );
    }
}

이래도 못 찾으면 “검색 결과가 없습니다. 영문 종목명(예: NAVER)으로도 시도해보세요”라는 메시지를 보여준다. 완벽하지는 않지만, 주요 종목은 대부분 커버된다.

검색 결과 캐싱 — localStorage

사용자가 “삼성전자”를 검색한 뒤 다른 페이지를 갔다가 돌아오면? 서버 캐시(트랜지언트)가 있긴 하지만, AJAX 요청 자체를 안 보내면 더 빠르다. localStorage에 마지막 검색 결과를 저장해서, 페이지 로드 시 즉시 복원한다:

// 검색 성공 후 저장
try {
    localStorage.setItem('tg_sa_last', JSON.stringify({
        name: val,
        stock_name: res.data.stock_name,
        stock_code: res.data.stock_code,
        html: res.data.html,
        indicators: res.data.indicators,
        time: Date.now()
    }));
} catch(e) {}

// 페이지 로드 시 복원
try {
    var last = JSON.parse(localStorage.getItem('tg_sa_last'));
    if (last && last.html && last.name) {
        // 30분(1800초) 이내 데이터만 복원
        if (Date.now() - last.time < 1800000) {
            if (input) input.value = last.name;
            document.getElementById('tg-sa-results').innerHTML = last.html;
            window._saStockName = last.stock_name || '';
            window._saStockCode = last.stock_code || '';
            window._saIndicators = last.indicators || {};
            document.getElementById('tg-sa-ai-wrap').style.display = 'block';

            // Lightweight Charts도 복원
            if (last.stock_name) {
                var fd_restore = new FormData();
                fd_restore.append('action', 'nalkkul_sa_chart_data');
                fd_restore.append('nonce', nonce);
                fd_restore.append('code', last.stock_name);
                fetch(ajaxUrl, {method:'POST', body:fd_restore, credentials:'same-origin'})
                    .then(function(r){ return r.json(); })
                    .then(function(res){
                        if (res.success) { renderLWChart(res.data, 'tab1'); }
                    });
            }
        }
    }
} catch(e) {}

30분 제한을 건 이유: 주식 데이터는 시간이 지나면 stale해진다. 서버 캐시 TTL과 맞춰서 30분 이내 데이터만 복원한다. 30분이 지났으면 무시하고 새로 검색하게 한다.

HTML을 통째로 localStorage에 넣는 게 좀 과하다 싶지만, 실제로 압축 없이도 10~30KB 수준이라 문제없다. localStorage 용량은 보통 5~10MB다.

try/catch로 감싼 이유: 시크릿 모드나 저장 공간 부족 시 localStorage가 에러를 던질 수 있다. 캐시는 있으면 좋고 없어도 되는 것이므로, 에러가 나면 조용히 무시한다.

반응형 디자인

모바일에서도 쓸 수 있어야 한다. CSS 미디어 쿼리로 640px 이하에서 레이아웃을 조정한다:

@media(max-width:640px){
    .tg-sa-hero{padding:36px 20px 32px;border-radius:16px}
    .tg-sa-hero h2{font-size:22px}
    .tg-sa-search-box{flex-direction:column}  /* 검색창 세로 배치 */
    .tg-sa-search-box button{padding:12px}
    .tg-sa-tabs{gap:6px}
    .tg-sa-tab{padding:8px 16px;font-size:13px}

    /* 가격 정보 그리드: 3열 → 2열 */
    .tg-sa-info-grid{grid-template-columns:repeat(2,1fr)}

    /* MA, 거래량, 볼린저: 3열 → 1열 */
    .tg-sa-ma-grid{grid-template-columns:1fr}
    .tg-sa-vol-grid{grid-template-columns:1fr}
    .tg-sa-bb-grid{grid-template-columns:1fr}

    /* 스크리닝 카드: 2열 */
    .tg-sa-scr-cards{grid-template-columns:repeat(2,1fr);padding:12px;gap:10px}

    /* 박스권 카드: 1열 */
    .tg-sa-box-cards{grid-template-columns:1fr;padding:0 12px 12px}
}

핵심 전략은 grid-template-columns를 줄이는 것이다. 데스크톱에서 3열이었던 것을 모바일에서 1~2열로 바꾼다. 테이블도 overflow-x:auto로 감싸서 가로 스크롤이 가능하게 했다.

검색창은 데스크톱에서 가로 배치(입력 + 버튼)였던 것을 모바일에서 flex-direction:column으로 세로 배치한다. 터치 영역이 커져서 누르기 편해진다.

다크모드 CSS

다크모드는 body.dark-mode 선택자로 모든 스타일을 오버라이드한다. 양이 꽤 많지만 패턴은 같다:

/* 카드 배경 */
body.dark-mode .tg-sa-card{background:#1f2937;border-color:#374151;box-shadow:0 2px 12px rgba(0,0,0,.2)}
body.dark-mode .tg-sa-card-title{color:#e4e4e7;border-bottom-color:#374151}

/* 텍스트 */
body.dark-mode .tg-sa-stock-name,
body.dark-mode .tg-sa-td-name,
body.dark-mode .tg-sa-td-price{color:#e4e4e7}

/* 스크리닝 강세 카드 */
body.dark-mode .tg-sa-scr-card-bull{
    background:linear-gradient(135deg,#052e16,#14532d);
    border-color:#166534;
}

/* 스크리닝 약세 카드 */
body.dark-mode .tg-sa-scr-card-bear{
    background:linear-gradient(135deg,#431407,#7c2d12);
    border-color:#9a3412;
}

/* 박스권 카드 */
body.dark-mode .tg-sa-box-card{
    background:linear-gradient(135deg,#2d1b4e,#1e1338);
    border-color:#6b21a8;
}

/* 면책 문구 */
body.dark-mode .tg-sa-disclaimer{
    background:linear-gradient(135deg,#422006,#78350f);
    border-color:#92400e;
    color:#fde68a;
}

색상 선택 원칙: Tailwind CSS의 다크 팔레트를 참고했다. #1f2937(gray-800)이 카드 배경, #374151(gray-700)이 보더, #e4e4e7(zinc-200)이 기본 텍스트. 완전한 흰색이나 검정은 피하고, 약간의 색감을 넣어서 눈 피로를 줄인다.

면책 문구 처리

대시보드 상단에 눈에 잘 띄는 노란 배너로 면책 문구를 넣었다:

<div class="tg-sa-disclaimer">
    <span class="tg-sa-disclaimer-icon">⚠️</span>
    <div>본 정보는 투자 참고용이며, 투자 판단은 본인의 책임입니다. 
    본 사이트는 투자자문업을 영위하지 않습니다.</div>
</div>

스크리닝 탭 하단에도 별도 면책 문구가 있다. 이 두 군데는 최소한이다. 주식 관련 웹서비스를 운영한다면 "투자자문업이 아니다", "종목 추천이 아니다", "투자 책임은 본인에게 있다" 이 세 가지는 반드시 명시해야 한다.

성능 최적화 정리

이 대시보드에 적용한 성능 최적화 기법을 정리하면:

  1. 서버 캐시 (Transient API): API 응답 30분 캐싱. 같은 종목 반복 검색 시 API 호출 안 함.
  2. 2중 캐시: raw API 응답 캐시 + 분석 결과(HTML) 캐시. HTML 캐시 히트 시 지표 계산도 건너뜀.
  3. 날짜별 캐시: 스크리닝/시장 동향은 date('Ymd') 키로 하루 단위 캐시.
  4. 클라이언트 캐시 (localStorage): 마지막 검색 결과를 브라우저에 저장, 30분 이내 페이지 재방문 시 즉시 복원.
  5. Lazy loading: 탭을 클릭해야 데이터 로드 시작 (시장 동향, 스크리닝).
  6. CDN 동적 로딩: Lightweight Charts는 차트가 필요한 시점에만 로드.
  7. 서버 렌더링: HTML을 서버에서 완성해서 내려보냄. 클라이언트 DOM 조작 최소화.
  8. Rate limiting: 악의적 대량 호출 방지로 API 쿼터 보호.

전체 아키텍처 요약

5편에 걸쳐 만든 대시보드의 전체 구조를 텍스트 다이어그램으로 정리한다:

┌─────────────────────────────────────────────────────────┐
│  사용자 브라우저 (프론트엔드)                              │
│                                                         │
│  ┌───────────────────────────────────────────────────┐  │
│  │      

    
⚠️
본 정보는 투자 참고용이며, 투자 판단은 본인의 책임입니다. 본 사이트는 투자자문업을 영위하지 않습니다.
📊

주식 기술적 분석 대시보드

캔들스틱 차트, 이동평균선, RSI, 볼린저 밴드 등 기술적 지표 분석

60일 기술적 분석 데이터를 계산하고 있습니다...
🔍
시장 동향을 불러오고 있습니다...
📊
종목 스크리닝 데이터를 분석하고 있습니다...
📊
Shortcode │ │ │ │ │ │ │ │ ┌─────────┐ ┌─────────┐ ┌────────┐ ┌─────────┐ │ │ │ │ │기술분석 │ │시장동향 │ │공시/뉴스│ │스크리닝 │ │ │ │ │ │ 탭1 │ │ 탭2 │ │ 탭3 │ │ 탭4 │ │ │ │ │ └────┬────┘ └────┬────┘ └────────┘ └────┬────┘ │ │ │ │ │ │ (static) │ │ │ │ │ AJAX+nonce AJAX+nonce AJAX+nonce │ │ │ └───────┼───────────┼───────────────────────┼───────┘ │ │ │ │ │ │ │ ┌───────▼───────────▼───────────────────────▼───────┐ │ │ │ localStorage Cache │ │ │ │ (마지막 검색 결과 30분 캐시) │ │ │ └───────────────────────────────────────────────────┘ │ └─────────────────────────┬───────────────────────────────┘ │ fetch() + FormData ▼ ┌─────────────────────────────────────────────────────────┐ │ WordPress 서버 (백엔드) │ │ │ │ admin-ajax.php │ │ │ │ │ ┌────▼────────────────────────────────────────────┐ │ │ │ AJAX Handlers (nonce 검증 → rate limit 체크) │ │ │ │ │ │ │ │ nalkkul_sa_ajax_search() → 종목 검색+분석 │ │ │ │ nalkkul_sa_ajax_market() → 시장 동향 │ │ │ │ nalkkul_sa_ajax_screening()→ 종목 스크리닝 │ │ │ │ nalkkul_sa_ajax_chart_data()→ 차트 데이터 │ │ │ │ nalkkul_sa_ajax_ai() → AI 분석 │ │ │ └────────────────────┬─────────────────────────────┘ │ │ │ │ │ ┌────────────────────▼─────────────────────────────┐ │ │ │ Transient Cache (wp_options 테이블) │ │ │ │ - nalkkul_sa_{md5(params)} : API raw 응답 30분 │ │ │ │ - nalkkul_sa_search_{md5} : 분석 결과 HTML 30분 │ │ │ │ - nalkkul_sa_screening_{date} : 스크리닝 30분 │ │ │ │ - nalkkul_sa_rl_{md5(ip)} : rate limit 1시간 │ │ │ └────────────────────┬─────────────────────────────┘ │ │ │ cache miss시 │ │ ┌────────────────────▼─────────────────────────────┐ │ │ │ Technical Analysis Engine │ │ │ │ - nalkkul_sa_calc_ma() 이동평균선 │ │ │ │ - nalkkul_sa_detect_cross() 골든/데드크로스 │ │ │ │ - nalkkul_sa_calc_rsi() RSI 14일 │ │ │ │ - nalkkul_sa_calc_bollinger() 볼린저 밴드 │ │ │ │ - nalkkul_sa_score_stock() 종목 점수화 │ │ │ │ - nalkkul_sa_get_signals() 신호 태그 생성 │ │ │ └──────────────────────────────────────────────────┘ │ └─────────────────────────┬───────────────────────────────┘ │ wp_remote_get() ▼ ┌─────────────────────────────────────────────────────────┐ │ data.go.kr (공공데이터 포털) │ │ │ │ 금융위원회_주식시세정보 API │ │ GET /1160100/service/GetStockSecuritiesInfoService │ │ /getStockPriceInfo │ │ Params: serviceKey, itmsNm, basDt, numOfRows, pageNo │ │ Response: JSON (basDt, srtnCd, itmsNm, clpr, mkp, │ │ hipr, lopr, trqu, vs, fltRt, mrktTotAmt ...) │ └─────────────────────────────────────────────────────────┘

테이블 정렬 JavaScript

스크리닝 테이블의 컬럼 헤더를 클릭하면 정렬되는 기능도 빼먹을 수 없다:

function initScrTableSort() {
    var table = document.getElementById('tg-sa-scr-table');
    if (!table) return;
    var headers = table.querySelectorAll('th.tg-sa-sortable');
    var currentSort = 'score';
    var currentDir = 'desc';

    headers.forEach(function(th) {
        th.addEventListener('click', function() {
            var sortKey = th.getAttribute('data-sort');

            // 같은 컬럼 다시 클릭 → 정렬 방향 토글
            if (currentSort === sortKey) {
                currentDir = currentDir === 'desc' ? 'asc' : 'desc';
            } else {
                currentSort = sortKey;
                currentDir = 'desc';
            }

            // 활성 표시 업데이트
            headers.forEach(function(h) {
                h.classList.remove('tg-sa-sort-active');
                h.textContent = h.textContent.replace(/ [▼▲]$/, '');
            });
            th.classList.add('tg-sa-sort-active');
            th.textContent += currentDir === 'desc' ? ' ▼' : ' ▲';

            // 실제 정렬
            var tbody = table.querySelector('tbody');
            var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr'));

            rows.sort(function(a, b) {
                if (sortKey === 'name') {
                    var aVal = a.getAttribute('data-name');
                    var bVal = b.getAttribute('data-name');
                    return currentDir === 'desc'
                        ? bVal.localeCompare(aVal, 'ko')
                        : aVal.localeCompare(bVal, 'ko');
                }
                var aNum = parseFloat(a.getAttribute('data-' + sortKey)) || 0;
                var bNum = parseFloat(b.getAttribute('data-' + sortKey)) || 0;
                return currentDir === 'desc' ? bNum - aNum : aNum - bNum;
            });

            rows.forEach(function(row, idx) {
                row.querySelector('td:first-child').textContent = idx + 1;
                tbody.appendChild(row);
            });
        });
    });
}

한글 종목명 정렬에 localeCompare(aVal, 'ko')를 쓰는 게 포인트다. 단순 문자열 비교가 아니라 한국어 정렬 규칙을 따른다. tbody.appendChild(row)로 이미 존재하는 row를 다시 append하면 DOM에서 순서가 바뀐다 — 새로 만드는 게 아니라 이동하는 것이다.

Shortcode 등록과 인라인 스타일/스크립트

대시보드 전체를

⚠️
본 정보는 투자 참고용이며, 투자 판단은 본인의 책임입니다. 본 사이트는 투자자문업을 영위하지 않습니다.
📊

주식 기술적 분석 대시보드

캔들스틱 차트, 이동평균선, RSI, 볼린저 밴드 등 기술적 지표 분석

60일 기술적 분석 데이터를 계산하고 있습니다...
🔍
시장 동향을 불러오고 있습니다...
📊
종목 스크리닝 데이터를 분석하고 있습니다...
📊
숏코드로 어디든 넣을 수 있게 했다:

add_shortcode( 'nalkkul_stock_analysis', function() {
    ob_start();

    $nonce    = wp_create_nonce( 'nalkkul_sa_nonce' );
    $ajax_url = admin_url( 'admin-ajax.php' );
    $has_key  = defined( 'NALKKUL_DATA_GO_KR_KEY' ) && ! empty( NALKKUL_DATA_GO_KR_KEY );
    ?>
    <style>
    /* 2000줄 가량의 인라인 CSS */
    </style>
    
    <div class="tg-sa-wrap">
        <!-- 면책 문구 -->
        <!-- Hero + 탭 + 검색 -->
        <!-- 탭별 콘텐츠 영역 -->
        <!-- 차트 모달 -->
    </div>

    <script>
    (function(){
        // 400줄 가량의 JavaScript
    })();
    </script>
    <?php
    return ob_get_clean();
});

CSS와 JS를 별도 파일로 분리하지 않고 인라인으로 넣은 이유: mu-plugin에서 wp_enqueue_style/script를 쓰려면 URL 경로를 별도로 계산해야 하고, 파일도 따로 관리해야 한다. 단일 파일 구조를 유지하면 배포가 간단하다 — 파일 하나만 서버에 올리면 끝.

성능 측면에서, 인라인 CSS/JS는 추가 HTTP 요청이 없다는 장점이 있다. 물론 캐싱이 안 되는 단점도 있지만, 이 대시보드가 사용되는 페이지는 보통 1~2개뿐이라 별 문제가 안 된다.

시리즈 마무리: 2,900줄의 단일 파일

최종 결과물인 stock-analysis.php는 약 2,900줄이다. PHP 로직, CSS, HTML 템플릿, JavaScript가 전부 한 파일에 들어있다. 규모가 커지면 분리해야겠지만, 현재 수준에서는 "파일 하나만 관리하면 되는" 편리함이 더 크다.

전체 시리즈에서 다룬 내용 요약:

  • 1편: 금융위원회 API 연동, rawurlencode, 캐싱, rate limiting
  • 2편: MA(5/20/60일), RSI(14일), 볼린저 밴드 PHP 구현
  • 3편: 종목 스코어링(0~100점), 신호 태그, 박스권 탐지
  • 4편: Lightweight Charts 캔들스틱+거래량+MA 오버레이
  • 5편: AJAX 구조, nonce, 탭 UI, localStorage, 반응형, 다크모드

워드프레스에 주식 대시보드를 추가하면 단순한 블로그 대비 페이지 체류 시간과 재방문율이 눈에 띄게 올라간다. "도구형 콘텐츠가 수익화에 유리하다"는 말은 직접 만들어보면 체감한다. 이 시리즈가 비슷한 프로젝트를 구상 중인 분에게 도움이 되었으면 한다.


이 시리즈의 다른 글

이 글이 도움이 되셨다면 공유해주세요!

댓글 남기기

무엇이든 물어보세요! 💬