관련 지식
javascript, node.js, event-delegation-pattern, bubbling

HTML 문서는 수많은 태그의 조합으로 이루어져 있습니다. 어떤 태그는 눈에 직접적으로 보이는 효과를 제공하고 어떤 태그는 눈에 보이지 않지만 다른 요소를 감싸는 영역을 가지고 있기도 합니다.

인스펙터라는 말이 왠지 거창해 보입니다. 먼저 어떤 기능을 하는 것인지 보겠습니다.

여기서 보이는 DOM Inspector 는 ‘DOM 찾기’ 버튼을 클릭하면 마우스 아이콘 모양이 바뀌며 기능이 활성화 됩니다. 그때부턴 마우스의 움직임에 따라 마우스가 가리키는 DOM 영역에 빨간색 테두리가 생기며, 클릭을 하면 해당 영역의 HTML이 무엇인지 보여주고 원래대로 비활성화 됩니다.

이 기능은 개발자 도구에서 제공하는 기능에서 컨셉을 가져왔습니다.

정말로 쓸데없어 보이는 기능이지만 이 방법을 응용하여 사용자가 원하는 영역의 DOM만 정밀하게 다루는 방식 등 몇가지 활용 방법이 생길듯 합니다.

동작 순서

  1. ‘DOM 찾기’ 버튼을 클릭하면 마우스 아이콘이 바뀌고 인스펙터를 실행한다.
  2. 마우스가 가리키는 DOM은 빨간색 테두리가 생긴다
  3. 마우스의 포커스가 없어진 DOM은 빨간색 테두리를 없앤다.
  4. 마우스를 클릭하면 가리키는 DOM의 HTML 문자열을 보여주고 익스펙터를 종료한다.

동작 방식은 매우 단순합니다. 이벤트도 평소에 자주 사용하는 익숙한 이벤트일 것입니다. 그러나 이벤트를 제어해본 경험이 적고 이벤트의 특징을 모른다면 조금 당혹스러운 상황을 보실수 있습니다. 하나씩 진행해 보겠습니다.

마우스 바꾸기 & 새로운 클래스 추가하기

마우스 커서를 바꾸는 방법은 매우 간단합니다. 아래와 같이 사용하면 body 영역 안에서 마우스 커서는 십자 모양으로 적용됩니다.

  1. document.body.style.cursor = "crosshair";

마우스가 DOM에 포커스 되었을때 붉은색 테두리를 지정하는 방법은 간단하게 Inline Style을 수정시켜 적용하는 방법이 있습니다. 그러나 기존에 적용된 스타일이 있으면 핸들링하기 불편하므로 새로운 클래스를 하나 추가하여 클래스를 핸들링 할 것입니다.

  1. var style = document.createElement('style');
  2. style.type = 'text/css';
  3. style.innerHTML = '.overTargetClass { border: 2px solid red !important; }';
  4. document.getElementsByTagName('head')[0].appendChild(style);

위의 스크립트가 실행되면 overTargetClass 이름의 클래스를 가지는 style 태그가 <head> 영역에 추가 될 것입니다. 이제는 마우스 이벤트를 핸들링할 차례입니다.

이벤트 델리게이션 패턴

제가 작성했던 이전의 다른 글에서도 이벤트 델리게이션 패턴은 몇번 언급을 했습니다. 여기서도 마찬가지로 사용할 것입니다.

  1. var targets = "*";
  2. $("body").on("mouseenter", targets, activeFocus)
  3. .on("mouseleave", targets, deactiveFocus)
  4. .on("click", targets, onclick);

JQuery는 이벤트 델리게이션 패턴을 훌륭히 지원해주고 있고 이벤트를 받고 싶은 요소를 지정하면 해당 요소에서 이벤트가 발생했을때만 콜백 함수가 실행됩니다. 만약 a 나 p 태그등 일부 요소에 대해서만 이벤트를 받고 싶으면 targets만 변경하면 됩니다.

  1. var targets = "a, p";

mouseovermouseout 이벤트 대신 mouseentermouseleave를 사용했습니다. 두 가지의 차이점은 이벤트 버블링이 발생하느냐의 차이인데 아래 링크의 맨 밑에 있는 데모를 테스트 해보시면 이해가 더 쉬울것 같습니다.

https://api.jquery.com/mouseover/

그러나 여기서는 콜백함수에서 이벤트 버블링을 직접 막기 떄문에 mouseovermouseenter 어떤것을 사용해도 상관은 없습니다.

mouseenter(mouseover) 이벤트

마우스가 DOM 위로 올라가 있을때 발생하는 이벤트이고 이때 빨간색 테두리를 넣어야 합니다.

해야할 일은 아래와 같습니다.

  1. 이벤트 버블링 막기
    이벤트 버블링이 발생한다면 실제로 마우스가 가리키는 DOM 뿐만 아니라 상위의 부모 DOM 에서도 이벤트가 발생하므로 버블링을 막아야 합니다. mouseenter 이벤트를 사용하더라도 수동으로 막아주는것이 좋습니다.
  2. 빨간색 테두리 추가(클래스 추가)
    이벤트가 발생한 DOM에 overTargetClass 를 추가하여 빨간색 테두리를 만듭니다.
  3. onclick 이벤트 변경
    필수는 아닙니다. 하드코딩된 onclick 이벤트는 인스펙터에서 사용할 클릭 이벤트와 같이 실행될 수 있으므로 속성을 잠시 바꿔줍니다.
  4. 부모 DOM 에서 빨간색 테두리 제거
    밑에서 다시 언급하겠습니다.

이를 구현한 소스는 아래와 같습니다.

  1. function activeFocus(e) {
  2. if(e)
  3. e.stopPropagation(); //이벤트 버블링 막기
  4. var $this = $(this);
  5. $this.addClass("overTargetClass"); //빨간색 테두리 추가
  6. var clickDefine = $this.attr("onclick"); //하드코딩된 onclick 이벤트는 잠시 다른이름으로 변경
  7. if(clickDefine) {
  8. $this.attr("data-org-click", clickDefine);
  9. $this.removeAttr("onclick");
  10. }
  11. $this.parents(targets).each(function() { //부모 DOM의 테두리 제거
  12. deactiveFocus.call(this);
  13. });
  14. }

부모 DOM의 테두리를 제거하는 이유?

만약 아래와 같은 div가 있다고 했을때 div1 마우스를 올리면 mouseenter 이벤트가 발생하며 빨간색 테두리가 그려질 것입니다.

마우스를 div1에서 div2로 이동시키면 div2 에서 mouseenter 이벤트가 발생할 것입니다. 그러나 div1에서 mouseleave 이벤트가 발생하진 않습니다. 따라서 mouseenter이벤트 발생시 부모 요소의 DOM을 찾아 테두리를 삭제하는 로직이 필요합니다.

mouseleave(mouseout) 이벤트

마우스가 DOM에서 벗어났을때 발생하는 이벤트 입니다. overTargetClass 클래스를 제거하여 빨간색 테두리를 없애줍니다. mouseenter 이벤트때 적용한 모든 효과를 원래대로 되돌리면 되겠습니다.

  1. function deactiveFocus(e) {
  2. if(e)
  3. e.stopPropagation();
  4. var $this = $(this);
  5. $this.removeClass("overTargetClass"); //빨간색 테두리 제거
  6. var clickDefine = $this.attr("data-org-click"); //이름을 바꿔놨던 onclick 이벤트 복구
  7. if(clickDefine) {
  8. $this.attr("onclick", clickDefine);
  9. $this.removeAttr("data-org-click");
  10. }
  11. }

click 이벤트

특정 DOM을 클릭했을때 DOM의 HTML 소스를 보여준 후 인스펙터 모드는 종료되어야 합니다. 처음에 ‘DOM 찾기’ 버튼을 클릭하여 마우스 커서 변경, 이벤트 델리게이션 패턴으로 이벤트 등록했던 것을 모두 없애야 하는 시점이죠.

readyClick 변수를 이용한것을 주목할 필요가 있습니다. 최초 클릭시 이벤트 중복을 막기위해 사용한 것입니다.

  1. function onclick(e) {
  2. e.preventDefault();
  3. e.stopPropagation();
  4. if(readyClick) {
  5. document.body.style.cursor = 'default';
  6. deactiveFocus.call(this, e);
  7. $("body").off("mouseenter", activeFocus)
  8. .off("mouseleave", deactiveFocus)
  9. .off("click", onclick);
  10. cb(this);
  11. return;
  12. }
  13. readyClick = true;
  14. }

정리

처음에 봤던 기능을 구현하는데 많은 코드가 필요하진 않습니다. 하지만 이벤트에 대한 이해도가 부족하다면 비슷한 기능을 구현하는데 시간이 적지 않게 소요될 것입니다. DOM을 조작할땐 확실히 JQuery가 편하다보니 바닐라JS가 마구 뒤섞인 코드가 되었는데 코드 자체는 매우 심플해서 보기에 어렵진 않으실 겁니다. 최종 소스를 참고해서 소스를 조금씩 변경하여 이벤트가 어떻게 동작하는지 확인해보면 좋을것 같습니다.

최종 소스

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <title>CSS Template</title>
  5. <meta charset="utf-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1">
  7. <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
  8. <style>
  9. * {
  10. box-sizing: border-box;
  11. }
  12. body {
  13. font-family: Arial, Helvetica, sans-serif;
  14. }
  15. /* Style the header */
  16. header {
  17. background-color: #666;
  18. padding: 30px;
  19. text-align: center;
  20. font-size: 35px;
  21. color: white;
  22. }
  23. /* Create two columns/boxes that floats next to each other */
  24. nav {
  25. float: left;
  26. width: 30%;
  27. height: 300px; /* only for demonstration, should be removed */
  28. background: #ccc;
  29. padding: 20px;
  30. }
  31. /* Style the list inside the menu */
  32. nav ul {
  33. list-style-type: none;
  34. padding: 0;
  35. }
  36. article {
  37. float: left;
  38. padding: 20px;
  39. width: 70%;
  40. background-color: #f1f1f1;
  41. height: 300px; /* only for demonstration, should be removed */
  42. }
  43. /* Clear floats after the columns */
  44. section:after {
  45. content: "";
  46. display: table;
  47. clear: both;
  48. }
  49. /* Style the footer */
  50. footer {
  51. background-color: #777;
  52. padding: 10px;
  53. text-align: center;
  54. color: white;
  55. }
  56. /* Responsive layout - makes the two columns/boxes stack on top of each other instead of next to each other, on small screens */
  57. @media (max-width: 600px) {
  58. nav, article {
  59. width: 100%;
  60. height: auto;
  61. }
  62. }
  63. </style>
  64. </head>
  65. <body>
  66. <input type="button" id="btn" value="DOM 찾기" />
  67. <h2>CSS Layout Float</h2>
  68. <p>In this example, we have created a header, two columns/boxes and a footer. On smaller screens, the columns will stack on top of each other.</p>
  69. <p>Resize the browser window to see the responsive effect (you will learn more about this in our next chapter - HTML Responsive.)</p>
  70. <header>
  71. <h2>Cities</h2>
  72. </header>
  73. <section>
  74. <nav>
  75. <ul>
  76. <li><a href="#">London</a></li>
  77. <li><a href="#">Paris</a></li>
  78. <li><a href="#">Tokyo</a></li>
  79. </ul>
  80. </nav>
  81. <article>
  82. <h1>London</h1>
  83. <p>London is the capital city of England. It is the most populous city in the United Kingdom, with a metropolitan area of over 13 million inhabitants.</p>
  84. <p>Standing on the River Thames, London has been a major settlement for two millennia, its history going back to its founding by the Romans, who named it Londinium.</p>
  85. </article>
  86. </section>
  87. <footer>
  88. <p>Footer</p>
  89. </footer>
  90. <script>
  91. $("#btn").on("click", function() {
  92. domInspector(function(obj) {
  93. alert(obj.outerHTML);
  94. });
  95. });
  96. var domInspector = function(cb) {
  97. document.body.style.cursor = "crosshair";
  98. var style = document.createElement('style');
  99. style.type = 'text/css';
  100. style.innerHTML = '.overTargetClass { border: 2px solid red !important; }';
  101. document.getElementsByTagName('head')[0].appendChild(style);
  102. var targets = "*";
  103. var readyClick = false;
  104. $("body").on("mouseenter", targets, activeFocus)
  105. .on("mouseleave", targets, deactiveFocus)
  106. .on("click", targets, onclick);
  107. function onclick(e) {
  108. e.preventDefault();
  109. e.stopPropagation();
  110. if(readyClick) {
  111. document.body.style.cursor = 'default';
  112. deactiveFocus.call(this, e);
  113. $("body").off("mouseenter", activeFocus)
  114. .off("mouseleave", deactiveFocus)
  115. .off("click", onclick);
  116. cb(this);
  117. return;
  118. }
  119. readyClick = true;
  120. }
  121. function activeFocus(e) {
  122. if(e)
  123. e.stopPropagation();
  124. var $this = $(this);
  125. $this.addClass("overTargetClass");
  126. var clickDefine = $this.attr("onclick"); //하드코딩된 onclick 이벤트는 잠시 다른이름으로 변경
  127. if(clickDefine) {
  128. $this.attr("data-org-click", clickDefine);
  129. $this.removeAttr("onclick");
  130. }
  131. $this.parents(targets).each(function() {
  132. deactiveFocus.call(this);
  133. });
  134. }
  135. function deactiveFocus(e) {
  136. if(e)
  137. e.stopPropagation();
  138. var $this = $(this);
  139. $this.removeClass("overTargetClass");
  140. var clickDefine = $this.attr("data-org-click"); //이름을 바꿔놨던 onclick 이벤트 복구
  141. if(clickDefine) {
  142. $this.attr("onclick", clickDefine);
  143. $this.removeAttr("data-org-click");
  144. }
  145. }
  146. }
  147. </script>
  148. </body>
  149. </html>