Dico

Tab을 만들어보자

안녕하세요 Lovefield입니다.

오늘은 지인의 요청으로 JavaScript로 Tab 기능을 만드는 법에 대해서 알려드리겠습니다.

우선 기능을 만들기 전에 조건들을 적어볼까요?

  1. 버튼을 클릭하면 버튼에 해당하는 컨텐츠가 보여야 한다.
  2. 버튼을 클릭하면 버튼에 해당하지 않은 컨텐츠는 가려져야 한다.
  3. 탭의 영역이 여러 군데여도 작동해야 한다.
  4. 다중 탭(탭 안의 탭)일 때도 작동해야 한다.


위의 방식대로 작동하기 위한 HTML 구조는 다음과 같습니다.

<div class="tab_wrap tab_area">
  <div class="btn_area clearfix">
    <button class="btn btn_tab act" data-depth="0" data-idx="0">First</button>
    <button class="btn btn_tab" data-depth="0" data-idx="1">Second</button>
    <button class="btn btn_tab" data-depth="0" data-idx="2">Third</button>
    <button class="btn btn_tab" data-depth="0" data-idx="3">Fourth</button>
  </div>
  
  <div class="content_area act" data-depth="0" data-idx="0">First tab content</div>
  <div class="content_area" data-depth="0" data-idx="1">
    <p>Second tab content</p>
    <div class="tab_area">
      <div class="btn_area clearfix">
        <button class="btn btn_tab act" data-depth="1" data-idx="0">First</button>
        <button class="btn btn_tab" data-depth="1" data-idx="1">Second</button>
        <button class="btn btn_tab" data-depth="1" data-idx="2">Third</button>
        <button class="btn btn_tab" data-depth="1" data-idx="3">Fourth</button>
      </div>

      <div class="content_area act" data-depth="1" data-idx="0">2Depth First tab content</div>
      <div class="content_area" data-depth="1" data-idx="1">2Depth Second tab content</div>
      <div class="content_area" data-depth="1" data-idx="2">2Depth Third tab content</div>
      <div class="content_area" data-depth="1" data-idx="3">2Depth Fourth tab content</div>
    </div>
  </div>
  <div class="content_area" data-depth="0" data-idx="2">
    <p>Third tab content</p>
    <div class="tab_area">
        <div class="btn_area clearfix">
          <button class="btn btn_tab act" data-depth="1" data-idx="0">First</button>
          <button class="btn btn_tab" data-depth="1" data-idx="1">Second</button>
          <button class="btn btn_tab" data-depth="1" data-idx="2">Third</button>
          <button class="btn btn_tab" data-depth="1" data-idx="3">Fourth</button>
        </div>

        <div class="content_area act" data-depth="1" data-idx="0">2Depth First tab content</div>
        <div class="content_area" data-depth="1" data-idx="1">2Depth Second tab content</div>
        <div class="content_area" data-depth="1" data-idx="2">2Depth Third tab content</div>
        <div class="content_area" data-depth="1" data-idx="3">2Depth Fourth tab content</div>
      </div>
    </div>
  <div class="content_area" data-depth="0" data-idx="3">Fourth tab content</div>
</div>

<div class="tab_wrap tab_area">
  <div class="btn_area clearfix">
    <button class="btn btn_tab act" data-depth="0" data-idx="0">First</button>
    <button class="btn btn_tab" data-depth="0" data-idx="1">Second</button>
    <button class="btn btn_tab" data-depth="0" data-idx="2">Third</button>
    <button class="btn btn_tab" data-depth="0" data-idx="3">Fourth</button>
  </div>
  
  <div class="content_area act" data-depth="0" data-idx="0">First tab content</div>
  <div class="content_area" data-depth="0" data-idx="1">Second tab content</div>
  <div class="content_area" data-depth="0" data-idx="2">Third tab content</div>
  <div class="content_area" data-depth="0" data-idx="3">Fourth tab content</div>
</div>

총 2개의 탭 영역을 구성하였고요.

개인적으로 depth와 idx(index의 약자)는 data-* 형식을 이용하는 편입니다.

data-* 형식은 HTML에서 엘리먼트에게 특정한 값을 부여할때 사용하기 좋아서 애용하고 있습니다.

각각의 탭 동작 영역이 겹치지 않도록 tab_area 클레스를 사용했습니다.

활성화 클레스는 act로 사용했습니다.

버튼클레스는 btn_tab, 컨텐츠 클레스는 content_area로 사용했습니다.

html

HTML을 작성하면 위처럼 나오게 될거에요.

자 그럼 여기서 CSS를 사용해 탭의 모양을 만들어볼까요?

body{background:#f4f4f4}
.btn{padding:0;background:transparent;border:0;outline:0}
.clearfix::after{display:block;content:'';clear:both}

.tab_wrap{width:800px;margin:50px auto}
.tab_wrap .btn_tab{float:left;width:120px;height:30px;background:#fff;border-radius:10px 10px 0 0;text-align:center;line-height:30px}
.tab_wrap .btn_tab.act{background:#9adce2;font-weight:bold}
.tab_wrap .content_area{display:none;width:100%;min-height:200px;padding:10px;background:#fff;border-radius:0 0 10px 10px;box-sizing:border-box}
.tab_wrap .content_area.act{display:block}
.tab_wrap *[data-depth="1"]{background:#f4f4f4}

reset CSS는 생략하고 간단하게 만들어보았습니다.

css

다중탭은 2번째,3번째 탭안에 있기때문에 처음에는 이런 모양이 될거에요.

이제 제대로 작동하기 위해 JavaScript를 적용해 볼까요?

function bindingTabEvent(wrap){
  
}

우선 함수를 하나 만들어줍니다.

class 명을 받아서 이벤트를 부여하는 역할을 할 녀석이에요.

그래서 함수명은 탭 이벤트를 적용한다는 뜻으로 지었습니다.

function bindingTabEvent(wrap){
  let wrapEl = document.querySelectorAll(wrap);
  
  wrapEl.forEach(function(tabArea){
   
  });
}

인자로 넘어오는 값인 wrap(class 명)을 querySelectorAll 을 통해 선택합니다.

탭 영역이 불특정 다수이기 때문에 엘리먼트 리스트를 전달받았고요.

forEach 를 통해 각각의 탭 영역이 개별로 동작하도록 해줍니다.

wrapEl.forEach(function(tabArea){
  let btn = tabArea.querySelectorAll('.btn_tab');
  
  btn.forEach(function(item){
    
  });
});

각각의 탭 영역 안에서의 버튼을 찾아줍니다.

그 후에 각각의 버튼들에 이벤트를 부여해야 하므로 forEach를 이용합니다.

이제 forEach 안에서 버튼에게 이벤트를 부여할건데요.

item.addEventListener('click', function(){
  let parent = findParent(this, 'tab_area');
  // 현재 클릭한 버튼에 해당하는 탭의 영역단위를 묶고 있는 부모를 찾습니다.
  let idx = this.dataset['idx'];
  // 현재 버튼이 가지고 있는 idx 값.
  let depth = this.dataset['depth'];
  // 현재 버튼이 가지고 있는 depth값.
  let btnArr = parent.querySelectorAll('.btn_tab[data-depth="'+ depth +'"]');
  // 현재 버튼과 동일 선상에 있는 다른 버튼들.
  let contentArr = parent.querySelectorAll('.content_area[data-depth="'+ depth +'"]');
  // 현재 버튼에 해당하는 그륩단위 안의 컨텐츠 엘리먼트들.
  
  btnArr.forEach(function(btn){ btn.classList.remove('act'); });
  this.classList.add('act');
  contentArr.forEach(function(content){ content.classList.remove('act'); });
  parent.querySelector('.content_area[data-idx="'+ idx +'"][data-depth="'+ depth +'"]').classList.add('act');
});

다중 탭으로 인해서 각각의 탭 영역 단위로 작동해야 합니다.

즉 부모의 영역을 찾는 게 필요해서 findParent라는 함수를 만들어서 사용했는데요.

함수는 아래에서 추가로 설명해드리겠습니다.

버튼을 클릭할 경우는 다음과 같은 기능을 수행하게 합니다.

  1. btnArr에 해당하는 모든 버튼의 class에서 act를 제거.
  2. 현재 버튼의 class에 act를 추가
  3. contentArr에 해당하는 모든 엘리먼트의 class에서 act를 제거
  4. idx와 depth에 해당하는 컨텐츠 엘리먼트의 class에서 act를 추가

이제 작성한 함수를 실행시켜 주시면 됩니다.

bindingTabEvent('.tab_wrap');

괄호 안의 문자가 처음 함수를 선언할때 있었던 wrap으로 전달되면서 기능이 실행됩니다.

마지막으로 부모를 찾는 함수입니다.

function findParent(el, className){
  let check = el.parentNode.classList.contains(className);
  
  if(check === true){
     return el.parentNode;
  }else{
    return findParent(el.parentNode, className);
  }
}

재귀 함수라고 불리는데요.

원하는 값을 찾을 때까지 자기 자신을 호출하는 함수를 칭합니다.

인자로 현재 엘리먼트(el)와 찾아야 하는 부모의 클레스명(className)을 받아서 실행합니다.

현재 엘리먼트의 부모 엘리먼트에게 전달받은 클레스명이 있다면 부모 엘리먼트를 반환합니다.

부모 엘리먼트에게 전달받은 클레스명이 없다면 인자의 el을 부모 엘리먼트로 변경해서 다시 실행합니다.

꽤 간단한 로직이죠?

완성된 결과물은 아래에서 혹은 codepen에서 보실 수 있습니다.