개요
Bootstrap을 활용한 탭 패널 구현은 웹 애플리케이션에서 널리 사용되는 기능입니다. 이 글에서는 jQuery 기반의 탭 플러그인 구현 방법과 함께 발생할 수 있는 렌더링 문제점에 대해 설명하겠습니다.
필수 라이브러리 연결
탭 기능을 사용하기 위해 필요한 CSS와 JavaScript 파일을 연결합니다.
<link rel="stylesheet" href="bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="../css/custom-tab.css">
<script src="jquery/jquery-1.8.3.min.js"></script>
<script src="bootstrap/js/bootstrap.min.js"></script>
<script src="../js/custom-tab.js"></script>
탭 컨테이너 및 초기화
HTML 문서에 탭 컨테이너를 추가하고 JavaScript로 초기화합니다.
<div id="tabPanel"></div>
<script>
var presiteId = '${presiteId}';
var vehicleId = '${vehicleId}';
var equipmentCode = '${equipmentCode}';
$("#tabPanel").panelTabs({
items: [
{
id: 'editForm',
label: '편집',
url: "${ctxPath}/presite/edit/" + presiteId
},
{
id: 'vehicleInfo',
label: '차량정보',
url: "${ctxPath}/vehicle/info/" + vehicleId
},
{
id: 'equipmentInfo',
label: '장비',
url: "${ctxPath}/equipment/detail/" + equipmentCode
}
],
initialIndex: 0,
lazyLoad: true
});
</script>
플러그인 구현 코드
탭 기능을 제공하는 jQuery 플러그인의 전체 소스 코드입니다.
(function ($, window, document) {
'use strict';
var PLUGIN_NAME = 'panelTabs';
$.fn[PLUGIN_NAME] = function (options) {
var container = $(this);
if (this.length === 0) {
return null;
}
var instance = this.data(PLUGIN_NAME);
if (!instance) {
instance = new TabPanel(this, options);
this.data(PLUGIN_NAME, instance);
}
return instance;
};
var TabPanel = function (element, settings) {
this.$container = $(element);
this.config = $.extend(true, {}, this.defaults, settings);
this.initialize();
};
TabPanel.prototype.defaults = {
initialIndex: 0,
lazyLoad: true
};
TabPanel.prototype.templates = {
navList: '<ul class="nav nav-tabs"></ul>',
navItem: '<li><a href="#{0}" data-toggle="tab"><span>{1}</span></a></li>',
closeIcon: '<i class="fa fa-times close-btn" title="닫기"></i>',
contentArea: '<div class="tab-content"></div>',
contentPane: '<div class="tab-pane fade" id="{0}"></div>'
};
TabPanel.prototype.initialize = function () {
if (!this.config.items || this.config.items.length === 0) {
console.error("탭 항목을 지정해주세요.");
return;
}
if (this.config.initialIndex < 0 || this.config.initialIndex >= this.config.items.length) {
console.error("initialIndex 값이 유효하지 않습니다.");
this.config.initialIndex = this.defaults.initialIndex;
}
this.$container.empty();
this.buildStructure(this.config.items);
};
TabPanel.prototype.buildStructure = function (items) {
var navList = $(this.templates.navList);
var contentArea = $(this.templates.contentArea);
for (var i = 0; i < items.length; i++) {
var navItem = $(this.templates.navItem.format(items[i].id, items[i].label));
if (items[i].closable) {
var closeIcon = $(this.templates.closeIcon);
navItem.find("a").append(closeIcon).append(" ");
}
navList.append(navItem);
var contentPane = $(this.templates.contentPane.format(items[i].id));
contentArea.append(contentPane);
}
this.$container.append(navList);
this.$container.append(contentArea);
this.loadTabContent();
this.$container.find(".nav-tabs li:eq(" + this.config.initialIndex + ") a").tab("show");
};
TabPanel.prototype.loadTabContent = function () {
var self = this;
var loadState = {};
var items = this.config.items;
for (var i = 0; i < items.length; i++) {
if (!this.config.lazyLoad || this.config.initialIndex === i) {
if (items[i].url) {
$("#" + items[i].id).load(items[i].url, items[i].params);
loadState[items[i].id] = true;
} else {
console.error("id=" + items[i].id + "인 탭에 URL이 지정되지 않았습니다.");
loadState[items[i].id] = false;
}
} else {
loadState[items[i].id] = false;
(function (tabId, tabUrl, tabParams) {
self.$container.find(".nav-tabs a[href='#" + tabId + "']").on('show.bs.tab', function () {
if (!loadState[tabId]) {
$("#" + tabId).load(tabUrl, tabParams);
loadState[tabId] = true;
}
});
})(items[i].id, items[i].url, items[i].params);
}
}
this.$container.find(".nav-tabs li a i.close-btn").each(function (index, element) {
$(element).on('click', function () {
var targetId = $(this).closest("a").attr("href").substring(1);
if (self.getActiveTabId() === targetId) {
self.$container.find(".nav-tabs li:eq(0) a").tab("show");
}
$(this).closest("li").remove();
$("#" + targetId).remove();
});
});
};
TabPanel.prototype.addTab = function (options) {
var self = this;
var navItem = $(this.templates.navItem.format(options.id, options.label));
if (options.closable) {
var closeIcon = $(this.templates.closeIcon);
navItem.find("a").append(closeIcon).append(" ");
}
this.$container.find(".nav-tabs").append(navItem);
var contentPane = $(this.templates.contentPane.format(options.id));
this.$container.find(".tab-content").append(contentPane);
this.loadState[options.id] = false;
(function (tabId, tabUrl, tabParams) {
self.$container.find(".nav-tabs a[href='#" + tabId + "']").on('show.bs.tab', function () {
if (!self.loadState[tabId]) {
$("#" + tabId).load(tabUrl, tabParams);
self.loadState[tabId] = true;
}
});
})(options.id, options.url, options.params);
if (options.closable) {
this.$container.find(".nav-tabs li a[href='#" + options.id + "'] i.close-btn").on('click', function () {
var targetId = $(this).closest("a").attr("href").substring(1);
if (self.getActiveTabId() === targetId) {
self.$container.find(".nav-tabs li:eq(0) a").tab("show");
}
$(this).closest("li").remove();
$("#" + targetId).remove();
});
}
};
TabPanel.prototype.removeTab = function (tabId) {
$("#" + tabId).remove();
this.$container.find(".nav-tabs li a[href='#" + tabId + "']").closest("li").remove();
};
TabPanel.prototype.showTab = function (tabId) {
this.$container.find(".nav-tabs li a[href='#" + tabId + "']").tab("show");
};
TabPanel.prototype.getActiveTabId = function () {
var href = this.$container.find(".nav-tabs li.active a").attr("href");
return href ? href.substring(1) : null;
};
String.prototype.format = function () {
if (arguments.length === 0) return this;
var result = this;
for (var i = 0; i < arguments.length; i++) {
result = result.replace(new RegExp("\\{" + i + "\\}", "g"), arguments[i]);
}
return result;
};
})(jQuery, window, document);
주요 기능 설명
- 지연 로딩(lazyLoad) 기능: 탭을 클릭할 때마다 해당 콘텐츠를 비동기로 로드합니다. 초기 화면 로딩 속도를 개선할 수 있습니다.
- 탭 추가: addTab 메서드를 사용하여 동적으로 새로운 탭을 추가할 수 있습니다.
- 탭 제거: closeable 옵션이 true로 설정된 탭은 사용자가 닫을 수 있습니다.
- 활성 탭 조회: 현재 활성화된 탭의 ID를 가져올 수 있습니다.
렌더링 문제점 및 해결 방안
탭을 한 번에 여러 개 로드하는 경우, 콘텐츠가 잘못된 탭에 렌더링되는 문제가 발생할 수 있습니다. 이 문제는 다음과 같은 방식으로 해결할 수 있습니다:
$("#tabPanel").panelTabs({
items: [...],
lazyLoad: true
});
lazyLoad 옵션을 true로 설정하면 initially 첫 번째 탭만 로드하고, 나머지 탭은 사용자가 클릭할 때 비동기로 로드합니다. 이렇게 하면 콘텐츠 렌더링 순서 문제를 해결할 수 있습니다.
또 다른 방법은 loadAll 옵션을 사용하여 각 탭의 로드 완료 순서를 제어하거나, Promise를 활용하여 순차적으로 로드하는 방식도 있습니다.