커스텀 위젯¶
레이아웃에서 사용할 수 있는 커스텀 위젯을 개발하는 방법을 학습합니다.
위젯 개발 기초¶
위젯 구조¶
widgets/
└── my_widget/
├── conf/
│ └── info.xml # 위젯 정보
├── my_widget.class.php # 위젯 클래스
├── tpl/
│ ├── list.html # 기본 템플릿
│ ├── gallery.html # 갤러리 템플릿
│ └── config.html # 설정 템플릿
├── css/
│ └── widget.css # 스타일시트
├── js/
│ └── widget.js # JavaScript
└── lang/
├── ko.lang.php # 한국어 언어팩
└── en.lang.php # 영어 언어팩
위젯 정보 파일 (info.xml)¶
<?xml version="1.0" encoding="UTF-8"?>
<widget version="0.2">
<!-- 기본 정보 -->
<title xml:lang="ko">최신글 위젯</title>
<title xml:lang="en">Latest Posts Widget</title>
<description xml:lang="ko">게시판의 최신글을 표시하는 위젯입니다.</description>
<description xml:lang="en">Widget to display latest posts from boards.</description>
<!-- 버전 및 날짜 -->
<version>1.2.0</version>
<date>2024-01-01</date>
<!-- 제작자 정보 -->
<author email_address="developer@example.com" link="https://example.com">
<name xml:lang="ko">개발자</name>
<name xml:lang="en">Developer</name>
</author>
<!-- 설정 변수 -->
<extra_vars>
<!-- 게시판 선택 -->
<var name="mid_list" type="mid_list">
<title xml:lang="ko">대상 게시판</title>
<title xml:lang="en">Target Boards</title>
<description xml:lang="ko">글을 가져올 게시판을 선택하세요</description>
</var>
<!-- 출력 개수 -->
<var name="list_count" type="text" default="5">
<title xml:lang="ko">출력 개수</title>
<title xml:lang="en">Number of Items</title>
<description xml:lang="ko">출력할 글의 개수</description>
</var>
<!-- 제목 길이 -->
<var name="title_cut_size" type="text" default="40">
<title xml:lang="ko">제목 길이</title>
<title xml:lang="en">Title Length</title>
<description xml:lang="ko">제목 표시 길이 (0은 전체)</description>
</var>
<!-- 썸네일 사용 -->
<var name="use_thumbnail" type="select" default="Y">
<title xml:lang="ko">썸네일 사용</title>
<title xml:lang="en">Use Thumbnail</title>
<options value="Y">사용</options>
<options value="N">사용안함</options>
</var>
<!-- 썸네일 크기 -->
<var name="thumbnail_width" type="text" default="80">
<title xml:lang="ko">썸네일 너비</title>
<title xml:lang="en">Thumbnail Width</title>
<description xml:lang="ko">썸네일 이미지 너비 (px)</description>
</var>
<var name="thumbnail_height" type="text" default="80">
<title xml:lang="ko">썸네일 높이</title>
<title xml:lang="en">Thumbnail Height</title>
<description xml:lang="ko">썸네일 이미지 높이 (px)</description>
</var>
<!-- 날짜 표시 -->
<var name="show_date" type="select" default="Y">
<title xml:lang="ko">날짜 표시</title>
<title xml:lang="en">Show Date</title>
<options value="Y">표시</options>
<options value="N">숨김</options>
</var>
<!-- 닉네임 표시 -->
<var name="show_nickname" type="select" default="N">
<title xml:lang="ko">닉네임 표시</title>
<title xml:lang="en">Show Nickname</title>
<options value="Y">표시</options>
<options value="N">숨김</options>
</var>
<!-- 댓글수 표시 -->
<var name="show_comment_count" type="select" default="Y">
<title xml:lang="ko">댓글수 표시</title>
<title xml:lang="en">Show Comment Count</title>
<options value="Y">표시</options>
<options value="N">숨김</options>
</var>
<!-- 템플릿 선택 -->
<var name="template" type="select" default="list">
<title xml:lang="ko">템플릿</title>
<title xml:lang="en">Template</title>
<options value="list">목록형</options>
<options value="gallery">갤러리형</options>
<options value="card">카드형</options>
</var>
<!-- 캐시 시간 -->
<var name="cache_time" type="text" default="300">
<title xml:lang="ko">캐시 시간 (초)</title>
<title xml:lang="en">Cache Time (seconds)</title>
<description xml:lang="ko">0으로 설정하면 캐시를 사용하지 않습니다</description>
</var>
</extra_vars>
</widget>
위젯 클래스 구현¶
기본 위젯 클래스¶
<?php
/**
* 최신글 위젯
* @author Developer
* @version 1.2.0
*/
class my_widget extends WidgetHandler
{
/**
* 위젯 실행
*/
function proc($args)
{
// 설정값 검증 및 기본값 설정
$args = $this->validateArgs($args);
// 캐시 확인
if($args->cache_time > 0) {
$cache_key = $this->getCacheKey($args);
$cache_data = $this->getCache($cache_key);
if($cache_data) {
Context::set('widget_cache', true);
Context::set('document_list', $cache_data);
return $this->compileTemplate($args);
}
}
// 게시물 데이터 조회
$document_list = $this->getDocumentList($args);
// 썸네일 처리
if($args->use_thumbnail === 'Y') {
$document_list = $this->processThumbnails($document_list, $args);
}
// 캐시 저장
if($args->cache_time > 0) {
$this->setCache($cache_key, $document_list, $args->cache_time);
}
// 템플릿 변수 설정
Context::set('document_list', $document_list);
Context::set('widget_info', $args);
// CSS/JS 파일 추가
$this->addAssets($args);
// 템플릿 컴파일
return $this->compileTemplate($args);
}
/**
* 설정값 검증 및 기본값 설정
*/
private function validateArgs($args)
{
// 필수 설정값 확인
if(!$args->mid_list) {
$args->mid_list = array();
} elseif(!is_array($args->mid_list)) {
$args->mid_list = explode(',', $args->mid_list);
}
// 숫자형 설정값 검증
$args->list_count = max(1, min(50, (int)$args->list_count));
$args->title_cut_size = max(0, (int)$args->title_cut_size);
$args->thumbnail_width = max(10, min(500, (int)$args->thumbnail_width));
$args->thumbnail_height = max(10, min(500, (int)$args->thumbnail_height));
$args->cache_time = max(0, (int)$args->cache_time);
// Y/N 설정값 검증
$yn_options = array('use_thumbnail', 'show_date', 'show_nickname', 'show_comment_count');
foreach($yn_options as $option) {
if(!in_array($args->{$option}, array('Y', 'N'))) {
$args->{$option} = 'Y';
}
}
// 템플릿 검증
$allowed_templates = array('list', 'gallery', 'card');
if(!in_array($args->template, $allowed_templates)) {
$args->template = 'list';
}
return $args;
}
/**
* 게시물 목록 조회
*/
private function getDocumentList($args)
{
if(empty($args->mid_list)) {
return array();
}
// 모듈 일련번호 조회
$module_srls = array();
$oModuleModel = getModel('module');
foreach($args->mid_list as $mid) {
$module_info = $oModuleModel->getModuleInfoByMid($mid);
if($module_info && $module_info->module === 'board') {
$module_srls[] = $module_info->module_srl;
}
}
if(empty($module_srls)) {
return array();
}
// 문서 조회 조건 설정
$obj = new stdClass();
$obj->module_srl = implode(',', $module_srls);
$obj->list_count = $args->list_count;
$obj->sort_index = 'list_order';
$obj->order_type = 'asc';
$obj->statusList = array('PUBLIC');
// 공지사항 제외
$obj->exclude_notice = 'Y';
// 문서 조회
$oDocumentModel = getModel('document');
$output = $oDocumentModel->getDocumentList($obj);
if(!$output->toBool() || !$output->data) {
return array();
}
return $output->data;
}
/**
* 썸네일 처리
*/
private function processThumbnails($document_list, $args)
{
foreach($document_list as $document) {
$thumbnail = $this->generateThumbnail($document, $args->thumbnail_width, $args->thumbnail_height);
$document->thumbnail = $thumbnail;
}
return $document_list;
}
/**
* 썸네일 생성
*/
private function generateThumbnail($document, $width, $height)
{
// 첨부파일에서 이미지 찾기
$files = $document->getUploadedFiles();
if($files) {
foreach($files as $file) {
if($file->isImage()) {
return FileHandler::getThumbnail($file->uploaded_filename, $width, $height);
}
}
}
// 본문에서 이미지 추출
$content = $document->get('content');
if(preg_match('/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $content, $matches)) {
$image_url = $matches[1];
// 상대 경로를 절대 경로로 변환
if(strpos($image_url, 'http') !== 0) {
$image_url = Context::getRequestUri() . ltrim($image_url, '/');
}
return FileHandler::getThumbnail($image_url, $width, $height);
}
// 기본 이미지 반환
return $this->widget_path . 'images/no-image.png';
}
/**
* 캐시 키 생성
*/
private function getCacheKey($args)
{
$key_data = array(
'mid_list' => $args->mid_list,
'list_count' => $args->list_count,
'template' => $args->template,
'timestamp' => floor(time() / $args->cache_time)
);
return 'widget_my_widget_' . md5(serialize($key_data));
}
/**
* 캐시 조회
*/
private function getCache($cache_key)
{
$cache_file = _XE_PATH_ . 'files/cache/widgets/' . $cache_key . '.cache';
if(file_exists($cache_file)) {
$cache_data = unserialize(file_get_contents($cache_file));
return $cache_data;
}
return null;
}
/**
* 캐시 저장
*/
private function setCache($cache_key, $data, $cache_time)
{
$cache_dir = _XE_PATH_ . 'files/cache/widgets/';
if(!is_dir($cache_dir)) {
FileHandler::makeDir($cache_dir);
}
$cache_file = $cache_dir . $cache_key . '.cache';
file_put_contents($cache_file, serialize($data));
}
/**
* CSS/JS 자원 추가
*/
private function addAssets($args)
{
// CSS 파일 추가
$css_file = $this->widget_path . 'css/widget.css';
if(file_exists($css_file)) {
Context::addCSSFile($css_file);
}
// 템플릿별 CSS 추가
$template_css = $this->widget_path . 'css/' . $args->template . '.css';
if(file_exists($template_css)) {
Context::addCSSFile($template_css);
}
// JavaScript 파일 추가
$js_file = $this->widget_path . 'js/widget.js';
if(file_exists($js_file)) {
Context::addJSFile($js_file);
}
}
/**
* 템플릿 컴파일
*/
private function compileTemplate($args)
{
// 템플릿 파일 경로
$template_file = $args->template . '.html';
$template_path = $this->widget_path . 'tpl/' . $template_file;
if(!file_exists($template_path)) {
$template_file = 'list.html';
}
// 템플릿 컴파일
$widget_path = $this->widget_path;
$template = new TemplateHandler();
return $template->compile($widget_path . 'tpl/', $template_file);
}
}
?>
템플릿 파일¶
기본 목록 템플릿 (list.html)¶
<!-- 위젯 CSS 클래스 -->
<div class="widget_my_widget widget_list">
<!--@if($document_list)-->
<ul class="document_list">
<!--@foreach($document_list as $document)-->
<li class="document_item">
<!--@if($widget_info->use_thumbnail === 'Y' && $document->thumbnail)-->
<div class="thumbnail">
<a href="{getUrl('', 'mid', $document->get('mid'), 'act', 'dispBoardContent', 'document_srl', $document->document_srl)}">
<img src="{$document->thumbnail}" alt="{$document->getTitleText()}"
width="{$widget_info->thumbnail_width}" height="{$widget_info->thumbnail_height}" />
</a>
</div>
<!--@end-->
<div class="content">
<h3 class="title">
<a href="{getUrl('', 'mid', $document->get('mid'), 'act', 'dispBoardContent', 'document_srl', $document->document_srl)}">
<!--@if($widget_info->title_cut_size > 0)-->
{cut_str($document->getTitleText(), $widget_info->title_cut_size)}
<!--@else-->
{$document->getTitleText()}
<!--@end-->
</a>
<!--@if($widget_info->show_comment_count === 'Y' && $document->get('comment_count') > 0)-->
<span class="comment_count">[{$document->get('comment_count')}]</span>
<!--@end-->
</h3>
<div class="meta">
<!--@if($widget_info->show_nickname === 'Y')-->
<span class="author">{$document->getNickName()}</span>
<!--@end-->
<!--@if($widget_info->show_date === 'Y')-->
<span class="date">{zdate($document->get('regdate'), 'm.d')}</span>
<!--@end-->
</div>
</div>
</li>
<!--@end-->
</ul>
<!--@else-->
<p class="no_documents">등록된 게시물이 없습니다.</p>
<!--@end-->
</div>
갤러리 템플릿 (gallery.html)¶
<div class="widget_my_widget widget_gallery">
<!--@if($document_list)-->
<div class="gallery_grid">
<!--@foreach($document_list as $document)-->
<div class="gallery_item">
<div class="image_container">
<a href="{getUrl('', 'mid', $document->get('mid'), 'act', 'dispBoardContent', 'document_srl', $document->document_srl)}">
<!--@if($document->thumbnail)-->
<img src="{$document->thumbnail}" alt="{$document->getTitleText()}" class="thumbnail" />
<!--@else-->
<div class="no_image">
<i class="fa fa-image"></i>
</div>
<!--@end-->
</a>
<!--@if($widget_info->show_comment_count === 'Y' && $document->get('comment_count') > 0)-->
<div class="overlay">
<span class="comment_count">
<i class="fa fa-comment"></i>
{$document->get('comment_count')}
</span>
</div>
<!--@end-->
</div>
<div class="info">
<h4 class="title">
<a href="{getUrl('', 'mid', $document->get('mid'), 'act', 'dispBoardContent', 'document_srl', $document->document_srl)}">
<!--@if($widget_info->title_cut_size > 0)-->
{cut_str($document->getTitleText(), $widget_info->title_cut_size)}
<!--@else-->
{$document->getTitleText()}
<!--@end-->
</a>
</h4>
<!--@if($widget_info->show_date === 'Y' || $widget_info->show_nickname === 'Y')-->
<div class="meta">
<!--@if($widget_info->show_nickname === 'Y')-->
<span class="author">{$document->getNickName()}</span>
<!--@end-->
<!--@if($widget_info->show_date === 'Y')-->
<span class="date">{zdate($document->get('regdate'), 'm.d')}</span>
<!--@end-->
</div>
<!--@end-->
</div>
</div>
<!--@end-->
</div>
<!--@else-->
<p class="no_documents">등록된 게시물이 없습니다.</p>
<!--@end-->
</div>
카드형 템플릿 (card.html)¶
<div class="widget_my_widget widget_card">
<!--@if($document_list)-->
<div class="card_container">
<!--@foreach($document_list as $document)-->
<article class="card_item">
<!--@if($widget_info->use_thumbnail === 'Y' && $document->thumbnail)-->
<div class="card_image">
<a href="{getUrl('', 'mid', $document->get('mid'), 'act', 'dispBoardContent', 'document_srl', $document->document_srl)}">
<img src="{$document->thumbnail}" alt="{$document->getTitleText()}" />
</a>
</div>
<!--@end-->
<div class="card_content">
<h3 class="card_title">
<a href="{getUrl('', 'mid', $document->get('mid'), 'act', 'dispBoardContent', 'document_srl', $document->document_srl)}">
<!--@if($widget_info->title_cut_size > 0)-->
{cut_str($document->getTitleText(), $widget_info->title_cut_size)}
<!--@else-->
{$document->getTitleText()}
<!--@end-->
</a>
</h3>
<div class="card_summary">
{cut_str($document->getSummary(), 100)}
</div>
<div class="card_footer">
<!--@if($widget_info->show_nickname === 'Y')-->
<span class="author">
<i class="fa fa-user"></i>
{$document->getNickName()}
</span>
<!--@end-->
<!--@if($widget_info->show_date === 'Y')-->
<span class="date">
<i class="fa fa-calendar"></i>
{zdate($document->get('regdate'), 'Y.m.d')}
</span>
<!--@end-->
<!--@if($widget_info->show_comment_count === 'Y' && $document->get('comment_count') > 0)-->
<span class="comments">
<i class="fa fa-comment"></i>
{$document->get('comment_count')}
</span>
<!--@end-->
</div>
</div>
</article>
<!--@end-->
</div>
<!--@else-->
<div class="no_documents">
<i class="fa fa-inbox"></i>
<p>등록된 게시물이 없습니다.</p>
</div>
<!--@end-->
</div>
CSS 스타일¶
기본 스타일 (widget.css)¶
/* 공통 스타일 */
.widget_my_widget {
margin: 20px 0;
}
.widget_my_widget a {
text-decoration: none;
color: #333;
}
.widget_my_widget a:hover {
color: #007bff;
}
.no_documents {
text-align: center;
padding: 40px 20px;
color: #999;
font-style: italic;
}
/* 목록형 스타일 */
.widget_list .document_list {
list-style: none;
padding: 0;
margin: 0;
}
.widget_list .document_item {
display: flex;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.widget_list .document_item:last-child {
border-bottom: none;
}
.widget_list .thumbnail {
flex-shrink: 0;
margin-right: 15px;
}
.widget_list .thumbnail img {
border-radius: 4px;
object-fit: cover;
}
.widget_list .content {
flex: 1;
min-width: 0;
}
.widget_list .title {
margin: 0 0 5px 0;
font-size: 14px;
font-weight: normal;
line-height: 1.4;
}
.widget_list .title a {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.widget_list .comment_count {
color: #007bff;
font-size: 12px;
margin-left: 5px;
}
.widget_list .meta {
font-size: 12px;
color: #666;
}
.widget_list .meta span {
margin-right: 10px;
}
/* 갤러리형 스타일 */
.widget_gallery .gallery_grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.widget_gallery .gallery_item {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
.widget_gallery .gallery_item:hover {
transform: translateY(-2px);
}
.widget_gallery .image_container {
position: relative;
padding-bottom: 75%; /* 4:3 비율 */
overflow: hidden;
}
.widget_gallery .thumbnail {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.widget_gallery .no_image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
color: #dee2e6;
font-size: 2em;
}
.widget_gallery .overlay {
position: absolute;
top: 5px;
right: 5px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
}
.widget_gallery .info {
padding: 10px;
}
.widget_gallery .title {
margin: 0 0 5px 0;
font-size: 13px;
font-weight: 500;
line-height: 1.3;
}
.widget_gallery .title a {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.widget_gallery .meta {
font-size: 11px;
color: #999;
}
/* 카드형 스타일 */
.widget_card .card_container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
}
.widget_card .card_item {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.widget_card .card_item:hover {
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
}
.widget_card .card_image {
position: relative;
padding-bottom: 56.25%; /* 16:9 비율 */
overflow: hidden;
}
.widget_card .card_image img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.widget_card .card_content {
padding: 20px;
}
.widget_card .card_title {
margin: 0 0 10px 0;
font-size: 16px;
font-weight: 600;
line-height: 1.4;
}
.widget_card .card_summary {
color: #666;
font-size: 14px;
line-height: 1.5;
margin-bottom: 15px;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.widget_card .card_footer {
display: flex;
align-items: center;
gap: 15px;
font-size: 12px;
color: #999;
border-top: 1px solid #f0f0f0;
padding-top: 15px;
}
.widget_card .card_footer span {
display: flex;
align-items: center;
gap: 4px;
}
/* 반응형 */
@media (max-width: 768px) {
.widget_gallery .gallery_grid {
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
}
.widget_card .card_container {
grid-template-columns: 1fr;
}
.widget_list .document_item {
flex-direction: column;
}
.widget_list .thumbnail {
margin-right: 0;
margin-bottom: 10px;
align-self: flex-start;
}
}
JavaScript 기능¶
위젯 JavaScript (widget.js)¶
/**
* 위젯 JavaScript 기능
*/
(function() {
'use strict';
// 위젯 초기화
function initWidget() {
// 지연 로딩 설정
setupLazyLoading();
// 툴팁 설정
setupTooltips();
// 모달 설정
setupModal();
}
// 이미지 지연 로딩
function setupLazyLoading() {
if ('IntersectionObserver' in window) {
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
imageObserver.unobserve(img);
}
});
});
document.querySelectorAll('.widget_my_widget img[data-src]').forEach(img => {
imageObserver.observe(img);
});
}
}
// 툴팁 설정
function setupTooltips() {
const tooltipElements = document.querySelectorAll('.widget_my_widget [data-tooltip]');
tooltipElements.forEach(element => {
element.addEventListener('mouseenter', showTooltip);
element.addEventListener('mouseleave', hideTooltip);
});
}
function showTooltip(e) {
const element = e.target;
const tooltipText = element.dataset.tooltip;
if (!tooltipText) return;
const tooltip = document.createElement('div');
tooltip.className = 'widget-tooltip';
tooltip.textContent = tooltipText;
tooltip.style.cssText = `
position: absolute;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 6px 10px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 10000;
pointer-events: none;
`;
document.body.appendChild(tooltip);
const rect = element.getBoundingClientRect();
tooltip.style.left = rect.left + 'px';
tooltip.style.top = (rect.top - tooltip.offsetHeight - 5) + 'px';
element._tooltip = tooltip;
}
function hideTooltip(e) {
const element = e.target;
if (element._tooltip) {
element._tooltip.remove();
delete element._tooltip;
}
}
// 모달 설정
function setupModal() {
document.addEventListener('click', function(e) {
const modalTrigger = e.target.closest('[data-modal-target]');
if (modalTrigger) {
e.preventDefault();
openModal(modalTrigger.dataset.modalTarget);
}
const modalClose = e.target.closest('[data-modal-close]');
if (modalClose) {
e.preventDefault();
closeModal();
}
});
// ESC 키로 모달 닫기
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeModal();
}
});
}
function openModal(targetId) {
const modal = document.getElementById(targetId);
if (modal) {
modal.style.display = 'block';
document.body.style.overflow = 'hidden';
}
}
function closeModal() {
const modals = document.querySelectorAll('.widget-modal');
modals.forEach(modal => {
modal.style.display = 'none';
});
document.body.style.overflow = '';
}
// DOM 준비 시 초기화
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initWidget);
} else {
initWidget();
}
// 전역 함수로 노출
window.WidgetHelper = {
refreshWidget: function(widgetId) {
// 위젯 새로고침 기능
const widget = document.getElementById(widgetId);
if (widget) {
// AJAX로 위젯 데이터 새로고침
fetch('/widgets/my_widget/refresh.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
widget_id: widgetId
})
})
.then(response => response.text())
.then(html => {
widget.innerHTML = html;
initWidget(); // 새로 로드된 요소들 재초기화
})
.catch(error => {
console.error('위젯 새로고침 실패:', error);
});
}
}
};
})();
모범 사례¶
- 캐싱: 데이터 조회 결과를 적절히 캐싱하여 성능 향상
- 반응형: 다양한 화면 크기에 대응하는 반응형 디자인
- 접근성: 스크린 리더와 키보드 내비게이션 지원
- 성능: 이미지 지연 로딩과 효율적인 DOM 조작
- 확장성: 템플릿 시스템으로 다양한 디자인 지원
다음 단계¶
커스텀 위젯을 만들었다면, 위젯 캐싱에서 성능 최적화를 학습하세요.