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(즉시 실행 함수)로 감싸서 전역 오염을 막는다. ajaxUrl과 nonce는 이 클로저 안에서만 접근 가능하다.
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>
스크리닝 탭 하단에도 별도 면책 문구가 있다. 이 두 군데는 최소한이다. 주식 관련 웹서비스를 운영한다면 "투자자문업이 아니다", "종목 추천이 아니다", "투자 책임은 본인에게 있다" 이 세 가지는 반드시 명시해야 한다.
성능 최적화 정리
이 대시보드에 적용한 성능 최적화 기법을 정리하면:
- 서버 캐시 (Transient API): API 응답 30분 캐싱. 같은 종목 반복 검색 시 API 호출 안 함.
- 2중 캐시: raw API 응답 캐시 + 분석 결과(HTML) 캐시. HTML 캐시 히트 시 지표 계산도 건너뜀.
- 날짜별 캐시: 스크리닝/시장 동향은
date('Ymd')키로 하루 단위 캐시. - 클라이언트 캐시 (localStorage): 마지막 검색 결과를 브라우저에 저장, 30분 이내 페이지 재방문 시 즉시 복원.
- Lazy loading: 탭을 클릭해야 데이터 로드 시작 (시장 동향, 스크리닝).
- CDN 동적 로딩: Lightweight Charts는 차트가 필요한 시점에만 로드.
- 서버 렌더링: HTML을 서버에서 완성해서 내려보냄. 클라이언트 DOM 조작 최소화.
- 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, 볼린저 밴드 등 기술적 지표 분석
숏코드로 어디든 넣을 수 있게 했다:주식 기술적 분석 대시보드
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, 반응형, 다크모드
워드프레스에 주식 대시보드를 추가하면 단순한 블로그 대비 페이지 체류 시간과 재방문율이 눈에 띄게 올라간다. "도구형 콘텐츠가 수익화에 유리하다"는 말은 직접 만들어보면 체감한다. 이 시리즈가 비슷한 프로젝트를 구상 중인 분에게 도움이 되었으면 한다.
이 시리즈의 다른 글
- 1편 — 공공데이터 API 연동
- 2편 — 기술적 지표 계산 (MA, RSI, 볼린저 밴드)
- 3편 — 종목 스크리닝 시스템 구현
- 4편 — Lightweight Charts로 캔들스틱 차트 구현
- [현재 글] 5편 — AJAX 연동과 프론트엔드 완성