ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [FE] 부트스트랩 없이 왼쪽으로 펼쳐지는 드롭다운 메뉴 만들기
    공부/etc 2022. 9. 12. 12:00
    반응형

    사용한 것: html, css, javascript, jquery

     

    OSS 활동을 하면서 Export 버튼 확장 기능을 맡았다.

    기능 자체는 매우 간단한 기능이였지만, 여기에 삽질한 시간은 상상을 초월했다.

     

     

    1. HTML


    내용을 설명하자면 지금까지 Export라고 적힌 버튼을 클릭하면 기본적인 엑셀 레포트 파일을 다운 받는 것인데, 변경할 사항은 Export 버튼을 누르면 위 영상처럼 드롭다운 기능을 사용해 excel, rdf, yaml, json 같은 확장자를 선택할 수 있는 선택지가 펼쳐지고, 거기서 선택한 확장자를 클릭해 파일을 다운 받는 것이다.

     

    처음에는 현재 기여할 웹페이지에 관련 기능이 있을 줄 알고, 코드를 재사용하려고 페이지를 샅샅히 살펴봤었다. 하지만 드롭다운 기능을 사용하는 것이 없었고 내가 직접 만들어야 했다.

     

    내 상황은 input 태그로 만들어진 버튼을 클릭하면 바로 아래에 드롭다운 기능이 되어야 한다. 그래서 스택 오버 플로우에 검색을 해보니 부트스트랩을 적용해서 사용하는 코드가 많았지만, 현재 내가 기여할 페이지는 부트스트랩이 적용되지 않아서 나한테 필요한 내용을 찾을 수 없었다.

     

    label, span, div, ul li, .... 등등 다양한 태그를 붙여가면서 시도를 해봤다. 하지만 기능을 만들어도 하나둘씩 나사 빠진 경우가 있어서 알고리즘 문제를 풀 때 풀이가 막히면 다시 다 지우고 새마음 새시작으로 다시 풀어보는 것처럼 코드를 전부 지우고 쓰기를 반복했다. 이때 html과 css에 대해 정말 많이 배웠다.

     

    결론적으로는 div 태그로 컨테이너(div1)를 하나 만들어서 그 안에 input 태그의 버튼과 div 태그(div2)를 넣어주고, div2안에 드롭다운 선택지로 사용할 a태그를 추가하여 드롭다운 메뉴가 들어가도록 하였다.

     

    이러면 문제점이 하나 생긴다. div 태그의 경우엔 한 블록을 전부 차지하는 태그이기 때문에 같은 줄에 다른 버튼들도 있을 수 있게 만들어주어야 하고, 왼쪽으로 펼칠 수도 있어야한다. 이것은 css단에서 설정을 할 수 있다.


     

     

     

    2. CSS


    이건 스택 오버 플로우에 많은 내용이 나오기 때문에 도움을 받을 수 있었다.


    A. 같은 줄에 input 태그와 div 태그를 같이 있도록 만들기

    display: inline-block을 사용하면 된다.

    display: block은 무조건 자기 혼자 한 줄을 점유하는 것이다. div 태그가 display: block이 적용된 대표적인 예시이다.

    나는 input 버튼과 a 태그 리스트를 합쳐서 둘이 따로따로 노는 것이 아니라 하나의 완전체로 작동해야해서 inline의 특징과 block의 특징이 모두 필요하므로 inline-block으로 설정을 해주었다.


    B. 화면 맨 앞에 드롭다운 메뉴 선택지가 나오도록 하기

    글 맨 위에 있는 영상을 보면 버튼을 눌렀을 경우 버튼에 있는 내용이 본문에 있는 내용보다 더 앞에 나오게 된다.

     

    내 경우엔 div0을 부모로 두고 있는 div1과 본문이 서로 형제 요소이기 때문에 div0에 position: relative를 적용하였다.

    이후 드롭다운 선택지(div2)엔 position: absolute, z-index: x(특정값)를 적용해주면 된다.

     

    그리고 a태그가 한 줄에 하나씩 나올 수 있도록 각 a태그마다 display: block을 설정해준다.

    display:block은 z-index: x로 인해 본문의 내용이 아래로 가지 않고, 그대로 화면을 겹칠 수 있게 된다.

    각 a태그마다 block을 일일이 설정하지 말고 div2에 display: block 하나만 추가해도 되지 않겠냐 싶지만, display: block의 특징이 하나의 줄을 전부 차지하기 때문에 한 줄에 버튼 여러 개가 들어갈 수 있게 만들 수 없다.


    C. 드롭다운 선택지를 왼쪽으로 펼치기

     

    먼저 선택지를 왼쪽으로 펼친다면 미관상 글자를 오른쪽 정렬하는 것이 좋다.

    * text-align: right

    그리고 드롭다운 선택지를 왼쪽으로 펼치려면 input 태그의 버튼과 드롭다운 메뉴의 선택지가 오른쪽에서 일자로 만들어져야 한다. 이것은 div2에 right:0을 추가하면 된다.

    right: 0은 절대 위치를 수정하는 것인데, 여기서는 div1의 맨 오른쪽에 div2를 붙이는 것이다.

    만약 div1에 right:0을 하면 화면 맨 오른쪽에 붙어 있을 것이다.


    D. 처음 페이지에 들어 왔을 때 드롭다운 선택지가 보여지지 않도록하기

     

    당연하겠지만 페이지에 들어왔을 때, 처음부터 드롭다운 메뉴 선택지가 보여지지 않을 것이다.

    그래서 div2에 display: none을 추가해준다. 이것은 javascript, jquery를 사용하여 버튼을 클릭시 보여주게 할 것이다.


     

     

     

    3. Javascript, jQuery


    javascript와 jquery에서 코드를 작성할 때의 목표는 코드 재사용을 쉽게 만드는 것이였다.

    내가 만드는 기능은 여러 페이지에서 쓰이고, 같은 페이지에서도 페이지 상단과 하단에 쓰이는 곳이 있다. 거기다가 처음 만들어진 드롭다운 메뉴이기 때문에 앞으로 다른 기능을 위해 드롭다운 메뉴를 사용할 경우, 이 코드를 베이스로 만들 확률이 높기 때문에 여러가지를 고려해야 했다.


    A. id값 동적으로 할당하기

     

    자바스크립트 내부 로직에 html의 id값을 직접적으로 할당해줄 경우, 같은 기능을 하는 드롭다운이 같은 페이지에 있어도 새로운 함수를 더 만들어서 작동해줘야 한다는 문제점이 생긴다.

    그래서 id값을 직접적으로 할당하는 것보다는 동적으로 할당할 수 있도록 만들어주었다. 이렇게 되면 나중에 코드를 수정할 때, 정적 → 동적으로 코드를 바꾸는 것은 힘들지만, 동적 → 정적으로 코드를 변경하기 쉽다는 장점도 가질 수 있다.

     

    현재 이벤트가 발생하는 요소의 아이디를 찾는 것은 아래처럼 사용하면 된다.

    var targetId = event.target.id;

     

    이것을 바탕으로 부모 아이디를 찾는다면 closest(), 같은 부모를 가지는 형제 요소를 찾는다면 siblings()를 사용하면 된다.


    B. 드롭다운 메뉴 펼치기, 접기

     

    Export 버튼을 클릭하면 ExportList라는 메뉴를 펼치고 접어야 한다.

    이 코드는 이전에 다른 분이 현재 기능을 만들려고 시도하면서 만들었던 로직이라 그대로 재사용을 했다.

    exportList:function(){
        var buttonId = event.target.id;
        var exportListId = "#" + $("#" + buttonId).siblings("div").attr("id");
    
        if ($(exportListId).css('display')=='none') {
            $(exportListId).show();
        }else{
            $(exportListId).hide();
        }
        $(exportListId).menu();
    }

     

    여기서 siblings()는 같은 레벨에 있는 요소를 찾아주는 것이다.

     

    만약 input 버튼의 형제 요소를 찾기위해 siblings()을 사용한다면 div2가 나올 것이다.


    C. 드롭다운 메뉴가 펼쳐져 있을 때, 드롭다운 메뉴 외의 다른 영역을 클릭하면 드롭다운 메뉴 접기

     

    보통 드롭다운 메뉴를 만든다면 hover를 적용해서 만든다. 하지만 여기서는 클릭하면 메뉴를 펼치고, 다시 클릭하면 메뉴를 접어야 하기 때문에 외부 영역을 클릭하면 메뉴를 접을 수 있도록 설정을 따로 해주어야 했다.

    $(document).click(function(e){
        if(!$("#ExportContainerTop").has(e.target).length){
            $("#ExportListTop").hide();
        }
        if(!$("#ExportContainerBottom").has(e.target).length){
            $("#ExportListBottom").hide();
        }
    });

    이 코드는 드롭다운 메뉴가 페이지 상단과 하단 둘 다 있는 경우기 때문에 id값을 Export*Top, Export*Bottom으로 나눠주었다.


    D. 이벤트 버블링을 적용해 드롭다운 메뉴에서 선택지를 클릭하면 파일을 다운로드하기

     

    선택지는 a태그로 이루어져있고, 여기에 onclick 이벤트를 사용해서 각 a태그마다 설정을 해줄 수 있다.

    하지만 이 경우 나중에 드롭다운 메뉴 선택지를 추가할 때마다 코드를 일일이 추가해줘야 하고, B번처럼 한 페이지에 같은 기능을 하는 드롭다운이 있으면 코드 작성할 내용이 더 많아진다는 문제점이 생긴다.

     

    코드를 더 줄이고자 이벤트 버블링을 적용하였다.

    이벤트 버블링을 모르는 사람들을 위해 간단히 이야기하면 어떤 요소에서 이벤트가 발생하면, 해당 요소의 부모 요소, 그 부모의 부모의 요소, 그 부모의 부모의 부모의 요소, .... 가 이벤트 함수를 가지고 있다면 전부 순차적으로 실행될 수 있도록 하는 기능이다.

    이 그림으로 설명하면 a태그를 클릭하는 이벤트가 발생할 경우, a태그의 부모인 div2에 이벤트 함수가 있으면 실행되고, div2의 부모인 div1에 이벤트 함수가 있으면 실행되는 것이다. (중간에 버블링이 진행되지 않도록 중지할 수 있다.)

    이때 자식 → 부모로 거슬러 올라가는 것은 버블링이고, 부모 → 자식으로 내려가는건 캡처링이라고 부른다. 해당 내용은 아래 링크에 자세히 설명되어 있다.

     

    https://ko.javascript.info/bubbling-and-capturing

     

    버블링과 캡처링

     

    ko.javascript.info

     

    그래서 다운로드하는 a 태그에는 onclick 이벤트를 적지 않고, 그 부모인 div2에 onclick 이벤트를 발생하게 만들어준다.

    그러면 div2에서 a 태그에 있는 id 값을 통해 각각의 함수를 실행할 수 있게 만들어준다.

     

    <div id="div1">
        <input type="button" id="Export" value="Export"/>
        <div id="div2" onclick="fn.function1()">
        	<a id="A">aaa</a>
            <a id="B">bbb</a>
            <a id="C">ccc</a>
            <a id="D">ddd</a>
            <a id="E">eee</a>
        </div>
    </div>
    function1 : function() {
        var targetId = event.target.id;
        var parentId = $("#" + targetId).closest("div").attr("id")
    
        // download file
        if (targetId === "A") fn.fa();
        else if (targetId === "B") fn.fb();
        else if (targetId === "C") fn.fc();
        else if (targetId === "D") fn.fd();
        else if (targetId === "E") fn.fe();
        else if (targetId === "F") fn.ff();
    
        // hide list
        $("#" + parentId).hide();
    }

     

    여기서 closest()는 현재 요소의 상위 부모들중 가장 가까운 부모를 반환해준다.

    예를 들어 이 글에서 a 태그의 상위 요소는 div1과 div2가 있는데, a태그와 가장 가까운 부모는 div2이므로 div2를 반환한다.


     

    눈으로 보기엔 간단한 내용이였지만, 그 안에서는 진짜 많은 코드가 작동하게 된다.

    이거 포함해서 "이거 간단한 것 같은데요?"라고 말했다가 뒷통수 맞은 적이 한두번이 아니니 말하는걸 조심해야겠다.

    반응형

    댓글

Designed by Tistory.