Bootstrap 패널 탭 구현 가이드

개요

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를 활용하여 순차적으로 로드하는 방식도 있습니다.

태그: bootstrap jQuery tabs frontend ui-component

6월 18일 20:43에 게시됨