[javscript] 이벤트리스너 질문

조회수 754회

안녕하세요. 아래 codepen 링크는 제가 작성한 flex-panel (클릭하면 커졌다가 작아지는 패널) 코드입니다. https://codepen.io/w00kgod/pen/VgNaxW

구현한 기능 :

JS로 구현한 기능은 3가지 입니다.

1. 각 패널을 클릭하면 패널이 열리면서 open 과 open-active 클래스를 추가하기

this.classList.add('open');
        setInterval(function(){item.classList.add('open-active');},400)  

2 .패널이 클릭될때 이전에 클릭된게 있으면 닫고 패널이 열리게 하기

이 기능은 previousE라는 전역 변수를 이용하여 이벤트가 발생할때마다 발생이 일어난 element를 저장하여 구현하였습니다.

    const panel = document.querySelectorAll('.panel');
    var previousE = panel[0]; // 초기화 

    panel.forEach(function(item){

      item.addEventListener('click',function(){
        if(previousE.className.split(' ')[2]){
        previousE.classList.remove('open');
        }

        this.classList.add('open');
        setInterval(function(){item.classList.add('open-active');},400)  

        previousE = this // 현재 이벤트 previousE 에 저장 

        this.addEventListener('click', function(){
          this.classList.remove('open','open-active'); 
        });

      });
    })

3. 마지막으로 클릭했던 패널을 다시 클릭하여 열렸던 패널을 닫히게 하기

아래의 코드를 item.addEventListener 에 추가하였습니다.

   this.addEventListener('click', function(){
          this.classList.remove('open','open-active'); 
        });

그런데 여기서 문제점이 있습니다.

마지막 item.addEventListener 안에 this.addEventListener 를 받는 경우 패널이 열렸다 닫히는 과정이 딱 한번만 일어나게 됩니다.

질문1) 이 과정이 클릭할때마다 반복되어야 하는데 왜 한번만 발생한 뒤에는 다시 열리지 않을까요?

저의 시행착오 :

저는 AddClass.List로 open , open-active가 안들어가거나, remove로 open, open-active 클래스가 삭제가 되지 않았다고 생각하여 console.log(this)this.classList.add('open');this.classList.remove('open','open-active'); 주변을 확인해 보았습니다.

그런데 classList.add 가 작성된곳 위의 console.log(this) 에서는 넣지도 않은 open class가 들어가 있더군요.

질문2) console.log(this) 에서 open class가 들어가는 과정이 먼저 일어나는것은 호이스팅 때문인가요? 호이스팅이 어디에서 일어나는것인가요...? 질문3) console.log의 대상을 this가 아닌 다른것으로 해야할까요?

    console.log(this) // <div class="panel panel3 open open-active">
    this.classList.add('open')
    setInterval(function(){item.classList.add('open-active');},400) 

remove 근처에 this 에서는 open 클래스가 삭제되어 있습니다. 아마도 이것은 위에 초기화에서 open만 삭제해주어 발생한것같습니다.

   this.addEventListener('click', function(){
      console.log(this) //<div class="panel panel3 open-active">
      this.classList.remove('open','open-active'); 
      console.log(this) //div class="panel panel3 open-active">
    }

질문3) 질문1과 크게 다르지 않은 질문입니다만, 제가 작성한 방식( 과거 이벤트를 저장하고 , 이벤트 리스너에 이벤트 리스너를 또 달아서 해결하는 방식)으로 이 문제를 해결할 수 있을까요? 사실 , 좋은 방법은 아닌것같은데.. 이러한 방법으로 해결할 수 있을까? 라는 생각이 들어 질문을 드립니다.

질문2와 질문3은 질문1을 해결하다 보니 발생한 문제입니다.질문1을 해결할 수 있는 더 나은 방법을 알려주신다면 더 감사할것같습니다!

긴 질문글 읽어주셔서 감사합니다!

  • (•́ ✖ •̀)
    알 수 없는 사용자

2 답변

  • 질문 1 2 3 각각에 딱딱 들어맞는 답은 저보다 더 이론이 풍부하신 분이 봐주실 것으로 믿고 저는 그냥 실무적인 차원에서 답을 드리자면… 지금 구현하신 게 소위 아코디언이라 불리는 것이므로, 그 구현의 적당한 로직 순서가 뭘까를 근본적으로 고찰해 보면 좋을 것 같습니다.

    1. 태초에는, 모든 패널은 닫혀 있다.
    2. 사용자가, 패널 C를 누른다.
    3. 그러면 JS는, 일단 C를 제외한 모든 패널을 닫는다.
    4. 그 다음 JS는, C가 열린 것인지 닫힌 것인지 판단한다.
      1. C가 닫혀 있었다면, JS는 C를 열고 종료한다.
      2. C가 열려 있었다면, JS는 C를 닫고 종료한다.
    5. 사용자는, 기대한 경험을 얻는다.

    보면 아시겠지만 이 로직은 '마지막으로 눌린 패널'이 뭔지를 별도 플래그 변수에 저장하지 않습니다. 그냥 뭔가 패널이 눌릴 때마다 눌림 이벤트가 발생한 event.target 패널과 그렇지 않은 패널들의 2가지를 지정하고 각각을 작동시키지요.

    이를테면, 지금 주어진 마크업에서 panel1~panel5를 클래스명이 아닌 ID로 뺀 다음, 이렇게 바꿔써 보는 겁니다.

    const panel = document.querySelectorAll(".panel")
    panel.forEach(function(item) {
      item.addEventListener('click', function () {
        var clickedItem = this
        var everythingElse = document.querySelectorAll('.panel:not(#'+clickedItem.id+')')
        everythingElse.forEach(function (el) {
          el.classList.remove('open-active', 'open')
        })
        if (clickedItem.classList.contains('open')) {
          clickedItem.classList.remove('open-active', 'open')
        } else {
          clickedItem.classList.add('open')
          setInterval(function() {
            clickedItem.classList.add("open-active")
          }, 400)
        }
      })
    })
    

    참고가 되면 좋겠네요.

    PS. 12번 라인에서 this가 아닌 itemsetInterval을 주고 계시는데, 지금 보이는 .open-active 관련해서 꼬여 있는 것들은 이 부분 때문에 일어나는 것이 아닌가 의심해 봅니다.

    • 말씀해주신것처럼 로직을 구현해야겠네요. 왜 제가 만들때는 그런로직을 구현을 못했을까요ㅎㅎ... ㅠㅠ 알 수 없는 사용자 2019.2.21 08:21
    • 당연히 아시는것이겠지만 c가 아닌 패널을 선택할때 탬플릿리터럴을 사용하면 좀 더 보기좋게 짤 수있네요. var everythingElse = document.querySelectorAll(`.panel:not(#${clickedItem.id})`) 처음에 괄호사이에 따옴표가 들어가있어서.. 이게 뭐지...? 했습니다. css :not(선택자) 방식으로 특정 id가 아닌 엘리멘트들의 노드리스트를 저장할 수 있는줄 처음알았네요. 다시한번 답변해주셔서 감사합니다!!!!!! 알 수 없는 사용자 2019.2.21 08:24
    • 아뇨 템플릿리터럴은 몰랐어요.. ㅎ 엽토군 2019.2.21 08:52
    • ㅎ.... 도움이 됬다니 기쁘군여...ㅎㅎ... 알 수 없는 사용자 2019.2.21 09:54
  • 애니메이션 delay를 주기 위한 용도 말고도 특별히 자바스크립트로 추가 컨트롤 하기 위한게 아니라면,

    (codepen 코드 기준) 자바스크립트에서 setTimeout을 사용하지 마시고 CSS의 transition-delay 를 사용하세요.

    CSS:

    .panel {
      transition-delay: 0.4s;
      /*...이하 생략...*/
    }
    

    javascript:

    // setInterval(function(){item.classList.add('open-active');},400);
    item.classList.add('open-active');
    

    자바스크립트 코드 내에서 엘리먼트의 클래스 이름으로 상태 관리를 해야 하는 상황이라면 setTimeout의 사용을 주의하지 않으면 상태가 꼬이게 됩니다.

    위 코드만으로 전체 기능을 다 고칠 수는 없고 더 다듬어야 하겠지만

    일반적으로 안전한 상태관리 코드를 위해서라면 비동기 로직을 삭제하고 대체가능한 방향으로 가시는게 좋아요.

    • 네 .. 에니메이션 효과를 주기위해서 사용할때는 setTimeout 대신에 transition-delay 를 쓰는게 좋다고 얼핏들었던것같은데 이렇게 시행착오를 겪으니 뼈저리게 느껴지네요.... 감사합니다!!!! 알 수 없는 사용자 2019.2.21 08:00
    • 그런데 질문이 하나 더 있습니다. 작성해주신것처럼 panel 안에 transition-delay를 넣으니 원하는 코드가 잘 작동이 되는데요. 알 수 없는 사용자 2019.2.21 08:41
    • .panel.open-active>*:first-child { transition-delay: 0.4s; transform: translateY(0); } .panel.open-active>*:last-child { transform: translateY(0); transition-delay: 0.4s; } 알 수 없는 사용자 2019.2.21 08:41
    • open이 추가 된 다음, transition-delay가 일어나야 하므로 .open-active 안에 넣어주어야하는것 아닌가요? .panel에 넣으면 open이 추가될때에도 딜레이가 발생하는것이지 않나요? ( 그런데 0.4초밖에 안되고 티가 잘 안나니까 그냥 한꺼번에 사용하는것인가요? ) 아니면 open이 추가될때는 딜레이가 발생하지 않는것인가요...? 알 수 없는 사용자 2019.2.21 08:42
    • 사실 해당 속성값이 들어가야야 할 정확한 위치는 코드를 자세히 보지 않아서 모릅니다만 아마도 말씀하신대로 다른곳에 넣어주는게 맞을거에요. 그래야 각각 다르게 적용되겠죠 doodoji 2019.2.21 11:06
    • 와... 알 수 없는 사용자 2019.2.22 02:17

답변을 하려면 로그인이 필요합니다.

프로그래머스 커뮤니티는 개발자들을 위한 Q&A 서비스입니다. 로그인해야 답변을 작성하실 수 있습니다.

(ಠ_ಠ)
(ಠ‿ಠ)